From 8fcb76ac8d120bdd29f53207c14e9cb3cfdd7128 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 20:57:26 +0200 Subject: [PATCH 01/42] Use Rustix::Errno to unify error code mapping. --- crates/wasi/src/preview2/host/network.rs | 148 +++++++++++++++-------- 1 file changed, 98 insertions(+), 50 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 023a5d9f3026..b37de3297d9c 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -1,9 +1,12 @@ +use rustix::io::Errno; + use crate::preview2::bindings::sockets::network::{ self, ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4Address, Ipv4SocketAddress, Ipv6Address, Ipv6SocketAddress, }; use crate::preview2::network::TableNetworkExt; use crate::preview2::{TableError, WasiView}; +use anyhow::anyhow; use std::io; impl network::Host for T { @@ -16,67 +19,112 @@ impl network::Host for T { } } + +/// Unfortunately, Rust's io::ErrorKind is missing more than half of the relevant error codes. +/// This trait provides access to a unified error code. +pub(crate) trait SystemError: std::error::Error { + fn errno(&self) -> Option; +} + +impl SystemError for Errno { + fn errno(&self) -> Option { + Some(*self) + } +} + +impl SystemError for std::io::Error { + fn errno(&self) -> Option { + if let Some(errno) = Errno::from_io_error(self) { + return Some(errno); + } + + // Error is probably synthesized in Rust code. Luckily, the errors kinds map pretty straightforward back to native error codes. + match self.kind() { + std::io::ErrorKind::AddrInUse => Some(Errno::ADDRINUSE), + std::io::ErrorKind::AddrNotAvailable => Some(Errno::ADDRNOTAVAIL), + std::io::ErrorKind::AlreadyExists => Some(Errno::EXIST), + std::io::ErrorKind::BrokenPipe => Some(Errno::PIPE), + std::io::ErrorKind::ConnectionAborted => Some(Errno::CONNABORTED), + std::io::ErrorKind::ConnectionRefused => Some(Errno::CONNREFUSED), + std::io::ErrorKind::ConnectionReset => Some(Errno::CONNRESET), + std::io::ErrorKind::Interrupted => Some(Errno::INTR), + std::io::ErrorKind::InvalidInput => Some(Errno::INVAL), + std::io::ErrorKind::NotConnected => Some(Errno::NOTCONN), + std::io::ErrorKind::NotFound => Some(Errno::NOENT), + std::io::ErrorKind::OutOfMemory => Some(Errno::NOMEM), + std::io::ErrorKind::PermissionDenied => Some(Errno::ACCESS), // Alternative: EPERM + std::io::ErrorKind::TimedOut => Some(Errno::TIMEDOUT), + std::io::ErrorKind::Unsupported => Some(Errno::NOTSUP), + std::io::ErrorKind::WouldBlock => Some(Errno::WOULDBLOCK), // Alternative: EAGAIN + + _ => None, + } + } +} + + impl From for network::Error { fn from(error: TableError) -> Self { Self::trap(error.into()) } } -impl From for network::Error { - fn from(error: io::Error) -> Self { - match error.kind() { - // Errors that we can directly map. - io::ErrorKind::PermissionDenied => ErrorCode::AccessDenied, - io::ErrorKind::ConnectionRefused => ErrorCode::ConnectionRefused, - io::ErrorKind::ConnectionReset => ErrorCode::ConnectionReset, - io::ErrorKind::NotConnected => ErrorCode::NotConnected, - io::ErrorKind::AddrInUse => ErrorCode::AddressInUse, - io::ErrorKind::AddrNotAvailable => ErrorCode::AddressNotBindable, - io::ErrorKind::WouldBlock => ErrorCode::WouldBlock, - io::ErrorKind::TimedOut => ErrorCode::Timeout, - io::ErrorKind::Unsupported => ErrorCode::NotSupported, - io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory, - - // Errors we don't expect to see here. - io::ErrorKind::Interrupted | io::ErrorKind::ConnectionAborted => { - // Transient errors should be skipped. - return Self::trap(error.into()); - } - - // Errors not expected from network APIs. - io::ErrorKind::WriteZero - | io::ErrorKind::InvalidInput - | io::ErrorKind::InvalidData - | io::ErrorKind::BrokenPipe - | io::ErrorKind::NotFound - | io::ErrorKind::UnexpectedEof - | io::ErrorKind::AlreadyExists => return Self::trap(error.into()), - - // Errors that don't correspond to a Rust `io::ErrorKind`. - io::ErrorKind::Other => match error.raw_os_error() { - None => return Self::trap(error.into()), - Some(libc::ENOBUFS) | Some(libc::ENOMEM) => ErrorCode::OutOfMemory, - Some(libc::EOPNOTSUPP) => ErrorCode::NotSupported, - Some(libc::ENETUNREACH) | Some(libc::EHOSTUNREACH) | Some(libc::ENETDOWN) => { - ErrorCode::RemoteUnreachable - } - Some(libc::ECONNRESET) => ErrorCode::ConnectionReset, - Some(libc::ECONNREFUSED) => ErrorCode::ConnectionRefused, - Some(libc::EADDRINUSE) => ErrorCode::AddressInUse, - Some(_) => return Self::trap(error.into()), - }, - _ => return Self::trap(error.into()), +impl From for network::Error { + fn from(error: T) -> Self { + let errno = match error.errno() { + Some(errno) => errno, + None => return Self::trap(anyhow!("Unknown network error: {:?}", error)), + }; + + match errno { + Errno::WOULDBLOCK => ErrorCode::WouldBlock, + #[allow(unreachable_patterns)] // EWOULDBLOCK and EAGAIN can have the same value. + Errno::AGAIN => ErrorCode::WouldBlock, + Errno::INTR => ErrorCode::WouldBlock, + #[cfg(not(windows))] + Errno::PERM => ErrorCode::AccessDenied, + Errno::ACCESS => ErrorCode::AccessDenied, + Errno::ADDRINUSE => ErrorCode::AddressInUse, + Errno::ADDRNOTAVAIL => ErrorCode::AddressNotBindable, + Errno::ALREADY => ErrorCode::ConcurrencyConflict, + Errno::TIMEDOUT => ErrorCode::Timeout, + Errno::CONNREFUSED => ErrorCode::ConnectionRefused, + Errno::CONNRESET => ErrorCode::ConnectionReset, + // Errno::CONNABORTED => ErrorCode::Todo, // FIXME + // Errno::INVAL => ErrorCode::Todo, // FIXME + Errno::HOSTUNREACH => ErrorCode::RemoteUnreachable, + Errno::HOSTDOWN => ErrorCode::RemoteUnreachable, + Errno::NETDOWN => ErrorCode::RemoteUnreachable, + Errno::NETUNREACH => ErrorCode::RemoteUnreachable, + #[cfg(target_os = "linux")] + Errno::NONET => ErrorCode::RemoteUnreachable, + Errno::ISCONN => ErrorCode::AlreadyConnected, + Errno::NOTCONN => ErrorCode::NotConnected, + Errno::DESTADDRREQ => ErrorCode::NotConnected, + #[cfg(not(windows))] + Errno::NFILE => ErrorCode::NewSocketLimit, + Errno::MFILE => ErrorCode::NewSocketLimit, + Errno::MSGSIZE => ErrorCode::DatagramTooLarge, + #[cfg(not(windows))] + Errno::NOMEM => ErrorCode::OutOfMemory, + Errno::NOBUFS => ErrorCode::OutOfMemory, + Errno::OPNOTSUPP => ErrorCode::NotSupported, + Errno::NOPROTOOPT => ErrorCode::NotSupported, + Errno::PFNOSUPPORT => ErrorCode::NotSupported, + Errno::PROTONOSUPPORT => ErrorCode::NotSupported, + Errno::PROTOTYPE => ErrorCode::NotSupported, + Errno::SOCKTNOSUPPORT => ErrorCode::NotSupported, + Errno::AFNOSUPPORT => ErrorCode::NotSupported, + + // Trap on unexpected errors. These include: + // - EINPROGRESS: Should have been handled by connect. + // - ENOTSOCK, EFAULT, EBADF, EBADFD, ENOSYS: Implementation error on our side. + _ => return network::Error::trap(anyhow!("Unexpected error: {:?}", error)), } .into() } } -impl From for network::Error { - fn from(error: rustix::io::Errno) -> Self { - std::io::Error::from(error).into() - } -} - impl From for std::net::SocketAddr { fn from(addr: IpSocketAddress) -> Self { match addr { From a620fd0beed879b1d7c012a883bb99d43a6d3f06 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 21:20:27 +0200 Subject: [PATCH 02/42] Clarify Connect failure state --- crates/wasi/src/preview2/host/tcp.rs | 5 ++++- crates/wasi/src/preview2/tcp.rs | 3 +++ crates/wasi/wit/deps/sockets/tcp.wit | 9 +++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 89f9bc240207..49ecc4577a03 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -133,7 +133,10 @@ impl tcp::Host for T { // Check whether the connect succeeded. match sockopt::get_socket_error(socket.tcp_socket()) { Ok(Ok(())) => {} - Err(err) | Ok(Err(err)) => return Err(err.into()), + Err(err) | Ok(Err(err)) => { + socket.tcp_state = HostTcpState::ConnectFailed; + return Err(err.into()); + }, } } _ => return Err(ErrorCode::NotInProgress.into()), diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 9721e2c3c37a..0157774dcbaa 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -35,6 +35,9 @@ pub(crate) enum HostTcpState { /// An outgoing connection is ready to be established. ConnectReady, + /// An outgoing connection was attempted but failed. + ConnectFailed, + /// An outgoing connection has been established. Connected, } diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 3922769b308e..a61bfda5aa75 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -57,6 +57,15 @@ interface tcp { /// - the socket is transitioned into the Connection state /// - a pair of streams is returned that can be used to read & write to the connection /// + /// POSIX mentions: + /// > If connect() fails, the state of the socket is unspecified. Conforming applications should + /// > close the file descriptor and create a new socket before attempting to reconnect. + /// + /// WASI prescribes the following behavior: + /// - If `connect` fails because an input/state validation error, the socket should remain usable. + /// - If a connection was actually attempted but failed, the socket should become unusable for further network communication. + /// Besides `drop`, any method after such a failure may return an error. + /// /// # Typical `start` errors /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) From 17c8b8e84c725b19e74f0983376594f9256b1225 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 21:43:48 +0200 Subject: [PATCH 03/42] Allow accept() to return transient errors. The original provision was added to align with preview3 streams that may only fail once. However, after discussing with Dan Gohman, we came to the conclusion that a stream of result<> could do the trick fine too. Fixes: https://github.com/WebAssembly/wasi-sockets/issues/22 --- crates/wasi/src/preview2/host/network.rs | 2 +- crates/wasi/wit/deps/sockets/network.wit | 3 +++ crates/wasi/wit/deps/sockets/tcp.wit | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index b37de3297d9c..234e190044e7 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -90,7 +90,7 @@ impl From for network::Error { Errno::TIMEDOUT => ErrorCode::Timeout, Errno::CONNREFUSED => ErrorCode::ConnectionRefused, Errno::CONNRESET => ErrorCode::ConnectionReset, - // Errno::CONNABORTED => ErrorCode::Todo, // FIXME + Errno::CONNABORTED => ErrorCode::ConnectionAborted, // Errno::INVAL => ErrorCode::Todo, // FIXME Errno::HOSTUNREACH => ErrorCode::RemoteUnreachable, Errno::HOSTDOWN => ErrorCode::RemoteUnreachable, diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index a198ea8017de..3db4daa192d9 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -130,6 +130,9 @@ interface network { /// The connection was reset. connection-reset, + + /// A connection was aborted. + connection-aborted, // ### UDP SOCKET ERRORS ### diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index a61bfda5aa75..7d9c0b79ee0e 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -136,8 +136,8 @@ interface tcp { /// # Typical errors /// - `not-listening`: Socket is not in the Listener state. (EINVAL) /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// - /// Host implementations must skip over transient errors returned by the native accept syscall. + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References /// - From 3aa2bbb4fa466e63b793dce2c3b9a59546d1b0e1 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 21:55:09 +0200 Subject: [PATCH 04/42] Fold `ephemeral-ports-exhausted` into `address-in-use` There is no cross-platform way to know the distinction between them --- crates/wasi/wit/deps/sockets/network.wit | 5 +---- crates/wasi/wit/deps/sockets/tcp.wit | 6 +++--- crates/wasi/wit/deps/sockets/udp.wit | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index 3db4daa192d9..b4c209c9aa69 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -107,12 +107,9 @@ interface network { /// A bind operation failed because the provided address is not an address that the `network` can bind to. address-not-bindable, - /// A bind operation failed because the provided address is already in use. + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. address-in-use, - /// A bind operation failed because there are no ephemeral ports available. - ephemeral-ports-exhausted, - /// The remote address is not reachable remote-unreachable, diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 7d9c0b79ee0e..f6981ecc19bb 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -37,7 +37,7 @@ interface tcp { /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. @@ -80,7 +80,7 @@ interface tcp { /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) /// - `connection-reset`: The connection was reset. (ECONNRESET) /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// @@ -110,7 +110,7 @@ interface tcp { /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) /// - `not-in-progress`: A `listen` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index 700b9e247692..842c5a16ab1d 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -38,7 +38,7 @@ interface udp { /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. @@ -72,7 +72,7 @@ interface udp { /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// From 615847888a166284b416b1b8972ec29e871cb22c Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 22:07:36 +0200 Subject: [PATCH 05/42] Clarify `local-address` behavior on unbound socket --- crates/wasi/src/preview2/host/tcp.rs | 7 +++++++ crates/wasi/wit/deps/sockets/tcp.wit | 6 ++++++ crates/wasi/wit/deps/sockets/udp.wit | 6 ++++++ 3 files changed, 19 insertions(+) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 49ecc4577a03..11f4db73293c 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -226,6 +226,13 @@ impl tcp::Host for T { fn local_address(&mut self, this: tcp::TcpSocket) -> Result { let table = self.table(); let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Default => return Err(ErrorCode::NotBound.into()), + HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), + _ => {} + } + let addr = socket .tcp_socket() .as_socketlike_view::() diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index f6981ecc19bb..a182a453f7c1 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -148,6 +148,12 @@ interface tcp { /// Get the bound local address. /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `not-bound` when the socket hasn't been bound yet. + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. /// diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index 842c5a16ab1d..d8bbc57f10b8 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -143,6 +143,12 @@ interface udp { /// Get the current bound address. /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `not-bound` when the socket hasn't been bound yet. + /// /// # Typical errors /// - `not-bound`: The socket is not bound to any local address. /// From 184ae308348b267a3e85145ca769a5f91fd9d89a Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 21 Sep 2023 22:23:12 +0200 Subject: [PATCH 06/42] Remove `concurrency-conflict` clutter, and just document it to be always possible. --- crates/wasi/wit/deps/sockets/network.wit | 3 +++ crates/wasi/wit/deps/sockets/tcp.wit | 13 ------------- crates/wasi/wit/deps/sockets/udp.wit | 9 --------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index b4c209c9aa69..cc9f608304d0 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -23,6 +23,7 @@ interface network { /// - `access-denied` /// - `not-supported` /// - `out-of-memory` + /// - `concurrency-conflict` /// /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. enum error-code { @@ -50,6 +51,8 @@ interface network { timeout, /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY concurrency-conflict, /// Trying to finish an asynchronous operation that: diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index a182a453f7c1..01b2130d9f68 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -34,7 +34,6 @@ interface tcp { /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) @@ -73,7 +72,6 @@ interface tcp { /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `already-connected`: The socket is already in the Connection state. (EISCONN) /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) @@ -107,7 +105,6 @@ interface tcp { /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) /// - `already-listening`: The socket is already in the Listener state. - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) /// /// # Typical `finish` errors /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) @@ -189,7 +186,6 @@ interface tcp { /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) ipv6-only: func(this: tcp-socket) -> result set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> @@ -197,20 +193,13 @@ interface tcp { /// /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) keep-alive: func(this: tcp-socket) -> result set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) no-delay: func(this: tcp-socket) -> result set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> @@ -219,7 +208,6 @@ interface tcp { /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) unicast-hop-limit: func(this: tcp-socket) -> result set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> @@ -237,7 +225,6 @@ interface tcp { /// # Typical errors /// - `already-connected`: (set) The socket is already in the Connection state. /// - `already-listening`: (set) The socket is already in the Listener state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) receive-buffer-size: func(this: tcp-socket) -> result set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> send-buffer-size: func(this: tcp-socket) -> result diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index d8bbc57f10b8..e98eda017207 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -35,7 +35,6 @@ interface udp { /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) /// - `already-bound`: The socket is already bound. (EINVAL) - /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) @@ -69,7 +68,6 @@ interface udp { /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) /// /// # Typical `finish` errors /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) @@ -184,14 +182,10 @@ interface udp { /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `already-bound`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) ipv6-only: func(this: udp-socket) -> result set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) unicast-hop-limit: func(this: udp-socket) -> result set-unicast-hop-limit: func(this: udp-socket, value: u8) -> result<_, error-code> @@ -205,9 +199,6 @@ interface udp { /// for internal metadata structures. /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) receive-buffer-size: func(this: udp-socket) -> result set-receive-buffer-size: func(this: udp-socket, value: u64) -> result<_, error-code> send-buffer-size: func(this: udp-socket) -> result From 72ec5e4dffe814a11bdabbd35bf876301f8ff0a3 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 23 Sep 2023 11:01:12 +0200 Subject: [PATCH 07/42] Simplify state errors. They were unnecessarily detailed and mostly have no standardized equivalent in POSIX, so wasi-libc will probably just map them all back into a single EOPNOTSUPP or EINVAL or ... EISCONN/ENOTCONN can be derived in wasi-libc based on context and/or by checking `remote-address`. For example, `shutdown` can only be called on connected sockets, so if it returns `invalid-state` it can be unambiguously mapped to ENOTCONN. --- crates/wasi/src/preview2/host/network.rs | 6 +-- crates/wasi/src/preview2/host/tcp.rs | 54 +++++++++++++++++++----- crates/wasi/wit/deps/sockets/network.wit | 21 ++------- crates/wasi/wit/deps/sockets/tcp.wit | 36 ++++++++-------- crates/wasi/wit/deps/sockets/udp.wit | 14 +++--- 5 files changed, 74 insertions(+), 57 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 234e190044e7..0eea6672c721 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -98,9 +98,9 @@ impl From for network::Error { Errno::NETUNREACH => ErrorCode::RemoteUnreachable, #[cfg(target_os = "linux")] Errno::NONET => ErrorCode::RemoteUnreachable, - Errno::ISCONN => ErrorCode::AlreadyConnected, - Errno::NOTCONN => ErrorCode::NotConnected, - Errno::DESTADDRREQ => ErrorCode::NotConnected, + Errno::ISCONN => ErrorCode::InvalidState, + Errno::NOTCONN => ErrorCode::InvalidState, + Errno::DESTADDRREQ => ErrorCode::InvalidState, #[cfg(not(windows))] Errno::NFILE => ErrorCode::NewSocketLimit, Errno::MFILE => ErrorCode::NewSocketLimit, diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 11f4db73293c..d761662a98e7 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -29,7 +29,8 @@ impl tcp::Host for T { match socket.tcp_state { HostTcpState::Default => {} - _ => return Err(ErrorCode::NotInProgress.into()), + HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), + _ => return Err(ErrorCode::InvalidState.into()), } let network = table.get_network(network)?; @@ -72,8 +73,14 @@ impl tcp::Host for T { match socket.tcp_state { HostTcpState::Default => {} - HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + HostTcpState::Bound + | HostTcpState::Connected + | HostTcpState::ConnectFailed + | HostTcpState::Listening => return Err(ErrorCode::InvalidState.into()), + HostTcpState::Connecting + | HostTcpState::ConnectReady + | HostTcpState::ListenStarted + | HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), } let network = table.get_network(network)?; @@ -136,7 +143,7 @@ impl tcp::Host for T { Err(err) | Ok(Err(err)) => { socket.tcp_state = HostTcpState::ConnectFailed; return Err(err.into()); - }, + } } } _ => return Err(ErrorCode::NotInProgress.into()), @@ -156,9 +163,14 @@ impl tcp::Host for T { match socket.tcp_state { HostTcpState::Bound => {} - HostTcpState::ListenStarted => return Err(ErrorCode::AlreadyListening.into()), - HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + HostTcpState::Default + | HostTcpState::Connected + | HostTcpState::ConnectFailed + | HostTcpState::Listening => return Err(ErrorCode::InvalidState.into()), + HostTcpState::ListenStarted + | HostTcpState::Connecting + | HostTcpState::ConnectReady + | HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), } socket @@ -194,8 +206,7 @@ impl tcp::Host for T { match socket.tcp_state { HostTcpState::Listening => {} - HostTcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + _ => return Err(ErrorCode::InvalidState.into()), } // Do the OS accept call. @@ -228,7 +239,7 @@ impl tcp::Host for T { let socket = table.get_tcp_socket(this)?; match socket.tcp_state { - HostTcpState::Default => return Err(ErrorCode::NotBound.into()), + HostTcpState::Default => return Err(ErrorCode::InvalidState.into()), HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), _ => {} } @@ -243,6 +254,12 @@ impl tcp::Host for T { fn remote_address(&mut self, this: tcp::TcpSocket) -> Result { let table = self.table(); let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Connected => {} + _ => return Err(ErrorCode::InvalidState.into()), + } + let addr = socket .tcp_socket() .as_socketlike_view::() @@ -304,6 +321,13 @@ impl tcp::Host for T { fn set_ipv6_only(&mut self, this: tcp::TcpSocket, value: bool) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; + + match socket.tcp_state { + HostTcpState::Default => {} + HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), + _ => return Err(ErrorCode::InvalidState.into()), + } + Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?) } @@ -317,7 +341,7 @@ impl tcp::Host for T { match socket.tcp_state { HostTcpState::Listening => {} - _ => return Err(ErrorCode::NotInProgress.into()), + _ => return Err(ErrorCode::InvalidState.into()), } let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; @@ -465,6 +489,14 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; + match socket.tcp_state { + HostTcpState::Connected => {} + HostTcpState::Connecting | HostTcpState::ConnectReady => { + return Err(ErrorCode::ConcurrencyConflict.into()) + } + _ => return Err(ErrorCode::InvalidState.into()), + } + let how = match shutdown_type { ShutdownType::Receive => std::net::Shutdown::Read, ShutdownType::Send => std::net::Shutdown::Write, diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index cc9f608304d0..cf4eca102718 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -89,24 +89,15 @@ interface network { // ### TCP & UDP SOCKET ERRORS ### + /// The operation is not valid in the socket's current state. + invalid-state, + /// A new socket resource could not be created because of a system limit. new-socket-limit, /// The socket is already attached to another network. already-attached, - /// The socket is already bound. - already-bound, - - /// The socket is already in the Connection state. - already-connected, - - /// The socket is not bound to any local address. - not-bound, - - /// The socket is not in the Connection state. - not-connected, - /// A bind operation failed because the provided address is not an address that the `network` can bind to. address-not-bindable, @@ -118,12 +109,6 @@ interface network { // ### TCP SOCKET ERRORS ### - - /// The socket is already in the Listener state. - already-listening, - - /// The socket is already in the Listener state. - not-listening, /// The connection was forcefully rejected connection-refused, diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 01b2130d9f68..86e93a25bf33 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -33,7 +33,7 @@ interface tcp { /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) - /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) @@ -70,8 +70,8 @@ interface tcp { /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - /// - `already-connected`: The socket is already in the Connection state. (EISCONN) - /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) + /// - `invalid-state`: The socket is already in the Connection state. (EISCONN) + /// - `invalid-state`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) @@ -102,9 +102,9 @@ interface tcp { /// - the socket must already be explicitly bound. /// /// # Typical `start` errors - /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) - /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) - /// - `already-listening`: The socket is already in the Listener state. + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the Listener state. /// /// # Typical `finish` errors /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) @@ -131,7 +131,7 @@ interface tcp { /// `tcp-socket` is dropped before its child streams are dropped. /// /// # Typical errors - /// - `not-listening`: Socket is not in the Listener state. (EINVAL) + /// - `invalid-state`: Socket is not in the Listener state. (EINVAL) /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) @@ -149,10 +149,10 @@ interface tcp { /// > If the socket has not been bound to a local name, the value /// > stored in the object pointed to by `address` is unspecified. /// - /// WASI is stricter and requires `local-address` to return `not-bound` when the socket hasn't been bound yet. + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. + /// - `invalid-state`: The socket is not bound to any local address. /// /// # References /// - @@ -161,10 +161,10 @@ interface tcp { /// - local-address: func(this: tcp-socket) -> result - /// Get the bound remote address. + /// Get the remote address. /// /// # Typical errors - /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) /// /// # References /// - @@ -184,7 +184,7 @@ interface tcp { /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. - /// - `already-bound`: (set) The socket is already bound. + /// - `invalid-state`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) ipv6-only: func(this: tcp-socket) -> result set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> @@ -192,7 +192,7 @@ interface tcp { /// Hints the desired listen queue size. Implementations are free to ignore this. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. + /// - `invalid-state`: (set) The socket is already in the Connection state. set-listen-backlog-size: func(this: tcp-socket, value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. @@ -206,8 +206,8 @@ interface tcp { /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `invalid-state`: (set) The socket is already in the Connection state. + /// - `invalid-state`: (set) The socket is already in the Listener state. unicast-hop-limit: func(this: tcp-socket) -> result set-unicast-hop-limit: func(this: tcp-socket, value: u8) -> result<_, error-code> @@ -223,8 +223,8 @@ interface tcp { /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `already-listening`: (set) The socket is already in the Listener state. + /// - `invalid-state`: (set) The socket is already in the Connection state. + /// - `invalid-state`: (set) The socket is already in the Listener state. receive-buffer-size: func(this: tcp-socket) -> result set-receive-buffer-size: func(this: tcp-socket, value: u64) -> result<_, error-code> send-buffer-size: func(this: tcp-socket) -> result @@ -252,7 +252,7 @@ interface tcp { /// The shutdown function does not close (drop) the socket. /// /// # Typical errors - /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) + /// - `invalid-state`: The socket is not in the Connection state. (ENOTCONN) /// /// # References /// - diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index e98eda017207..871dbbd8914b 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -34,7 +34,7 @@ interface udp { /// /// # Typical `start` errors /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) - /// - `already-bound`: The socket is already bound. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) @@ -89,7 +89,7 @@ interface udp { /// If `max-results` is 0, this function returns successfully with an empty list. /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. (EINVAL) + /// - `invalid-state`: The socket is not bound to any local address. (EINVAL) /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) /// @@ -123,7 +123,7 @@ interface udp { /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) /// - `already-connected`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) - /// - `not-bound`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. + /// - `invalid-state`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) @@ -145,10 +145,10 @@ interface udp { /// > If the socket has not been bound to a local name, the value /// > stored in the object pointed to by `address` is unspecified. /// - /// WASI is stricter and requires `local-address` to return `not-bound` when the socket hasn't been bound yet. + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. + /// - `invalid-state`: The socket is not bound to any local address. /// /// # References /// - @@ -160,7 +160,7 @@ interface udp { /// Get the address set with `connect`. /// /// # Typical errors - /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) /// /// # References /// - @@ -180,7 +180,7 @@ interface udp { /// /// # Typical errors /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. - /// - `already-bound`: (set) The socket is already bound. + /// - `invalid-state`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) ipv6-only: func(this: udp-socket) -> result set-ipv6-only: func(this: udp-socket, value: bool) -> result<_, error-code> From 24a3712990aceae3d703f3d39ff10fd50550cab6 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 23 Sep 2023 11:17:18 +0200 Subject: [PATCH 08/42] Document that connect may return ECONNABORTED --- crates/wasi/wit/deps/sockets/tcp.wit | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 86e93a25bf33..0b09b11d13f0 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -77,6 +77,7 @@ interface tcp { /// - `timeout`: Connection timed out. (ETIMEDOUT) /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. From 70c79ff8bd658dd246f01821f57dba7fa396bd14 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Tue, 26 Sep 2023 16:44:57 +0200 Subject: [PATCH 09/42] Remove create-tcp/udp-socket not supported errors. These stem from back when the entire wasi-sockets proposal was one big single thing. In this day and age, when an implementation doesn't want to support TCP and/or UDP, it can simply _not_ implement that interface, rather than returning an error at runtime. --- crates/wasi/wit/deps/sockets/tcp-create-socket.wit | 1 - crates/wasi/wit/deps/sockets/udp-create-socket.wit | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit index f43bc8979047..c7cce0ef745c 100644 --- a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit @@ -14,7 +14,6 @@ interface tcp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// diff --git a/crates/wasi/wit/deps/sockets/udp-create-socket.wit b/crates/wasi/wit/deps/sockets/udp-create-socket.wit index cd4c08fb1000..864f107e19e7 100644 --- a/crates/wasi/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/udp-create-socket.wit @@ -14,7 +14,6 @@ interface udp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// From 553a9ec8fbb5e49cedbf6ed9c9ed93eac713dcfb Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Tue, 26 Sep 2023 17:10:33 +0200 Subject: [PATCH 10/42] Simplify "not supported" and "invalid argument" error cases. There is a myriad of reasons why an argument might be invalid or an operation might be not supported. But there is few cross platform consistency in which of those error cases result in which error codes. The error codes that have been removed were fairly specific, but: - Were still missing error cases. So additional error codes would have needed to be created. - Implementations would have to bend over backwards to make it work cross platform, especially beyond just Win/Mac/Linux. - Didn't all have an equivalent in POSIX, so they would map back into a generic EINVAL anyways. --- crates/wasi/src/preview2/host/network.rs | 2 +- crates/wasi/src/preview2/host/tcp.rs | 20 +++++++++---- .../wasi/wit/deps/sockets/ip-name-lookup.wit | 6 ++-- crates/wasi/wit/deps/sockets/network.wit | 29 ++++--------------- .../wit/deps/sockets/tcp-create-socket.wit | 4 +-- crates/wasi/wit/deps/sockets/tcp.wit | 20 ++++++++----- .../wit/deps/sockets/udp-create-socket.wit | 4 +-- crates/wasi/wit/deps/sockets/udp.wit | 22 +++++++------- 8 files changed, 53 insertions(+), 54 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 0eea6672c721..67e78ea8ada0 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -91,7 +91,7 @@ impl From for network::Error { Errno::CONNREFUSED => ErrorCode::ConnectionRefused, Errno::CONNRESET => ErrorCode::ConnectionReset, Errno::CONNABORTED => ErrorCode::ConnectionAborted, - // Errno::INVAL => ErrorCode::Todo, // FIXME + Errno::INVAL => ErrorCode::InvalidArgument, Errno::HOSTUNREACH => ErrorCode::RemoteUnreachable, Errno::HOSTDOWN => ErrorCode::RemoteUnreachable, Errno::NETDOWN => ErrorCode::RemoteUnreachable, diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index d761662a98e7..93b4d7eb0777 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -17,6 +17,8 @@ use rustix::net::sockopt; use std::any::Any; use tokio::io::Interest; +use super::network::SystemError; + impl tcp::Host for T { fn start_bind( &mut self, @@ -37,9 +39,12 @@ impl tcp::Host for T { let binder = network.0.tcp_binder(local_address)?; // Perform the OS bind call. - binder.bind_existing_tcp_listener( - &*socket.tcp_socket().as_socketlike_view::(), - )?; + binder + .bind_existing_tcp_listener(&*socket.tcp_socket().as_socketlike_view::()) + .map_err(|error| match error.errno() { + Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument.into(), + _ => Into::::into(error), + })?; let socket = table.get_tcp_socket_mut(this)?; socket.tcp_state = HostTcpState::BindStarted; @@ -102,9 +107,14 @@ impl tcp::Host for T { return Ok(()); } // continue in progress, - Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} + Err(err) if err.errno() == Some(INPROGRESS) => {} // or fail immediately. - Err(err) => return Err(err.into()), + Err(err) => { + return Err(match err.errno() { + Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument.into(), + _ => Into::::into(err), + }) + } } let socket = table.get_tcp_socket_mut(this)?; diff --git a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit index f15d19d037da..1018683954d8 100644 --- a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit +++ b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit @@ -25,9 +25,9 @@ interface ip-name-lookup { /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. /// /// # Typical errors - /// - `invalid-name`: `name` is a syntactically invalid domain name. - /// - `invalid-name`: `name` is an IP address. - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) + /// - `invalid-argument`: `name` is a syntactically invalid domain name. + /// - `invalid-argument`: `name` is an IP address. + /// - `not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) /// /// # References: /// - diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index cf4eca102718..92f70f957190 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/wit/deps/sockets/network.wit @@ -42,6 +42,11 @@ interface network { /// POSIX equivalent: EOPNOTSUPP not-supported, + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + /// Not enough memory to complete the operation. /// /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY @@ -68,24 +73,6 @@ interface network { would-block, - // ### IP ERRORS ### - - /// The specified address-family is not supported. - address-family-not-supported, - - /// An IPv4 address was passed to an IPv6 resource, or vice versa. - address-family-mismatch, - - /// The socket address is not a valid remote address. E.g. the IP address is set to INADDR_ANY, or the port is set to 0. - invalid-remote-address, - - /// The operation is only supported on IPv4 resources. - ipv4-only-operation, - - /// The operation is only supported on IPv6 resources. - ipv6-only-operation, - - // ### TCP & UDP SOCKET ERRORS ### @@ -94,9 +81,6 @@ interface network { /// A new socket resource could not be created because of a system limit. new-socket-limit, - - /// The socket is already attached to another network. - already-attached, /// A bind operation failed because the provided address is not an address that the `network` can bind to. address-not-bindable, @@ -125,9 +109,6 @@ interface network { // ### NAME LOOKUP ERRORS ### - - /// The provided name is a syntactically invalid domain name. - invalid-name, /// Name does not exist or has no suitable associated IP addresses. name-unresolvable, diff --git a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit index c7cce0ef745c..977b8cbf7cd3 100644 --- a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/tcp-create-socket.wit @@ -14,8 +14,8 @@ interface tcp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) - /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References /// - diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 0b09b11d13f0..c8eb3d1c62f0 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -32,7 +32,9 @@ interface tcp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL) /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors @@ -66,10 +68,14 @@ interface tcp { /// Besides `drop`, any method after such a failure may return an error. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket was explicitly bound and the `connect` call is trying to connect to this socket's own local binding. + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `invalid-state`: The socket is already in the Connection state. (EISCONN) /// - `invalid-state`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// @@ -133,7 +139,7 @@ interface tcp { /// /// # Typical errors /// - `invalid-state`: Socket is not in the Listener state. (EINVAL) - /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// @@ -184,8 +190,8 @@ interface tcp { /// Equivalent to the IPV6_V6ONLY socket option. /// /// # Typical errors - /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. /// - `invalid-state`: (set) The socket is already bound. + /// - `not-supported`: (get/set) `this` socket is an IPv4 socket. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) ipv6-only: func(this: tcp-socket) -> result set-ipv6-only: func(this: tcp-socket, value: bool) -> result<_, error-code> diff --git a/crates/wasi/wit/deps/sockets/udp-create-socket.wit b/crates/wasi/wit/deps/sockets/udp-create-socket.wit index 864f107e19e7..ada6ef07723b 100644 --- a/crates/wasi/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi/wit/deps/sockets/udp-create-socket.wit @@ -14,8 +14,8 @@ interface udp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) - /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References: /// - diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index 871dbbd8914b..2db8b039792f 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/wit/deps/sockets/udp.wit @@ -33,7 +33,7 @@ interface udp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors @@ -64,10 +64,11 @@ interface udp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// /// # Typical `finish` errors /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) @@ -119,10 +120,11 @@ interface udp { /// call `remote-address` to get their address. /// /// # Typical errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `already-connected`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) /// - `invalid-state`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) @@ -179,7 +181,7 @@ interface udp { /// Equivalent to the IPV6_V6ONLY socket option. /// /// # Typical errors - /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. + /// - `not-supported`: (get/set) `this` socket is an IPv4 socket. /// - `invalid-state`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) ipv6-only: func(this: udp-socket) -> result From 86cb6e998419547521f26f1d06da30b3741675d7 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Tue, 26 Sep 2023 17:31:41 +0200 Subject: [PATCH 11/42] Move example_body out of lib.rs into its own test-case make room for other tests. --- crates/test-programs/tests/wasi-sockets.rs | 9 +- .../src/bin/tcp_sample_application.rs | 122 ++++++++++++++++++ .../wasi-sockets-tests/src/bin/tcp_v4.rs | 27 ---- .../wasi-sockets-tests/src/bin/tcp_v6.rs | 29 ----- .../wasi-sockets-tests/src/lib.rs | 84 ------------ 5 files changed, 124 insertions(+), 147 deletions(-) create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs delete mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs delete mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index c16e9d4fadf7..aec78ae776ee 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -66,11 +66,6 @@ async fn run(name: &str) -> anyhow::Result<()> { } #[test_log::test(tokio::test(flavor = "multi_thread"))] -async fn tcp_v4() { - run("tcp_v4").await.unwrap(); -} - -#[test_log::test(tokio::test(flavor = "multi_thread"))] -async fn tcp_v6() { - run("tcp_v6").await.unwrap(); +async fn tcp_sample_application() { + run("tcp_sample_application").await.unwrap(); } diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs new file mode 100644 index 000000000000..e773b11b14b6 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs @@ -0,0 +1,122 @@ +use wasi_sockets_tests::*; +use wasi::poll::poll; +use wasi::io::streams; +use wasi::sockets::network::{ + self, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, +}; +use wasi::sockets::{instance_network, tcp, tcp_create_socket}; + + +fn test_sample_application(family: network::IpAddressFamily, bind_address: IpSocketAddress) { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let net = instance_network::instance_network(); + let sock = tcp_create_socket::create_tcp_socket(family).unwrap(); + + let sub = tcp::subscribe(sock); + + tcp::start_bind(sock, net, bind_address).unwrap(); + + wait(sub); + wasi::poll::poll::drop_pollable(sub); + + tcp::finish_bind(sock).unwrap(); + + let sub = tcp::subscribe(sock); + + tcp::start_listen(sock).unwrap(); + wait(sub); + tcp::finish_listen(sock).unwrap(); + + let addr = tcp::local_address(sock).unwrap(); + + let client = tcp_create_socket::create_tcp_socket(family).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = write(client_output, &[]); + assert_eq!(n, 0); + assert_eq!(status, streams::StreamStatus::Open); + + let (n, status) = write(client_output, first_message); + assert_eq!(n, first_message.len()); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + + let (empty_data, status) = streams::read(input, 0).unwrap(); + assert!(empty_data.is_empty()); + assert_eq!(status, streams::StreamStatus::Open); + + let (data, status) = streams::blocking_read(input, first_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(input); + streams::drop_output_stream(output); + tcp::drop_tcp_socket(accepted); + + // Check that we sent and recieved our message! + assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + + // Another client + let client = tcp_create_socket::create_tcp_socket(family).unwrap(); + let client_sub = tcp::subscribe(client); + + tcp::start_connect(client, net, addr).unwrap(); + wait(client_sub); + let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + + let (n, status) = write(client_output, second_message); + assert_eq!(n, second_message.len()); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(client_input); + streams::drop_output_stream(client_output); + poll::drop_pollable(client_sub); + tcp::drop_tcp_socket(client); + + wait(sub); + let (accepted, input, output) = tcp::accept(sock).unwrap(); + let (data, status) = streams::blocking_read(input, second_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + streams::drop_input_stream(input); + streams::drop_output_stream(output); + tcp::drop_tcp_socket(accepted); + + // Check that we sent and recieved our message! + assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + + poll::drop_pollable(sub); + tcp::drop_tcp_socket(sock); + network::drop_network(net); +} + +fn main() { + test_sample_application( + IpAddressFamily::Ipv4, + IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, // use any free port + address: (127, 0, 0, 1), // localhost + }), + ); + test_sample_application( + IpAddressFamily::Ipv6, + IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: 0, // use any free port + address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost + flow_info: 0, + scope_id: 0, + }), + ); +} \ No newline at end of file diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs deleted file mode 100644 index 2388cb074afe..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! A simple TCP testcase, using IPv4. - -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress}; -use wasi::sockets::{instance_network, tcp, tcp_create_socket}; -use wasi_sockets_tests::*; - -fn main() { - let net = instance_network::instance_network(); - - let sock = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv4).unwrap(); - - let addr = IpSocketAddress::Ipv4(Ipv4SocketAddress { - port: 0, // use any free port - address: (127, 0, 0, 1), // localhost - }); - - let sub = tcp::subscribe(sock); - - tcp::start_bind(sock, net, addr).unwrap(); - - wait(sub); - wasi::poll::poll::drop_pollable(sub); - - tcp::finish_bind(sock).unwrap(); - - example_body(net, sock, IpAddressFamily::Ipv4) -} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs deleted file mode 100644 index b5ff8358cc09..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Like v4.rs, but with IPv6. - -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; -use wasi::sockets::{instance_network, tcp, tcp_create_socket}; -use wasi_sockets_tests::*; - -fn main() { - let net = instance_network::instance_network(); - - let sock = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv6).unwrap(); - - let addr = IpSocketAddress::Ipv6(Ipv6SocketAddress { - port: 0, // use any free port - address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost - flow_info: 0, - scope_id: 0, - }); - - let sub = tcp::subscribe(sock); - - tcp::start_bind(sock, net, addr).unwrap(); - - wait(sub); - wasi::poll::poll::drop_pollable(sub); - - tcp::finish_bind(sock).unwrap(); - - example_body(net, sock, IpAddressFamily::Ipv6) -} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index fb06654c313a..48524c407a3e 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -2,7 +2,6 @@ wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); use wasi::io::streams; use wasi::poll::poll; -use wasi::sockets::{network, tcp, tcp_create_socket}; pub fn wait(sub: poll::Pollable) { loop { @@ -58,86 +57,3 @@ pub fn write(output: streams::OutputStream, mut bytes: &[u8]) -> (usize, streams (total, streams::StreamStatus::Open) } - -pub fn example_body(net: tcp::Network, sock: tcp::TcpSocket, family: network::IpAddressFamily) { - let first_message = b"Hello, world!"; - let second_message = b"Greetings, planet!"; - - let sub = tcp::subscribe(sock); - - tcp::start_listen(sock).unwrap(); - wait(sub); - tcp::finish_listen(sock).unwrap(); - - let addr = tcp::local_address(sock).unwrap(); - - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = tcp::subscribe(client); - - tcp::start_connect(client, net, addr).unwrap(); - wait(client_sub); - let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - - let (n, status) = write(client_output, &[]); - assert_eq!(n, 0); - assert_eq!(status, streams::StreamStatus::Open); - - let (n, status) = write(client_output, first_message); - assert_eq!(n, first_message.len()); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(client_input); - streams::drop_output_stream(client_output); - poll::drop_pollable(client_sub); - tcp::drop_tcp_socket(client); - - wait(sub); - let (accepted, input, output) = tcp::accept(sock).unwrap(); - - let (empty_data, status) = streams::read(input, 0).unwrap(); - assert!(empty_data.is_empty()); - assert_eq!(status, streams::StreamStatus::Open); - - let (data, status) = streams::blocking_read(input, first_message.len() as u64).unwrap(); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(input); - streams::drop_output_stream(output); - tcp::drop_tcp_socket(accepted); - - // Check that we sent and recieved our message! - assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. - - // Another client - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = tcp::subscribe(client); - - tcp::start_connect(client, net, addr).unwrap(); - wait(client_sub); - let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - - let (n, status) = write(client_output, second_message); - assert_eq!(n, second_message.len()); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(client_input); - streams::drop_output_stream(client_output); - poll::drop_pollable(client_sub); - tcp::drop_tcp_socket(client); - - wait(sub); - let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, status) = streams::blocking_read(input, second_message.len() as u64).unwrap(); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(input); - streams::drop_output_stream(output); - tcp::drop_tcp_socket(accepted); - - // Check that we sent and recieved our message! - assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. - - poll::drop_pollable(sub); - tcp::drop_tcp_socket(sock); - network::drop_network(net); -} From eabf4802809cd03253ce892a98aac99ab0056e81 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Tue, 26 Sep 2023 18:31:48 +0200 Subject: [PATCH 12/42] Refactor TCP integration tests: - Ad-hoc skeleton implementation of resources. - Add blocking wrappers around async operations. --- .../src/bin/tcp_sample_application.rs | 128 ++++------- .../wasi-sockets-tests/src/lib.rs | 213 +++++++++++++++--- 2 files changed, 227 insertions(+), 114 deletions(-) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs index e773b11b14b6..a443bfae820a 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs @@ -1,105 +1,69 @@ -use wasi_sockets_tests::*; -use wasi::poll::poll; use wasi::io::streams; use wasi::sockets::network::{ self, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, }; -use wasi::sockets::{instance_network, tcp, tcp_create_socket}; - +use wasi::sockets::tcp; +use wasi_sockets_tests::*; fn test_sample_application(family: network::IpAddressFamily, bind_address: IpSocketAddress) { let first_message = b"Hello, world!"; let second_message = b"Greetings, planet!"; - let net = instance_network::instance_network(); - let sock = tcp_create_socket::create_tcp_socket(family).unwrap(); - - let sub = tcp::subscribe(sock); - - tcp::start_bind(sock, net, bind_address).unwrap(); - - wait(sub); - wasi::poll::poll::drop_pollable(sub); - - tcp::finish_bind(sock).unwrap(); - - let sub = tcp::subscribe(sock); - - tcp::start_listen(sock).unwrap(); - wait(sub); - tcp::finish_listen(sock).unwrap(); + let net = NetworkResource::default(); + let listener = TcpSocketResource::new(family).unwrap(); - let addr = tcp::local_address(sock).unwrap(); + listener.bind(&net, bind_address).unwrap(); + listener.listen().unwrap(); - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = tcp::subscribe(client); + let addr = tcp::local_address(listener.handle).unwrap(); - tcp::start_connect(client, net, addr).unwrap(); - wait(client_sub); - let (client_input, client_output) = tcp::finish_connect(client).unwrap(); + { + let client = TcpSocketResource::new(family).unwrap(); + let (_client_input, client_output) = client.connect(&net, addr).unwrap(); - let (n, status) = write(client_output, &[]); - assert_eq!(n, 0); - assert_eq!(status, streams::StreamStatus::Open); + let (n, status) = client_output.write(&[]); + assert_eq!(n, 0); + assert_eq!(status, streams::StreamStatus::Open); - let (n, status) = write(client_output, first_message); - assert_eq!(n, first_message.len()); - assert_eq!(status, streams::StreamStatus::Open); + let (n, status) = client_output.write(first_message); + assert_eq!(n, first_message.len()); + assert_eq!(status, streams::StreamStatus::Open); + } - streams::drop_input_stream(client_input); - streams::drop_output_stream(client_output); - poll::drop_pollable(client_sub); - tcp::drop_tcp_socket(client); + { + let (_accepted, input, _output) = listener.accept().unwrap(); - wait(sub); - let (accepted, input, output) = tcp::accept(sock).unwrap(); + let (empty_data, status) = streams::read(input.handle, 0).unwrap(); + assert!(empty_data.is_empty()); + assert_eq!(status, streams::StreamStatus::Open); - let (empty_data, status) = streams::read(input, 0).unwrap(); - assert!(empty_data.is_empty()); - assert_eq!(status, streams::StreamStatus::Open); + let (data, status) = + streams::blocking_read(input.handle, first_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); - let (data, status) = streams::blocking_read(input, first_message.len() as u64).unwrap(); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(input); - streams::drop_output_stream(output); - tcp::drop_tcp_socket(accepted); - - // Check that we sent and recieved our message! - assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + // Check that we sent and recieved our message! + assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + } // Another client - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = tcp::subscribe(client); - - tcp::start_connect(client, net, addr).unwrap(); - wait(client_sub); - let (client_input, client_output) = tcp::finish_connect(client).unwrap(); - - let (n, status) = write(client_output, second_message); - assert_eq!(n, second_message.len()); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(client_input); - streams::drop_output_stream(client_output); - poll::drop_pollable(client_sub); - tcp::drop_tcp_socket(client); - - wait(sub); - let (accepted, input, output) = tcp::accept(sock).unwrap(); - let (data, status) = streams::blocking_read(input, second_message.len() as u64).unwrap(); - assert_eq!(status, streams::StreamStatus::Open); - - streams::drop_input_stream(input); - streams::drop_output_stream(output); - tcp::drop_tcp_socket(accepted); - - // Check that we sent and recieved our message! - assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. - - poll::drop_pollable(sub); - tcp::drop_tcp_socket(sock); - network::drop_network(net); + { + let client = TcpSocketResource::new(family).unwrap(); + let (_client_input, client_output) = client.connect(&net, addr).unwrap(); + + let (n, status) = client_output.write(second_message); + assert_eq!(n, second_message.len()); + assert_eq!(status, streams::StreamStatus::Open); + } + + { + let (_accepted, input, _output) = listener.accept().unwrap(); + let (data, status) = + streams::blocking_read(input.handle, second_message.len() as u64).unwrap(); + assert_eq!(status, streams::StreamStatus::Open); + + // Check that we sent and recieved our message! + assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + } } fn main() { diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index 48524c407a3e..a97f7d6472f0 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -2,58 +2,207 @@ wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); use wasi::io::streams; use wasi::poll::poll; +use wasi::sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::{instance_network, tcp, tcp_create_socket}; -pub fn wait(sub: poll::Pollable) { - loop { - let wait = poll::poll_oneoff(&[sub]); - if wait[0] { - break; +pub struct PollableResource { + pub handle: poll::Pollable, +} + +impl Drop for PollableResource { + fn drop(&mut self) { + poll::drop_pollable(self.handle); + } +} + +impl PollableResource { + pub fn wait(&self) { + loop { + let wait = poll::poll_oneoff(&[self.handle]); + if wait[0] { + break; + } + } + } +} + +pub struct InputStreamResource { + pub handle: streams::InputStream, +} + +impl Drop for InputStreamResource { + fn drop(&mut self) { + streams::drop_input_stream(self.handle); + } +} + +impl InputStreamResource { + pub fn subscribe(&self) -> PollableResource { + PollableResource { + handle: streams::subscribe_to_input_stream(self.handle), } } } -pub struct DropPollable { - pub pollable: poll::Pollable, +pub struct OutputStreamResource { + pub handle: streams::OutputStream, } -impl Drop for DropPollable { +impl Drop for OutputStreamResource { fn drop(&mut self) { - poll::drop_pollable(self.pollable); + streams::drop_output_stream(self.handle); } } -pub fn write(output: streams::OutputStream, mut bytes: &[u8]) -> (usize, streams::StreamStatus) { - let total = bytes.len(); - let mut written = 0; +impl OutputStreamResource { + pub fn subscribe(&self) -> PollableResource { + PollableResource { + handle: streams::subscribe_to_output_stream(self.handle), + } + } + + pub fn write(&self, mut bytes: &[u8]) -> (usize, streams::StreamStatus) { + let total = bytes.len(); + let mut written = 0; - let s = DropPollable { - pollable: streams::subscribe_to_output_stream(output), - }; + let s = self.subscribe(); - while !bytes.is_empty() { - poll::poll_oneoff(&[s.pollable]); + while !bytes.is_empty() { + s.wait(); - let permit = match streams::check_write(output) { - Ok(n) => n, - Err(_) => return (written, streams::StreamStatus::Ended), - }; + let permit = match streams::check_write(self.handle) { + Ok(n) => n, + Err(_) => return (written, streams::StreamStatus::Ended), + }; - let len = bytes.len().min(permit as usize); - let (chunk, rest) = bytes.split_at(len); + let len = bytes.len().min(permit as usize); + let (chunk, rest) = bytes.split_at(len); - match streams::write(output, chunk) { - Ok(()) => {} - Err(_) => return (written, streams::StreamStatus::Ended), + match streams::write(self.handle, chunk) { + Ok(()) => {} + Err(_) => return (written, streams::StreamStatus::Ended), + } + + match streams::blocking_flush(self.handle) { + Ok(()) => {} + Err(_) => return (written, streams::StreamStatus::Ended), + } + + bytes = rest; + written += len; } - match streams::blocking_flush(output) { - Ok(()) => {} - Err(_) => return (written, streams::StreamStatus::Ended), + (total, streams::StreamStatus::Open) + } +} + +pub struct NetworkResource { + pub handle: wasi::sockets::network::Network, +} + +impl Drop for NetworkResource { + fn drop(&mut self) { + wasi::sockets::network::drop_network(self.handle); + } +} + +impl NetworkResource { + pub fn default() -> NetworkResource { + NetworkResource { + handle: instance_network::instance_network(), } + } +} + +pub struct TcpSocketResource { + pub handle: wasi::sockets::tcp::TcpSocket, +} + +impl Drop for TcpSocketResource { + fn drop(&mut self) { + wasi::sockets::tcp::drop_tcp_socket(self.handle); + } +} - bytes = rest; - written += len; +impl TcpSocketResource { + pub fn new(address_family: IpAddressFamily) -> Result { + Ok(TcpSocketResource { + handle: tcp_create_socket::create_tcp_socket(address_family)?, + }) } - (total, streams::StreamStatus::Open) + pub fn subscribe(&self) -> PollableResource { + PollableResource { + handle: tcp::subscribe(self.handle), + } + } + + pub fn bind(&self, network: &NetworkResource, local_address: IpSocketAddress) -> Result<(), ErrorCode> { + let sub = self.subscribe(); + + tcp::start_bind(self.handle, network.handle, local_address)?; + + loop { + match tcp::finish_bind(self.handle) { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } + + pub fn listen(&self) -> Result<(), ErrorCode> { + let sub = self.subscribe(); + + tcp::start_listen(self.handle)?; + + loop { + match tcp::finish_listen(self.handle) { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } + + pub fn connect( + &self, + network: &NetworkResource, + remote_address: IpSocketAddress, + ) -> Result<(InputStreamResource, OutputStreamResource), ErrorCode> { + let sub = self.subscribe(); + + tcp::start_connect(self.handle, network.handle, remote_address)?; + + loop { + match tcp::finish_connect(self.handle) { + Err(ErrorCode::WouldBlock) => sub.wait(), + Err(e) => return Err(e), + Ok((input, output)) => { + return Ok(( + InputStreamResource { handle: input }, + OutputStreamResource { handle: output }, + )) + } + } + } + } + + pub fn accept( + &self, + ) -> Result<(TcpSocketResource, InputStreamResource, OutputStreamResource), ErrorCode> { + let sub = self.subscribe(); + + loop { + match tcp::accept(self.handle) { + Err(ErrorCode::WouldBlock) => sub.wait(), + Err(e) => return Err(e), + Ok((client, input, output)) => { + return Ok(( + TcpSocketResource { handle: client }, + InputStreamResource { handle: input }, + OutputStreamResource { handle: output }, + )) + } + } + } + } } From 3a76e8a502652c9f86ee1e4de459fe5d583fe92f Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 28 Sep 2023 16:42:09 +0200 Subject: [PATCH 13/42] Fix get/set_unicast_hop_limit on Linux --- crates/wasi/src/preview2/host/tcp.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 93b4d7eb0777..f72da06f53ec 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -390,7 +390,7 @@ impl tcp::Host for T { // fall back to the other. match sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { Ok(value) => Ok(value), - Err(Errno::NOPROTOOPT) => { + Err(Errno::NOPROTOOPT | Errno::OPNOTSUPP) => { let value = sockopt::get_ip_ttl(socket.tcp_socket())?; let value = value.try_into().unwrap(); Ok(value) @@ -411,7 +411,7 @@ impl tcp::Host for T { // fall back to the other. match sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value)) { Ok(()) => Ok(()), - Err(Errno::NOPROTOOPT) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), + Err(Errno::NOPROTOOPT | Errno::OPNOTSUPP) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), Err(err) => Err(err.into()), } } From f6ea0792c6bfb8a67e21aad36e8e6cd8e37e8ac9 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 28 Sep 2023 16:45:18 +0200 Subject: [PATCH 14/42] Test TCP socket states --- crates/test-programs/tests/wasi-sockets.rs | 6 + .../src/bin/tcp_sample_application.rs | 2 +- .../wasi-sockets-tests/src/bin/tcp_states.rs | 339 ++++++++++++++++++ .../wasi-sockets-tests/src/lib.rs | 87 ++++- 4 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index aec78ae776ee..a25bf2dd7961 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -69,3 +69,9 @@ async fn run(name: &str) -> anyhow::Result<()> { async fn tcp_sample_application() { run("tcp_sample_application").await.unwrap(); } + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_states() { + run("tcp_states").await.unwrap(); +} + diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs index a443bfae820a..98e6679d0722 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs @@ -83,4 +83,4 @@ fn main() { scope_id: 0, }), ); -} \ No newline at end of file +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs new file mode 100644 index 000000000000..87d3139aa8ab --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs @@ -0,0 +1,339 @@ +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::tcp; +use wasi_sockets_tests::*; + +fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { + let sock = TcpSocketResource::new(family).unwrap(); + + // Skipping: tcp::start_bind + assert!(matches!( + tcp::finish_bind(sock.handle), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::start_connect + assert!(matches!( + tcp::finish_connect(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::start_listen(sock.handle), + Err(ErrorCode::InvalidState) // Unlike POSIX, trying to listen without an explicit bind should fail in WASI. + )); + assert!(matches!( + tcp::finish_listen(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::accept(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::shutdown(sock.handle, tcp::ShutdownType::Both), + Err(ErrorCode::InvalidState) + )); + + assert!(matches!( + tcp::local_address(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::remote_address(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert_eq!(tcp::address_family(sock.handle), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(tcp::ipv6_only(sock.handle), Ok(_))); + + // Even on platforms that don't support dualstack sockets, + // setting ipv6_only to true (disabling dualstack mode) should work. + assert!(matches!(tcp::set_ipv6_only(sock.handle, true), Ok(_))); + } else { + assert!(matches!( + tcp::ipv6_only(sock.handle), + Err(ErrorCode::NotSupported) + )); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::NotSupported) + )); + } + + // assert!(matches!(tcp::set_listen_backlog_size(sock.handle, 32), Ok(_))); // FIXME + assert!(matches!(tcp::keep_alive(sock.handle), Ok(_))); + assert!(matches!(tcp::set_keep_alive(sock.handle, false), Ok(_))); + assert!(matches!(tcp::no_delay(sock.handle), Ok(_))); + assert!(matches!(tcp::set_no_delay(sock.handle, false), Ok(_))); + assert!(matches!(tcp::unicast_hop_limit(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_unicast_hop_limit(sock.handle, 255), + Ok(_) + )); + assert!(matches!(tcp::receive_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_receive_buffer_size(sock.handle, 16000), + Ok(_) + )); + assert!(matches!(tcp::send_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_send_buffer_size(sock.handle, 16000), + Ok(_) + )); +} + +fn test_tcp_bound_state_invariants(net: &NetworkResource, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocketResource::new(family).unwrap(); + sock.bind(net, bind_address).unwrap(); + + assert!(matches!( + tcp::start_bind(sock.handle, net.handle, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_bind(sock.handle), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::start_connect + assert!(matches!( + tcp::finish_connect(sock.handle), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::start_listen + assert!(matches!( + tcp::finish_listen(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::accept(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::shutdown(sock.handle, tcp::ShutdownType::Both), + Err(ErrorCode::InvalidState) + )); + + assert!(matches!(tcp::local_address(sock.handle), Ok(_))); + assert!(matches!( + tcp::remote_address(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert_eq!(tcp::address_family(sock.handle), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(tcp::ipv6_only(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!( + tcp::ipv6_only(sock.handle), + Err(ErrorCode::NotSupported) + )); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + )); + } + + // assert!(matches!(tcp::set_listen_backlog_size(sock.handle, 32), Err(ErrorCode::AlreadyBound))); // FIXME + assert!(matches!(tcp::keep_alive(sock.handle), Ok(_))); + assert!(matches!(tcp::set_keep_alive(sock.handle, false), Ok(_))); + assert!(matches!(tcp::no_delay(sock.handle), Ok(_))); + assert!(matches!(tcp::set_no_delay(sock.handle, false), Ok(_))); + assert!(matches!(tcp::unicast_hop_limit(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_unicast_hop_limit(sock.handle, 255), + Ok(_) + )); + assert!(matches!(tcp::receive_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_receive_buffer_size(sock.handle, 16000), + Ok(_) + )); + assert!(matches!(tcp::send_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_send_buffer_size(sock.handle, 16000), + Ok(_) + )); +} + +fn test_tcp_listening_state_invariants(net: &NetworkResource, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocketResource::new(family).unwrap(); + sock.bind(net, bind_address).unwrap(); + sock.listen().unwrap(); + + assert!(matches!( + tcp::start_bind(sock.handle, net.handle, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_bind(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::start_connect(sock.handle, net.handle, bind_address), // Actual address shouldn't matter + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_connect(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::start_listen(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_listen(sock.handle), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::accept + assert!(matches!( + tcp::shutdown(sock.handle, tcp::ShutdownType::Both), + Err(ErrorCode::InvalidState) + )); + + assert!(matches!(tcp::local_address(sock.handle), Ok(_))); + assert!(matches!( + tcp::remote_address(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert_eq!(tcp::address_family(sock.handle), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(tcp::ipv6_only(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!( + tcp::ipv6_only(sock.handle), + Err(ErrorCode::NotSupported) + )); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + )); + } + + // assert!(matches!(tcp::set_listen_backlog_size(sock.handle, 32), Err(ErrorCode::AlreadyBound))); // FIXME + assert!(matches!(tcp::keep_alive(sock.handle), Ok(_))); + assert!(matches!(tcp::set_keep_alive(sock.handle, false), Ok(_))); + assert!(matches!(tcp::no_delay(sock.handle), Ok(_))); + assert!(matches!(tcp::set_no_delay(sock.handle, false), Ok(_))); + assert!(matches!(tcp::unicast_hop_limit(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_unicast_hop_limit(sock.handle, 255), + Ok(_) + )); + assert!(matches!(tcp::receive_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_receive_buffer_size(sock.handle, 16000), + Ok(_) + )); + assert!(matches!(tcp::send_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_send_buffer_size(sock.handle, 16000), + Ok(_) + )); +} + +fn test_tcp_connected_state_invariants(net: &NetworkResource, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock_listener = TcpSocketResource::new(family).unwrap(); + sock_listener.bind(net, bind_address).unwrap(); + sock_listener.listen().unwrap(); + let addr_listener = tcp::local_address(sock_listener.handle).unwrap(); + let sock = TcpSocketResource::new(family).unwrap(); + let (_input, _output) = sock.connect(net, addr_listener).unwrap(); + + assert!(matches!( + tcp::start_bind(sock.handle, net.handle, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_bind(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::start_connect(sock.handle, net.handle, addr_listener), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_connect(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::start_listen(sock.handle), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + tcp::finish_listen(sock.handle), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + tcp::accept(sock.handle), + Err(ErrorCode::InvalidState) + )); + // Skipping: tcp::shutdown + + assert!(matches!(tcp::local_address(sock.handle), Ok(_))); + assert!(matches!(tcp::remote_address(sock.handle), Ok(_))); + assert_eq!(tcp::address_family(sock.handle), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(tcp::ipv6_only(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!( + tcp::ipv6_only(sock.handle), + Err(ErrorCode::NotSupported) + )); + assert!(matches!( + tcp::set_ipv6_only(sock.handle, true), + Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + )); + } + + assert!(matches!(tcp::keep_alive(sock.handle), Ok(_))); + assert!(matches!(tcp::set_keep_alive(sock.handle, false), Ok(_))); + assert!(matches!(tcp::no_delay(sock.handle), Ok(_))); + assert!(matches!(tcp::set_no_delay(sock.handle, false), Ok(_))); + assert!(matches!(tcp::unicast_hop_limit(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_unicast_hop_limit(sock.handle, 255), + Ok(_) + )); + assert!(matches!(tcp::receive_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_receive_buffer_size(sock.handle, 16000), + Ok(_) + )); + assert!(matches!(tcp::send_buffer_size(sock.handle), Ok(_))); + assert!(matches!( + tcp::set_send_buffer_size(sock.handle, 16000), + Ok(_) + )); +} + +fn main() { + let net = NetworkResource::default(); + + test_tcp_unbound_state_invariants(IpAddressFamily::Ipv4); + test_tcp_unbound_state_invariants(IpAddressFamily::Ipv6); + + test_tcp_bound_state_invariants(&net, IpAddressFamily::Ipv4); + test_tcp_bound_state_invariants(&net, IpAddressFamily::Ipv6); + + test_tcp_listening_state_invariants(&net, IpAddressFamily::Ipv4); + test_tcp_listening_state_invariants(&net, IpAddressFamily::Ipv6); + + test_tcp_connected_state_invariants(&net, IpAddressFamily::Ipv4); + test_tcp_connected_state_invariants(&net, IpAddressFamily::Ipv6); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index a97f7d6472f0..0a229f780261 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -2,7 +2,7 @@ wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); use wasi::io::streams; use wasi::poll::poll; -use wasi::sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, IpAddress, Ipv4SocketAddress, Ipv6SocketAddress}; use wasi::sockets::{instance_network, tcp, tcp_create_socket}; pub struct PollableResource { @@ -206,3 +206,88 @@ impl TcpSocketResource { } } } + + + + + + +impl IpAddress { + pub const IPV4_LOOPBACK: IpAddress = IpAddress::Ipv4((127, 0, 0, 1)); + pub const IPV6_LOOPBACK: IpAddress = IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 1)); + + pub const IPV4_UNSPECIFIED: IpAddress = IpAddress::Ipv4((0, 0, 0, 0)); + pub const IPV6_UNSPECIFIED: IpAddress = IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 0)); + + pub const IPV4_MAPPED_LOOPBACK: IpAddress = + IpAddress::Ipv6((0, 0, 0, 0, 0, 0xFFFF, 0x7F00, 0x0001)); + + pub const fn new_loopback(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_LOOPBACK, + IpAddressFamily::Ipv6 => Self::IPV6_LOOPBACK, + } + } + + pub const fn new_unspecified(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_UNSPECIFIED, + IpAddressFamily::Ipv6 => Self::IPV6_UNSPECIFIED, + } + } + + pub const fn family(&self) -> IpAddressFamily { + match self { + IpAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } +} + +impl PartialEq for IpAddress { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Ipv4(left), Self::Ipv4(right)) => left == right, + (Self::Ipv6(left), Self::Ipv6(right)) => left == right, + _ => false, + } + } +} + +impl IpSocketAddress { + pub const fn new(ip: IpAddress, port: u16) -> IpSocketAddress { + match ip { + IpAddress::Ipv4(addr) => IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: port, + address: addr, + }), + IpAddress::Ipv6(addr) => IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: port, + address: addr, + flow_info: 0, + scope_id: 0, + }), + } + } + + pub const fn ip(&self) -> IpAddress { + match self { + IpSocketAddress::Ipv4(addr) => IpAddress::Ipv4(addr.address), + IpSocketAddress::Ipv6(addr) => IpAddress::Ipv6(addr.address), + } + } + + pub const fn port(&self) -> u16 { + match self { + IpSocketAddress::Ipv4(addr) => addr.port, + IpSocketAddress::Ipv6(addr) => addr.port, + } + } + + pub const fn family(&self) -> IpAddressFamily { + match self { + IpSocketAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpSocketAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } +} \ No newline at end of file From ce8577cb28c5524184bd1c22bd959b69108f167b Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 30 Sep 2023 15:57:12 +0200 Subject: [PATCH 15/42] Keep track of address family ourselves. Because I need the family for input validation. And the resulting code is more straightforward. --- crates/wasi/src/preview2/host/network.rs | 9 +++ crates/wasi/src/preview2/host/tcp.rs | 73 +++++------------------- crates/wasi/src/preview2/tcp.rs | 11 ++-- 3 files changed, 31 insertions(+), 62 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 67e78ea8ada0..4e48200a0501 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -232,3 +232,12 @@ impl From for cap_net_ext::AddressFamily { } } } + +impl From for IpAddressFamily { + fn from(family: cap_net_ext::AddressFamily) -> Self { + match family { + cap_net_ext::AddressFamily::Ipv4 => IpAddressFamily::Ipv4, + cap_net_ext::AddressFamily::Ipv6 => IpAddressFamily::Ipv6, + } + } +} diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index f72da06f53ec..be31282825f9 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -9,7 +9,7 @@ use crate::preview2::poll::TablePollableExt; use crate::preview2::stream::TableStreamExt; use crate::preview2::tcp::{HostTcpSocket, HostTcpState, TableTcpSocketExt}; use crate::preview2::{HostPollable, PollableFuture, WasiView}; -use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; +use cap_net_ext::{AddressFamily, Blocking, PoolExt, TcpListenerExt}; use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; use rustix::io::Errno; @@ -226,7 +226,7 @@ impl tcp::Host for T { .as_socketlike_view::() .accept_with(Blocking::No) })?; - let mut tcp_socket = HostTcpSocket::from_tcp_stream(connection)?; + let mut tcp_socket = HostTcpSocket::from_tcp_stream(connection, socket.family)?; // Mark the socket as connected so that we can exit early from methods like `start-bind`. tcp_socket.tcp_state = HostTcpState::Connected; @@ -281,45 +281,7 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - // If `SO_DOMAIN` is available, use it. - // - // TODO: OpenBSD also supports this; upstream PRs are posted. - #[cfg(not(any( - windows, - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - )))] - { - use rustix::net::AddressFamily; - - let family = sockopt::get_socket_domain(socket.tcp_socket())?; - let family = match family { - AddressFamily::INET => IpAddressFamily::Ipv4, - AddressFamily::INET6 => IpAddressFamily::Ipv6, - _ => return Err(ErrorCode::NotSupported.into()), - }; - Ok(family) - } - - // When `SO_DOMAIN` is not available, emulate it. - #[cfg(any( - windows, - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - if let Ok(_) = sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { - return Ok(IpAddressFamily::Ipv6); - } - if let Ok(_) = sockopt::get_ip_ttl(socket.tcp_socket()) { - return Ok(IpAddressFamily::Ipv4); - } - Err(ErrorCode::NotSupported.into()) - } + Ok(socket.family.into()) } fn ipv6_only(&mut self, this: tcp::TcpSocket) -> Result { @@ -386,17 +348,12 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - // We don't track whether the socket is IPv4 or IPv6 so try one and - // fall back to the other. - match sockopt::get_ipv6_unicast_hops(socket.tcp_socket()) { - Ok(value) => Ok(value), - Err(Errno::NOPROTOOPT | Errno::OPNOTSUPP) => { - let value = sockopt::get_ip_ttl(socket.tcp_socket())?; - let value = value.try_into().unwrap(); - Ok(value) - } - Err(err) => Err(err.into()), - } + let ttl = match socket.family { + AddressFamily::Ipv4 => sockopt::get_ip_ttl(socket.tcp_socket())?.try_into().unwrap(), + AddressFamily::Ipv6 => sockopt::get_ipv6_unicast_hops(socket.tcp_socket())?, + }; + + Ok(ttl) } fn set_unicast_hop_limit( @@ -407,13 +364,13 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; - // We don't track whether the socket is IPv4 or IPv6 so try one and - // fall back to the other. - match sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value)) { - Ok(()) => Ok(()), - Err(Errno::NOPROTOOPT | Errno::OPNOTSUPP) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), - Err(err) => Err(err.into()), + + match socket.family { + AddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?, + AddressFamily::Ipv6 => sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value))?, } + + Ok(()) } fn receive_buffer_size(&mut self, this: tcp::TcpSocket) -> Result { diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 0157774dcbaa..60ec1e23e96a 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -53,6 +53,8 @@ pub(crate) struct HostTcpSocket { /// The current state in the bind/listen/accept/connect progression. pub(crate) tcp_state: HostTcpState, + + pub(crate) family: AddressFamily, } pub(crate) struct TcpReadStream { @@ -222,18 +224,18 @@ impl HostTcpSocket { // Create a new host socket and set it to non-blocking, which is needed // by our async implementation. let tcp_listener = TcpListener::new(family, Blocking::No)?; - Self::from_tcp_listener(tcp_listener) + Self::from_tcp_listener(tcp_listener, family) } /// Create a `HostTcpSocket` from an existing socket. /// /// The socket must be in non-blocking mode. - pub fn from_tcp_stream(tcp_socket: cap_std::net::TcpStream) -> io::Result { + pub fn from_tcp_stream(tcp_socket: cap_std::net::TcpStream, family: AddressFamily) -> io::Result { let tcp_listener = TcpListener::from(rustix::fd::OwnedFd::from(tcp_socket)); - Self::from_tcp_listener(tcp_listener) + Self::from_tcp_listener(tcp_listener, family) } - pub fn from_tcp_listener(tcp_listener: cap_std::net::TcpListener) -> io::Result { + pub fn from_tcp_listener(tcp_listener: cap_std::net::TcpListener, family: AddressFamily) -> io::Result { let fd = tcp_listener.into_raw_socketlike(); let std_stream = unsafe { std::net::TcpStream::from_raw_socketlike(fd) }; let stream = with_ambient_tokio_runtime(|| tokio::net::TcpStream::try_from(std_stream))?; @@ -241,6 +243,7 @@ impl HostTcpSocket { Ok(Self { inner: Arc::new(stream), tcp_state: HostTcpState::Default, + family, }) } From bb92b9f959223af7bcb73cc5e1eefd93b1d72d79 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 30 Sep 2023 16:13:21 +0200 Subject: [PATCH 16/42] Add more tests and make it work on Linux --- crates/test-programs/tests/wasi-sockets.rs | 14 ++ .../wasi-sockets-tests/src/bin/tcp_bind.rs | 147 +++++++++++++++++ .../wasi-sockets-tests/src/bin/tcp_connect.rs | 126 +++++++++++++++ .../src/bin/tcp_sockopts.rs | 151 ++++++++++++++++++ .../wasi-sockets-tests/src/lib.rs | 2 + crates/wasi/src/preview2/host/tcp.rs | 151 ++++++++++++++++-- crates/wasi/wit/deps/sockets/tcp.wit | 13 +- 7 files changed, 588 insertions(+), 16 deletions(-) create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index a25bf2dd7961..2e894ac02d2b 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -70,8 +70,22 @@ async fn tcp_sample_application() { run("tcp_sample_application").await.unwrap(); } +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_bind() { + run("tcp_bind").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_connect() { + run("tcp_connect").await.unwrap(); +} + #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn tcp_states() { run("tcp_states").await.unwrap(); } +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn tcp_sockopts() { + run("tcp_sockopts").await.unwrap(); +} \ No newline at end of file diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs new file mode 100644 index 000000000000..39c2e918ceaf --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs @@ -0,0 +1,147 @@ +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::tcp; +use wasi_sockets_tests::*; + +/// Bind a socket and let the system determine a port. +fn test_tcp_bind_ephemeral_port(net: &NetworkResource, ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock = TcpSocketResource::new(ip.family()).unwrap(); + sock.bind(net, bind_addr).unwrap(); + + let bound_addr = tcp::local_address(sock.handle).unwrap(); + + assert_eq!(bind_addr.ip(), bound_addr.ip()); + assert_ne!(bind_addr.port(), bound_addr.port()); +} + +/// Bind a socket on a specified port. +fn test_tcp_bind_specific_port(net: &NetworkResource, ip: IpAddress) { + const PORT: u16 = 54321; + + let bind_addr = IpSocketAddress::new(ip, PORT); + + let sock = TcpSocketResource::new(ip.family()).unwrap(); + sock.bind(net, bind_addr).unwrap(); + + let bound_addr = tcp::local_address(sock.handle).unwrap(); + + assert_eq!(bind_addr.ip(), bound_addr.ip()); + assert_eq!(bind_addr.port(), bound_addr.port()); +} + +/// Two sockets may not be actively bound to the same address at the same time. +fn test_tcp_bind_addrinuse(net: &NetworkResource, ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock1 = TcpSocketResource::new(ip.family()).unwrap(); + sock1.bind(net, bind_addr).unwrap(); + sock1.listen().unwrap(); + + let bound_addr = tcp::local_address(sock1.handle).unwrap(); + + let sock2 = TcpSocketResource::new(ip.family()).unwrap(); + assert_eq!(sock2.bind(net, bound_addr), Err(ErrorCode::AddressInUse)); +} + +// Try binding to an address that is not configured on the system. +fn test_tcp_bind_addrnotavail(net: &NetworkResource, ip: IpAddress) { + let bind_addr = IpSocketAddress::new(ip, 0); + + let sock = TcpSocketResource::new(ip.family()).unwrap(); + + assert_eq!( + sock.bind(net, bind_addr), + Err(ErrorCode::AddressNotBindable) + ); +} + +/// Bind should validate the address family. +fn test_tcp_bind_wrong_family(net: &NetworkResource, family: IpAddressFamily) { + let wrong_ip = match family { + IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK, + IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK, + }; + + let sock = TcpSocketResource::new(family).unwrap(); + let result = sock.bind(net, IpSocketAddress::new(wrong_ip, 0)); + + assert!(matches!(result, Err(ErrorCode::InvalidArgument))); +} + +/// Bind only works on unicast addresses. +fn test_tcp_bind_non_unicast(net: &NetworkResource) { + + let ipv4_broadcast = IpSocketAddress::new(IpAddress::IPV4_BROADCAST, 0); + let ipv4_multicast = IpSocketAddress::new(IpAddress::Ipv4((224, 254, 0, 0)), 0); + let ipv6_multicast = IpSocketAddress::new(IpAddress::Ipv6((0xff00, 0, 0, 0, 0, 0, 0, 0)), 0); + + let sock_v4 = TcpSocketResource::new(IpAddressFamily::Ipv4).unwrap(); + let sock_v6 = TcpSocketResource::new(IpAddressFamily::Ipv6).unwrap(); + + assert!(matches!(sock_v4.bind(net, ipv4_broadcast), Err(ErrorCode::InvalidArgument))); + assert!(matches!(sock_v4.bind(net, ipv4_multicast), Err(ErrorCode::InvalidArgument))); + assert!(matches!(sock_v6.bind(net, ipv6_multicast), Err(ErrorCode::InvalidArgument))); +} + +fn test_tcp_bind_dual_stack(net: &NetworkResource) { + let sock = TcpSocketResource::new(IpAddressFamily::Ipv6).unwrap(); + let addr = IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, 0); + + // Even on platforms that don't support dualstack sockets, + // setting ipv6_only to true (disabling dualstack mode) should work. + tcp::set_ipv6_only(sock.handle, true).unwrap(); + + // Binding an IPv4-mapped-IPv6 address on a ipv6-only socket should fail: + assert!(matches!( + sock.bind(net, addr), + Err(ErrorCode::InvalidArgument) + )); + + match tcp::set_ipv6_only(sock.handle, false) { + Err(ErrorCode::NotSupported) => { + println!("Skipping dual stack test"); + return; + } + Err(e) => panic!("Unexpected set_ipv6_only error code: {:?}", e), + Ok(_) => { + sock.bind(net, addr).unwrap(); + + let bound_addr = tcp::local_address(sock.handle).unwrap(); + + assert_eq!(bound_addr.family(), IpAddressFamily::Ipv6); + } + } +} + +fn main() { + const RESERVED_IPV4_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 0)); // Reserved for documentation and examples. + const RESERVED_IPV6_ADDRESS: IpAddress = IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)); // Reserved for documentation and examples. + + let net = NetworkResource::default(); + + test_tcp_bind_ephemeral_port(&net, IpAddress::IPV4_LOOPBACK); + test_tcp_bind_ephemeral_port(&net, IpAddress::IPV6_LOOPBACK); + test_tcp_bind_ephemeral_port(&net, IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_ephemeral_port(&net, IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_specific_port(&net, IpAddress::IPV4_LOOPBACK); + test_tcp_bind_specific_port(&net, IpAddress::IPV6_LOOPBACK); + test_tcp_bind_specific_port(&net, IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_specific_port(&net, IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_addrinuse(&net, IpAddress::IPV4_LOOPBACK); + test_tcp_bind_addrinuse(&net, IpAddress::IPV6_LOOPBACK); + test_tcp_bind_addrinuse(&net, IpAddress::IPV4_UNSPECIFIED); + test_tcp_bind_addrinuse(&net, IpAddress::IPV6_UNSPECIFIED); + + test_tcp_bind_addrnotavail(&net, RESERVED_IPV4_ADDRESS); + test_tcp_bind_addrnotavail(&net, RESERVED_IPV6_ADDRESS); + + test_tcp_bind_wrong_family(&net, IpAddressFamily::Ipv4); + test_tcp_bind_wrong_family(&net, IpAddressFamily::Ipv6); + + test_tcp_bind_non_unicast(&net); + + test_tcp_bind_dual_stack(&net); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs new file mode 100644 index 000000000000..220060259059 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs @@ -0,0 +1,126 @@ +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::tcp; +use wasi_sockets_tests::*; + +const SOME_PORT: u16 = 47; // If the tests pass, this will never actually be connected to. + +/// `0.0.0.0` / `::` is not a valid remote address in WASI. +fn test_tcp_connect_unspec(net: &NetworkResource, family: IpAddressFamily) { + let addr = IpSocketAddress::new(IpAddress::new_unspecified(family), SOME_PORT); + let sock = TcpSocketResource::new(family).unwrap(); + + assert!(matches!( + sock.connect(net, addr), + Err(ErrorCode::InvalidArgument) + )); +} + +/// 0 is not a valid remote port. +fn test_tcp_connect_port_0(net: &NetworkResource, family: IpAddressFamily) { + let addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocketResource::new(family).unwrap(); + + assert!(matches!( + sock.connect(net, addr), + Err(ErrorCode::InvalidArgument) + )); +} + +/// Bind should validate the address family. +fn test_tcp_connect_wrong_family(net: &NetworkResource, family: IpAddressFamily) { + let wrong_ip = match family { + IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK, + IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK, + }; + let remote_addr = IpSocketAddress::new(wrong_ip, SOME_PORT); + + let sock = TcpSocketResource::new(family).unwrap(); + + assert!(matches!( + sock.connect(net, remote_addr), + Err(ErrorCode::InvalidArgument) + )); +} + +/// Can only connect to unicast addresses. +fn test_tcp_connect_non_unicast(net: &NetworkResource) { + let ipv4_broadcast = IpSocketAddress::new(IpAddress::IPV4_BROADCAST, SOME_PORT); + let ipv4_multicast = IpSocketAddress::new(IpAddress::Ipv4((224, 254, 0, 0)), SOME_PORT); + let ipv6_multicast = + IpSocketAddress::new(IpAddress::Ipv6((0xff00, 0, 0, 0, 0, 0, 0, 0)), SOME_PORT); + + let sock_v4 = TcpSocketResource::new(IpAddressFamily::Ipv4).unwrap(); + let sock_v6 = TcpSocketResource::new(IpAddressFamily::Ipv6).unwrap(); + + assert!(matches!( + sock_v4.connect(net, ipv4_broadcast), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!( + sock_v4.connect(net, ipv4_multicast), + Err(ErrorCode::InvalidArgument) + )); + assert!(matches!( + sock_v6.connect(net, ipv6_multicast), + Err(ErrorCode::InvalidArgument) + )); +} + +fn test_tcp_connect_dual_stack(net: &NetworkResource) { + // Set-up: + let v4_listener = TcpSocketResource::new(IpAddressFamily::Ipv4).unwrap(); + v4_listener + .bind(&net, IpSocketAddress::new(IpAddress::IPV4_LOOPBACK, 0)) + .unwrap(); + v4_listener.listen().unwrap(); + + let v4_listener_addr = tcp::local_address(v4_listener.handle).unwrap(); + let v6_listener_addr = + IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, v4_listener_addr.port()); + + let v6_client = TcpSocketResource::new(IpAddressFamily::Ipv6).unwrap(); + + // Tests: + + // Even on platforms that don't support dualstack sockets, + // setting ipv6_only to true (disabling dualstack mode) should work. + tcp::set_ipv6_only(v6_client.handle, true).unwrap(); + + // Connecting to an IPv4-mapped-IPv6 address on an ipv6-only socket should fail: + assert!(matches!( + v6_client.connect(net, v6_listener_addr), + Err(ErrorCode::InvalidArgument) + )); + + match tcp::set_ipv6_only(v6_client.handle, false) { + Err(ErrorCode::NotSupported) => { + println!("Skipping dual stack test"); + return; + } + Err(e) => panic!("Unexpected set_ipv6_only error code: {:?}", e), + Ok(_) => {} + } + + v6_client.connect(net, v6_listener_addr).unwrap(); + + let connected_addr = tcp::local_address(v6_client.handle).unwrap(); + + assert_eq!(connected_addr.family(), IpAddressFamily::Ipv6); +} + +fn main() { + let net = NetworkResource::default(); + + test_tcp_connect_unspec(&net, IpAddressFamily::Ipv4); + test_tcp_connect_unspec(&net, IpAddressFamily::Ipv6); + + test_tcp_connect_port_0(&net, IpAddressFamily::Ipv4); + test_tcp_connect_port_0(&net, IpAddressFamily::Ipv6); + + test_tcp_connect_wrong_family(&net, IpAddressFamily::Ipv4); + test_tcp_connect_wrong_family(&net, IpAddressFamily::Ipv6); + + test_tcp_connect_non_unicast(&net); + + test_tcp_connect_dual_stack(&net); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs new file mode 100644 index 000000000000..b8e9106f039f --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -0,0 +1,151 @@ +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::tcp; +use wasi_sockets_tests::*; + +fn test_tcp_sockopt_defaults(family: IpAddressFamily) { + + let sock = TcpSocketResource::new(family).unwrap(); + + assert_eq!(tcp::address_family(sock.handle), family); + + if family == IpAddressFamily::Ipv6 { + tcp::ipv6_only(sock.handle).unwrap(); // Only verify that it has a default value at all, but either value is valid. + } + + tcp::keep_alive(sock.handle).unwrap(); // Only verify that it has a default value at all, but either value is valid. + assert_eq!(tcp::no_delay(sock.handle).unwrap(), false); + assert!(tcp::unicast_hop_limit(sock.handle).unwrap() > 0); + assert!(tcp::receive_buffer_size(sock.handle).unwrap() > 0); + assert!(tcp::send_buffer_size(sock.handle).unwrap() > 0); +} + +fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { + + let sock = TcpSocketResource::new(family).unwrap(); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(tcp::set_ipv6_only(sock.handle, true), Ok(_))); + assert!(matches!(tcp::set_ipv6_only(sock.handle, false), Ok(_) | Err(ErrorCode::NotSupported))); + } + + // FIXME: #7034 + // assert!(matches!(tcp::set_listen_backlog_size(sock.handle, 0), Ok(_))); // Unsupported sizes should be silently capped. + // assert!(matches!(tcp::set_listen_backlog_size(sock.handle, u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + + assert!(matches!(tcp::set_keep_alive(sock.handle, true), Ok(_))); + assert!(matches!(tcp::set_keep_alive(sock.handle, false), Ok(_))); + + assert!(matches!(tcp::set_no_delay(sock.handle, true), Ok(_))); + assert!(matches!(tcp::set_no_delay(sock.handle, false), Ok(_))); + + assert!(matches!(tcp::set_unicast_hop_limit(sock.handle, 0), Err(ErrorCode::InvalidArgument))); + assert!(matches!(tcp::set_unicast_hop_limit(sock.handle, 1), Ok(_))); + assert!(matches!(tcp::set_unicast_hop_limit(sock.handle, u8::MAX), Ok(_))); + + assert!(matches!(tcp::set_receive_buffer_size(sock.handle, 0), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(tcp::set_receive_buffer_size(sock.handle, u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(tcp::set_send_buffer_size(sock.handle, 0), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(tcp::set_send_buffer_size(sock.handle, u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. +} + +fn test_tcp_sockopt_readback(family: IpAddressFamily) { + + let sock = TcpSocketResource::new(family).unwrap(); + + if family == IpAddressFamily::Ipv6 { + tcp::set_ipv6_only(sock.handle, true).unwrap(); + assert_eq!(tcp::ipv6_only(sock.handle).unwrap(), true); + + if let Ok(_) = tcp::set_ipv6_only(sock.handle, false) { + assert_eq!(tcp::ipv6_only(sock.handle).unwrap(), false); + } + } + + tcp::set_keep_alive(sock.handle, true).unwrap(); + assert_eq!(tcp::keep_alive(sock.handle).unwrap(), true); + tcp::set_keep_alive(sock.handle, false).unwrap(); + assert_eq!(tcp::keep_alive(sock.handle).unwrap(), false); + + tcp::set_no_delay(sock.handle, true).unwrap(); + assert_eq!(tcp::no_delay(sock.handle).unwrap(), true); + tcp::set_no_delay(sock.handle, false).unwrap(); + assert_eq!(tcp::no_delay(sock.handle).unwrap(), false); + + tcp::set_unicast_hop_limit(sock.handle, 42).unwrap(); + assert_eq!(tcp::unicast_hop_limit(sock.handle).unwrap(), 42); + + tcp::set_receive_buffer_size(sock.handle, 0x10000).unwrap(); + assert_eq!(tcp::receive_buffer_size(sock.handle).unwrap(), 0x10000); + + tcp::set_send_buffer_size(sock.handle, 0x10000).unwrap(); + assert_eq!(tcp::send_buffer_size(sock.handle).unwrap(), 0x10000); +} + +fn test_tcp_sockopt_inheritance(net: &NetworkResource, family: IpAddressFamily) { + + let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let listener = TcpSocketResource::new(family).unwrap(); + + let default_keep_alive = tcp::keep_alive(listener.handle).unwrap(); + + // Configure options on listener: + { + tcp::set_keep_alive(listener.handle, !default_keep_alive).unwrap(); + tcp::set_no_delay(listener.handle, true).unwrap(); + tcp::set_unicast_hop_limit(listener.handle, 42).unwrap(); + tcp::set_receive_buffer_size(listener.handle, 0x10000).unwrap(); + tcp::set_send_buffer_size(listener.handle, 0x10000).unwrap(); + } + + + listener.bind(&net, bind_addr).unwrap(); + listener.listen().unwrap(); + let bound_addr = tcp::local_address(listener.handle).unwrap(); + let client = TcpSocketResource::new(family).unwrap(); + client.connect(&net, bound_addr).unwrap(); + let (accepted_client, _, _) = listener.accept().unwrap(); + + // Verify options on accepted socket: + { + assert_eq!(tcp::keep_alive(accepted_client.handle).unwrap(), !default_keep_alive); + assert_eq!(tcp::no_delay(accepted_client.handle).unwrap(), true); + assert_eq!(tcp::unicast_hop_limit(accepted_client.handle).unwrap(), 42); + assert_eq!(tcp::receive_buffer_size(accepted_client.handle).unwrap(), 0x10000); + assert_eq!(tcp::send_buffer_size(accepted_client.handle).unwrap(), 0x10000); + } + + // Update options on listener to something else: + { + tcp::set_keep_alive(listener.handle, default_keep_alive).unwrap(); + tcp::set_no_delay(listener.handle, false).unwrap(); + tcp::set_unicast_hop_limit(listener.handle, 43).unwrap(); + tcp::set_receive_buffer_size(listener.handle, 0x20000).unwrap(); + tcp::set_send_buffer_size(listener.handle, 0x20000).unwrap(); + } + + // Verify that the already accepted socket was not affected: + { + assert_eq!(tcp::keep_alive(accepted_client.handle).unwrap(), !default_keep_alive); + assert_eq!(tcp::no_delay(accepted_client.handle).unwrap(), true); + assert_eq!(tcp::unicast_hop_limit(accepted_client.handle).unwrap(), 42); + assert_eq!(tcp::receive_buffer_size(accepted_client.handle).unwrap(), 0x10000); + assert_eq!(tcp::send_buffer_size(accepted_client.handle).unwrap(), 0x10000); + } +} + + +fn main() { + let net = NetworkResource::default(); + + test_tcp_sockopt_defaults(IpAddressFamily::Ipv4); + test_tcp_sockopt_defaults(IpAddressFamily::Ipv6); + + test_tcp_sockopt_input_ranges(IpAddressFamily::Ipv4); + test_tcp_sockopt_input_ranges(IpAddressFamily::Ipv6); + + test_tcp_sockopt_readback(IpAddressFamily::Ipv4); + test_tcp_sockopt_readback(IpAddressFamily::Ipv6); + + test_tcp_sockopt_inheritance(&net, IpAddressFamily::Ipv4); + test_tcp_sockopt_inheritance(&net, IpAddressFamily::Ipv6); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index 0a229f780261..8e66a2b42969 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -213,6 +213,8 @@ impl TcpSocketResource { impl IpAddress { + pub const IPV4_BROADCAST: IpAddress = IpAddress::Ipv4((255, 255, 255, 255)); + pub const IPV4_LOOPBACK: IpAddress = IpAddress::Ipv4((127, 0, 0, 1)); pub const IPV6_LOOPBACK: IpAddress = IpAddress::Ipv6((0, 0, 0, 0, 0, 0, 0, 1)); diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index be31282825f9..ea604428f24a 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -14,7 +14,10 @@ use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; use rustix::io::Errno; use rustix::net::sockopt; -use std::any::Any; +use std::{ + any::Any, + net::{IpAddr, SocketAddr}, +}; use tokio::io::Interest; use super::network::SystemError; @@ -28,6 +31,8 @@ impl tcp::Host for T { ) -> Result<(), network::Error> { let table = self.table_mut(); let socket = table.get_tcp_socket(this)?; + let network = table.get_network(network)?; + let local_address: SocketAddr = local_address.into(); match socket.tcp_state { HostTcpState::Default => {} @@ -35,7 +40,9 @@ impl tcp::Host for T { _ => return Err(ErrorCode::InvalidState.into()), } - let network = table.get_network(network)?; + validate_unicast(&local_address)?; + validate_address_family(&socket, &local_address)?; + let binder = network.0.tcp_binder(local_address)?; // Perform the OS bind call. @@ -75,6 +82,8 @@ impl tcp::Host for T { let table = self.table_mut(); let r = { let socket = table.get_tcp_socket(this)?; + let network = table.get_network(network)?; + let remote_address: SocketAddr = remote_address.into(); match socket.tcp_state { HostTcpState::Default => {} @@ -88,7 +97,10 @@ impl tcp::Host for T { | HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), } - let network = table.get_network(network)?; + validate_unicast(&remote_address)?; + validate_remote_address(&remote_address)?; + validate_address_family(&socket, &remote_address)?; + let connecter = network.0.tcp_connecter(remote_address)?; // Do an OS `connect`. Our socket is non-blocking, so it'll either... @@ -221,11 +233,31 @@ impl tcp::Host for T { // Do the OS accept call. let tcp_socket = socket.tcp_socket(); - let (connection, _addr) = tcp_socket.try_io(Interest::READABLE, || { - tcp_socket - .as_socketlike_view::() - .accept_with(Blocking::No) - })?; + let (connection, _addr) = tcp_socket + .try_io(Interest::READABLE, || { + tcp_socket + .as_socketlike_view::() + .accept_with(Blocking::No) + }) + .map_err(|error| match error.errno() { + // Normalize Linux' non-standard behavior. + // "Linux accept() passes already-pending network errors on the new socket as an error code from accept(). This behavior differs from other BSD socket implementations." + #[cfg(target_os = "linux")] + Some( + Errno::CONNRESET + | Errno::NETRESET + | Errno::HOSTUNREACH + | Errno::HOSTDOWN + | Errno::NETDOWN + | Errno::NETUNREACH + | Errno::PROTO + | Errno::NOPROTOOPT + | Errno::NONET + | Errno::OPNOTSUPP, + ) => ErrorCode::ConnectionAborted.into(), + + _ => Into::::into(error), + })?; let mut tcp_socket = HostTcpSocket::from_tcp_stream(connection, socket.family)?; // Mark the socket as connected so that we can exit early from methods like `start-bind`. @@ -364,6 +396,12 @@ impl tcp::Host for T { let table = self.table(); let socket = table.get_tcp_socket(this)?; + if value == 0 { + // A well-behaved IP application should never send out new packets with TTL 0. + // We validate the value ourselves because OS'es are not consistent in this. + // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation. + return Err(ErrorCode::InvalidArgument.into()); + } match socket.family { AddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?, @@ -376,7 +414,9 @@ impl tcp::Host for T { fn receive_buffer_size(&mut self, this: tcp::TcpSocket) -> Result { let table = self.table(); let socket = table.get_tcp_socket(this)?; - Ok(sockopt::get_socket_recv_buffer_size(socket.tcp_socket())? as u64) + + let value = sockopt::get_socket_recv_buffer_size(socket.tcp_socket())? as u64; + Ok(normalize_getsockopt_buffer_size(value)) } fn set_receive_buffer_size( @@ -386,17 +426,19 @@ impl tcp::Host for T { ) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_recv_buffer_size( socket.tcp_socket(), - value, + normalize_setsockopt_buffer_size(value), )?) } fn send_buffer_size(&mut self, this: tcp::TcpSocket) -> Result { let table = self.table(); let socket = table.get_tcp_socket(this)?; - Ok(sockopt::get_socket_send_buffer_size(socket.tcp_socket())? as u64) + + let value = sockopt::get_socket_send_buffer_size(socket.tcp_socket())? as u64; + Ok(normalize_getsockopt_buffer_size(value)) } fn set_send_buffer_size( @@ -406,10 +448,10 @@ impl tcp::Host for T { ) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; - let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_send_buffer_size( socket.tcp_socket(), - value, + normalize_setsockopt_buffer_size(value), )?) } @@ -518,3 +560,84 @@ const INPROGRESS: Errno = Errno::INPROGRESS; // #[cfg(windows)] const INPROGRESS: Errno = Errno::WOULDBLOCK; + +fn validate_unicast(addr: &SocketAddr) -> Result<(), network::Error> { + match to_canonical_compat(&addr.ip()) { + IpAddr::V4(ipv4) => { + if ipv4.is_multicast() || ipv4.is_broadcast() { + Err(ErrorCode::InvalidArgument.into()) + } else { + Ok(()) + } + } + IpAddr::V6(ipv6) => { + if ipv6.is_multicast() { + Err(ErrorCode::InvalidArgument.into()) + } else { + Ok(()) + } + } + } +} + +fn validate_remote_address(addr: &SocketAddr) -> Result<(), network::Error> { + if to_canonical_compat(&addr.ip()).is_unspecified() { + return Err(ErrorCode::InvalidArgument.into()); + } + + if addr.port() == 0 { + return Err(ErrorCode::InvalidArgument.into()); + } + + Ok(()) +} + +fn validate_address_family( + socket: &HostTcpSocket, + addr: &SocketAddr, +) -> Result<(), network::Error> { + match (socket.family, addr.ip()) { + (AddressFamily::Ipv4, IpAddr::V4(_)) => {} + (AddressFamily::Ipv6, IpAddr::V6(ipv6)) => { + if let Some(_) = ipv6.to_ipv4_mapped() { + if sockopt::get_ipv6_v6only(socket.tcp_socket())? { + // Address is IPv4-mapped IPv6 address, but socket is IPv6-only. + return Err(ErrorCode::InvalidArgument.into()); + } + } + } + _ => return Err(ErrorCode::InvalidArgument.into()), + } + + Ok(()) +} + +fn to_canonical_compat(addr: &IpAddr) -> IpAddr { + match addr { + IpAddr::V4(ipv4) => IpAddr::V4(*ipv4), + IpAddr::V6(ipv6) => { + if let Some(ipv4) = ipv6.to_ipv4_mapped() { + IpAddr::V4(ipv4) + } else if let Some(ipv4) = ipv6.to_ipv4() { + IpAddr::V4(ipv4) + } else { + IpAddr::V6(*ipv6) + } + } + } +} + +fn normalize_setsockopt_buffer_size(value: u64) -> usize { + value.clamp(1, i32::MAX as u64).try_into().unwrap() +} + +fn normalize_getsockopt_buffer_size(value: u64) -> u64 { + if cfg!(target_os = "linux") { + // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead. + // getsockopt returns this internally doubled value. + // We'll half the value to at least get it back into the same ballpark that the application requested it in. + value / 2 + } else { + value + } +} \ No newline at end of file diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index c8eb3d1c62f0..bbe047ab6f75 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -74,7 +74,6 @@ interface tcp { /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - /// - `invalid-argument`: The socket was explicitly bound and the `connect` call is trying to connect to this socket's own local binding. /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// - `invalid-state`: The socket is already in the Connection state. (EISCONN) /// - `invalid-state`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) @@ -128,7 +127,14 @@ interface tcp { /// Accept a new client socket. /// - /// The returned socket is bound and in the Connection state. + /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `ipv6-only` + /// - `keep-alive` + /// - `no-delay` + /// - `unicast-hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` /// /// On success, this function returns the newly accepted client socket along with /// a pair of streams that can be used to read & write to the connection. @@ -207,12 +213,15 @@ interface tcp { set-keep-alive: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. + /// + /// The default value is `false`. no-delay: func(this: tcp-socket) -> result set-no-delay: func(this: tcp-socket, value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. /// /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. /// - `invalid-state`: (set) The socket is already in the Connection state. /// - `invalid-state`: (set) The socket is already in the Listener state. unicast-hop-limit: func(this: tcp-socket) -> result From 4fc3770da1dd7f7da0e545a12e91bc2a30ef4668 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 30 Sep 2023 20:39:42 +0200 Subject: [PATCH 17/42] Fix Windows --- .../wasi-sockets-tests/src/bin/tcp_states.rs | 6 ++-- crates/wasi/src/preview2/host/network.rs | 10 +++--- crates/wasi/src/preview2/host/tcp.rs | 34 ++++++++++++++----- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs index 87d3139aa8ab..6f0d2f40aec5 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs @@ -133,7 +133,7 @@ fn test_tcp_bound_state_invariants(net: &NetworkResource, family: IpAddressFamil )); assert!(matches!( tcp::set_ipv6_only(sock.handle, true), - Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + Err(ErrorCode::NotSupported) )); } @@ -215,7 +215,7 @@ fn test_tcp_listening_state_invariants(net: &NetworkResource, family: IpAddressF )); assert!(matches!( tcp::set_ipv6_only(sock.handle, true), - Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + Err(ErrorCode::NotSupported) )); } @@ -297,7 +297,7 @@ fn test_tcp_connected_state_invariants(net: &NetworkResource, family: IpAddressF )); assert!(matches!( tcp::set_ipv6_only(sock.handle, true), - Err(ErrorCode::NotSupported | ErrorCode::InvalidState) + Err(ErrorCode::NotSupported) )); } diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 4e48200a0501..2587e59b5255 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -19,7 +19,6 @@ impl network::Host for T { } } - /// Unfortunately, Rust's io::ErrorKind is missing more than half of the relevant error codes. /// This trait provides access to a unified error code. pub(crate) trait SystemError: std::error::Error { @@ -42,19 +41,19 @@ impl SystemError for std::io::Error { match self.kind() { std::io::ErrorKind::AddrInUse => Some(Errno::ADDRINUSE), std::io::ErrorKind::AddrNotAvailable => Some(Errno::ADDRNOTAVAIL), - std::io::ErrorKind::AlreadyExists => Some(Errno::EXIST), - std::io::ErrorKind::BrokenPipe => Some(Errno::PIPE), std::io::ErrorKind::ConnectionAborted => Some(Errno::CONNABORTED), std::io::ErrorKind::ConnectionRefused => Some(Errno::CONNREFUSED), std::io::ErrorKind::ConnectionReset => Some(Errno::CONNRESET), std::io::ErrorKind::Interrupted => Some(Errno::INTR), std::io::ErrorKind::InvalidInput => Some(Errno::INVAL), std::io::ErrorKind::NotConnected => Some(Errno::NOTCONN), - std::io::ErrorKind::NotFound => Some(Errno::NOENT), + #[cfg(windows)] + std::io::ErrorKind::OutOfMemory => Some(Errno::NOBUFS), + #[cfg(not(windows))] std::io::ErrorKind::OutOfMemory => Some(Errno::NOMEM), std::io::ErrorKind::PermissionDenied => Some(Errno::ACCESS), // Alternative: EPERM std::io::ErrorKind::TimedOut => Some(Errno::TIMEDOUT), - std::io::ErrorKind::Unsupported => Some(Errno::NOTSUP), + std::io::ErrorKind::Unsupported => Some(Errno::OPNOTSUPP), std::io::ErrorKind::WouldBlock => Some(Errno::WOULDBLOCK), // Alternative: EAGAIN _ => None, @@ -62,7 +61,6 @@ impl SystemError for std::io::Error { } } - impl From for network::Error { fn from(error: TableError) -> Self { Self::trap(error.into()) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index ea604428f24a..fa0db5552886 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -50,6 +50,8 @@ impl tcp::Host for T { .bind_existing_tcp_listener(&*socket.tcp_socket().as_socketlike_view::()) .map_err(|error| match error.errno() { Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument.into(), + #[cfg(windows)] + Some(Errno::NOBUFS) => ErrorCode::AddressInUse.into(), // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. _ => Into::::into(error), })?; @@ -198,7 +200,12 @@ impl tcp::Host for T { socket .tcp_socket() .as_socketlike_view::() - .listen(None)?; + .listen(None) + .map_err(|error| match error.errno() { + #[cfg(windows)] + Some(Errno::MFILE) => ErrorCode::OutOfMemory.into(), // We're not trying to create a new socket. Rewrite it to less surprising error code. + _ => Into::::into(error), + })?; socket.tcp_state = HostTcpState::ListenStarted; @@ -240,6 +247,9 @@ impl tcp::Host for T { .accept_with(Blocking::No) }) .map_err(|error| match error.errno() { + #[cfg(windows)] + Some(Errno::INPROGRESS) => ErrorCode::WouldBlock.into(), // "A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function." + // Normalize Linux' non-standard behavior. // "Linux accept() passes already-pending network errors on the new socket as an error code from accept(). This behavior differs from other BSD socket implementations." #[cfg(target_os = "linux")] @@ -319,20 +329,25 @@ impl tcp::Host for T { fn ipv6_only(&mut self, this: tcp::TcpSocket) -> Result { let table = self.table(); let socket = table.get_tcp_socket(this)?; - Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?) + + match socket.family { + AddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), + AddressFamily::Ipv6 => Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?), + } } fn set_ipv6_only(&mut self, this: tcp::TcpSocket, value: bool) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_tcp_socket(this)?; - match socket.tcp_state { - HostTcpState::Default => {} - HostTcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), - _ => return Err(ErrorCode::InvalidState.into()), + match socket.family { + AddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), + AddressFamily::Ipv6 => match socket.tcp_state { + HostTcpState::Default => Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?), + HostTcpState::BindStarted => Err(ErrorCode::ConcurrencyConflict.into()), + _ => Err(ErrorCode::InvalidState.into()), + }, } - - Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?) } fn set_listen_backlog_size( @@ -534,6 +549,7 @@ impl tcp::Host for T { | HostTcpState::BindStarted | HostTcpState::Bound | HostTcpState::ListenStarted + | HostTcpState::ConnectFailed | HostTcpState::ConnectReady => {} HostTcpState::Listening | HostTcpState::Connecting | HostTcpState::Connected => { @@ -640,4 +656,4 @@ fn normalize_getsockopt_buffer_size(value: u64) -> u64 { } else { value } -} \ No newline at end of file +} From d764d62f5e43958b9aa79604674c86f9205625ec Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 30 Sep 2023 21:26:09 +0200 Subject: [PATCH 18/42] Simplify integration tests. All platforms supported by wasmtime also support dualstack sockets. --- .../wasi-sockets-tests/src/bin/tcp_bind.rs | 21 +++++++------------ .../wasi-sockets-tests/src/bin/tcp_connect.rs | 9 +------- .../src/bin/tcp_sockopts.rs | 8 +++---- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs index 39c2e918ceaf..997dbf19a179 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs @@ -98,20 +98,13 @@ fn test_tcp_bind_dual_stack(net: &NetworkResource) { Err(ErrorCode::InvalidArgument) )); - match tcp::set_ipv6_only(sock.handle, false) { - Err(ErrorCode::NotSupported) => { - println!("Skipping dual stack test"); - return; - } - Err(e) => panic!("Unexpected set_ipv6_only error code: {:?}", e), - Ok(_) => { - sock.bind(net, addr).unwrap(); - - let bound_addr = tcp::local_address(sock.handle).unwrap(); - - assert_eq!(bound_addr.family(), IpAddressFamily::Ipv6); - } - } + tcp::set_ipv6_only(sock.handle, false).unwrap(); + + sock.bind(net, addr).unwrap(); + + let bound_addr = tcp::local_address(sock.handle).unwrap(); + + assert_eq!(bound_addr.family(), IpAddressFamily::Ipv6); } fn main() { diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs index 220060259059..cccc6b1cb446 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs @@ -92,14 +92,7 @@ fn test_tcp_connect_dual_stack(net: &NetworkResource) { Err(ErrorCode::InvalidArgument) )); - match tcp::set_ipv6_only(v6_client.handle, false) { - Err(ErrorCode::NotSupported) => { - println!("Skipping dual stack test"); - return; - } - Err(e) => panic!("Unexpected set_ipv6_only error code: {:?}", e), - Ok(_) => {} - } + tcp::set_ipv6_only(v6_client.handle, false).unwrap(); v6_client.connect(net, v6_listener_addr).unwrap(); diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs index b8e9106f039f..91eb32e84019 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -25,7 +25,7 @@ fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { if family == IpAddressFamily::Ipv6 { assert!(matches!(tcp::set_ipv6_only(sock.handle, true), Ok(_))); - assert!(matches!(tcp::set_ipv6_only(sock.handle, false), Ok(_) | Err(ErrorCode::NotSupported))); + assert!(matches!(tcp::set_ipv6_only(sock.handle, false), Ok(_))); } // FIXME: #7034 @@ -55,10 +55,8 @@ fn test_tcp_sockopt_readback(family: IpAddressFamily) { if family == IpAddressFamily::Ipv6 { tcp::set_ipv6_only(sock.handle, true).unwrap(); assert_eq!(tcp::ipv6_only(sock.handle).unwrap(), true); - - if let Ok(_) = tcp::set_ipv6_only(sock.handle, false) { - assert_eq!(tcp::ipv6_only(sock.handle).unwrap(), false); - } + tcp::set_ipv6_only(sock.handle, false).unwrap(); + assert_eq!(tcp::ipv6_only(sock.handle).unwrap(), false); } tcp::set_keep_alive(sock.handle, true).unwrap(); From 9c0df8acfa24f4da7a1365987fadffe978dceafc Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 30 Sep 2023 21:26:26 +0200 Subject: [PATCH 19/42] Test ipv6_only inheritence --- .../wasi-sockets-tests/src/bin/tcp_sockopts.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs index 91eb32e84019..b10676b622c2 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -84,10 +84,15 @@ fn test_tcp_sockopt_inheritance(net: &NetworkResource, family: IpAddressFamily) let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); let listener = TcpSocketResource::new(family).unwrap(); + let default_ipv6_only = tcp::ipv6_only(listener.handle).unwrap_or(false); let default_keep_alive = tcp::keep_alive(listener.handle).unwrap(); // Configure options on listener: { + if family == IpAddressFamily::Ipv6 { + tcp::set_ipv6_only(listener.handle, !default_ipv6_only).unwrap(); + } + tcp::set_keep_alive(listener.handle, !default_keep_alive).unwrap(); tcp::set_no_delay(listener.handle, true).unwrap(); tcp::set_unicast_hop_limit(listener.handle, 42).unwrap(); @@ -105,6 +110,10 @@ fn test_tcp_sockopt_inheritance(net: &NetworkResource, family: IpAddressFamily) // Verify options on accepted socket: { + if family == IpAddressFamily::Ipv6 { + assert_eq!(tcp::ipv6_only(accepted_client.handle).unwrap(), !default_ipv6_only); + } + assert_eq!(tcp::keep_alive(accepted_client.handle).unwrap(), !default_keep_alive); assert_eq!(tcp::no_delay(accepted_client.handle).unwrap(), true); assert_eq!(tcp::unicast_hop_limit(accepted_client.handle).unwrap(), 42); From 3b81ec27d5ad58e672d588bebb5cf5230e07800d Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 1 Oct 2023 00:11:09 +0200 Subject: [PATCH 20/42] Test that socket options keep being respected, even if listen() has already been called --- .../src/bin/tcp_sockopts.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs index 51ee79a2edf0..4febb9977608 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -140,6 +140,38 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) { } } +fn test_tcp_sockopt_after_listen(net: &Network, family: IpAddressFamily) { + + let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let listener = TcpSocket::new(family).unwrap(); + listener.blocking_bind(&net, bind_addr).unwrap(); + listener.blocking_listen().unwrap(); + let bound_addr = listener.local_address().unwrap(); + + let default_keep_alive = listener.keep_alive().unwrap(); + + // Update options while the socket is already listening: + { + listener.set_keep_alive(!default_keep_alive).unwrap(); + listener.set_no_delay(true).unwrap(); + listener.set_unicast_hop_limit(42).unwrap(); + listener.set_receive_buffer_size(0x10000).unwrap(); + listener.set_send_buffer_size(0x10000).unwrap(); + } + + let client = TcpSocket::new(family).unwrap(); + client.blocking_connect(&net, bound_addr).unwrap(); + let (accepted_client, _, _) = listener.accept().unwrap(); + + // Verify options on accepted socket: + { + assert_eq!(accepted_client.keep_alive().unwrap(), !default_keep_alive); + assert_eq!(accepted_client.no_delay().unwrap(), true); + assert_eq!(accepted_client.unicast_hop_limit().unwrap(), 42); + assert_eq!(accepted_client.receive_buffer_size().unwrap(), 0x10000); + assert_eq!(accepted_client.send_buffer_size().unwrap(), 0x10000); + } +} fn main() { let net = Network::default(); @@ -155,4 +187,7 @@ fn main() { test_tcp_sockopt_inheritance(&net, IpAddressFamily::Ipv4); test_tcp_sockopt_inheritance(&net, IpAddressFamily::Ipv6); + + test_tcp_sockopt_after_listen(&net, IpAddressFamily::Ipv4); + test_tcp_sockopt_after_listen(&net, IpAddressFamily::Ipv6); } From 0901e19588cb4e2defa9dafb8cae7e54806ed10e Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 1 Oct 2023 10:05:27 +0200 Subject: [PATCH 21/42] cargo fmt --- crates/test-programs/tests/wasi-sockets.rs | 2 +- .../wasi-sockets-tests/src/bin/tcp_connect.rs | 2 +- .../src/bin/tcp_sockopts.rs | 13 +- .../wasi-sockets-tests/src/bin/tcp_states.rs | 134 ++++-------------- crates/wasi/src/preview2/tcp.rs | 10 +- 5 files changed, 43 insertions(+), 118 deletions(-) diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index f48852628f6f..2a3274755288 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -88,4 +88,4 @@ async fn tcp_states() { #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn tcp_sockopts() { run("tcp_sockopts").await.unwrap(); -} \ No newline at end of file +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs index ff7fe8893153..3629ad2bfca1 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs @@ -1,4 +1,4 @@ -use wasi::sockets::network::{Network, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network}; use wasi::sockets::tcp::TcpSocket; use wasi_sockets_tests::*; diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs index 4febb9977608..cc2086fcca46 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -1,9 +1,8 @@ -use wasi::sockets::network::{Network, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network}; use wasi::sockets::tcp::TcpSocket; use wasi_sockets_tests::*; fn test_tcp_sockopt_defaults(family: IpAddressFamily) { - let sock = TcpSocket::new(family).unwrap(); assert_eq!(sock.address_family(), family); @@ -20,7 +19,6 @@ fn test_tcp_sockopt_defaults(family: IpAddressFamily) { } fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { - let sock = TcpSocket::new(family).unwrap(); if family == IpAddressFamily::Ipv6 { @@ -38,7 +36,10 @@ fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { assert!(matches!(sock.set_no_delay(true), Ok(_))); assert!(matches!(sock.set_no_delay(false), Ok(_))); - assert!(matches!(sock.set_unicast_hop_limit(0), Err(ErrorCode::InvalidArgument))); + assert!(matches!( + sock.set_unicast_hop_limit(0), + Err(ErrorCode::InvalidArgument) + )); assert!(matches!(sock.set_unicast_hop_limit(1), Ok(_))); assert!(matches!(sock.set_unicast_hop_limit(u8::MAX), Ok(_))); @@ -49,7 +50,6 @@ fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { } fn test_tcp_sockopt_readback(family: IpAddressFamily) { - let sock = TcpSocket::new(family).unwrap(); if family == IpAddressFamily::Ipv6 { @@ -80,7 +80,6 @@ fn test_tcp_sockopt_readback(family: IpAddressFamily) { } fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) { - let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); let listener = TcpSocket::new(family).unwrap(); @@ -100,7 +99,6 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) { listener.set_send_buffer_size(0x10000).unwrap(); } - listener.blocking_bind(&net, bind_addr).unwrap(); listener.blocking_listen().unwrap(); let bound_addr = listener.local_address().unwrap(); @@ -141,7 +139,6 @@ fn test_tcp_sockopt_inheritance(net: &Network, family: IpAddressFamily) { } fn test_tcp_sockopt_after_listen(net: &Network, family: IpAddressFamily) { - let bind_addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0); let listener = TcpSocket::new(family).unwrap(); listener.blocking_bind(&net, bind_addr).unwrap(); diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs index 3cfc42ea0d95..3a2761f412ec 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs @@ -1,15 +1,12 @@ -use wasi::sockets::network::{Network, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress}; -use wasi::sockets::tcp::{TcpSocket, ShutdownType}; +use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network}; +use wasi::sockets::tcp::{ShutdownType, TcpSocket}; use wasi_sockets_tests::*; fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { let sock = TcpSocket::new(family).unwrap(); // Skipping: tcp::start_bind - assert!(matches!( - sock.finish_bind(), - Err(ErrorCode::NotInProgress) - )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); // Skipping: tcp::start_connect assert!(matches!( sock.finish_connect(), @@ -23,19 +20,13 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { sock.finish_listen(), Err(ErrorCode::NotInProgress) )); - assert!(matches!( - sock.accept(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.accept(), Err(ErrorCode::InvalidState))); assert!(matches!( sock.shutdown(ShutdownType::Both), Err(ErrorCode::InvalidState) )); - assert!(matches!( - sock.local_address(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.local_address(), Err(ErrorCode::InvalidState))); assert!(matches!( sock.remote_address(), Err(ErrorCode::InvalidState) @@ -49,10 +40,7 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { // setting ipv6_only to true (disabling dualstack mode) should work. assert!(matches!(sock.set_ipv6_only(true), Ok(_))); } else { - assert!(matches!( - sock.ipv6_only(), - Err(ErrorCode::NotSupported) - )); + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); assert!(matches!( sock.set_ipv6_only(true), Err(ErrorCode::NotSupported) @@ -65,20 +53,11 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { assert!(matches!(sock.no_delay(), Ok(_))); assert!(matches!(sock.set_no_delay(false), Ok(_))); assert!(matches!(sock.unicast_hop_limit(), Ok(_))); - assert!(matches!( - sock.set_unicast_hop_limit(255), - Ok(_) - )); + assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_))); assert!(matches!(sock.receive_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_receive_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); assert!(matches!(sock.send_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_send_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); } fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { @@ -90,10 +69,7 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { sock.start_bind(net, bind_address), Err(ErrorCode::InvalidState) )); - assert!(matches!( - sock.finish_bind(), - Err(ErrorCode::NotInProgress) - )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); // Skipping: tcp::start_connect assert!(matches!( sock.finish_connect(), @@ -104,10 +80,7 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { sock.finish_listen(), Err(ErrorCode::NotInProgress) )); - assert!(matches!( - sock.accept(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.accept(), Err(ErrorCode::InvalidState))); assert!(matches!( sock.shutdown(ShutdownType::Both), Err(ErrorCode::InvalidState) @@ -127,10 +100,7 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { Err(ErrorCode::InvalidState) )); } else { - assert!(matches!( - sock.ipv6_only(), - Err(ErrorCode::NotSupported) - )); + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); assert!(matches!( sock.set_ipv6_only(true), Err(ErrorCode::NotSupported) @@ -143,20 +113,11 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { assert!(matches!(sock.no_delay(), Ok(_))); assert!(matches!(sock.set_no_delay(false), Ok(_))); assert!(matches!(sock.unicast_hop_limit(), Ok(_))); - assert!(matches!( - sock.set_unicast_hop_limit(255), - Ok(_) - )); + assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_))); assert!(matches!(sock.receive_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_receive_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); assert!(matches!(sock.send_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_send_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); } fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { @@ -169,10 +130,7 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { sock.start_bind(net, bind_address), Err(ErrorCode::InvalidState) )); - assert!(matches!( - sock.finish_bind(), - Err(ErrorCode::NotInProgress) - )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); assert!(matches!( sock.start_connect(net, bind_address), // Actual address shouldn't matter Err(ErrorCode::InvalidState) @@ -181,10 +139,7 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { sock.finish_connect(), Err(ErrorCode::NotInProgress) )); - assert!(matches!( - sock.start_listen(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.start_listen(), Err(ErrorCode::InvalidState))); assert!(matches!( sock.finish_listen(), Err(ErrorCode::NotInProgress) @@ -209,10 +164,7 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { Err(ErrorCode::InvalidState) )); } else { - assert!(matches!( - sock.ipv6_only(), - Err(ErrorCode::NotSupported) - )); + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); assert!(matches!( sock.set_ipv6_only(true), Err(ErrorCode::NotSupported) @@ -225,20 +177,11 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { assert!(matches!(sock.no_delay(), Ok(_))); assert!(matches!(sock.set_no_delay(false), Ok(_))); assert!(matches!(sock.unicast_hop_limit(), Ok(_))); - assert!(matches!( - sock.set_unicast_hop_limit(255), - Ok(_) - )); + assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_))); assert!(matches!(sock.receive_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_receive_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); assert!(matches!(sock.send_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_send_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); } fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { @@ -254,10 +197,7 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { sock.start_bind(net, bind_address), Err(ErrorCode::InvalidState) )); - assert!(matches!( - sock.finish_bind(), - Err(ErrorCode::NotInProgress) - )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); assert!(matches!( sock.start_connect(net, addr_listener), Err(ErrorCode::InvalidState) @@ -266,18 +206,12 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { sock.finish_connect(), Err(ErrorCode::NotInProgress) )); - assert!(matches!( - sock.start_listen(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.start_listen(), Err(ErrorCode::InvalidState))); assert!(matches!( sock.finish_listen(), Err(ErrorCode::NotInProgress) )); - assert!(matches!( - sock.accept(), - Err(ErrorCode::InvalidState) - )); + assert!(matches!(sock.accept(), Err(ErrorCode::InvalidState))); // Skipping: tcp::shutdown assert!(matches!(sock.local_address(), Ok(_))); @@ -291,10 +225,7 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { Err(ErrorCode::InvalidState) )); } else { - assert!(matches!( - sock.ipv6_only(), - Err(ErrorCode::NotSupported) - )); + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); assert!(matches!( sock.set_ipv6_only(true), Err(ErrorCode::NotSupported) @@ -306,20 +237,11 @@ fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { assert!(matches!(sock.no_delay(), Ok(_))); assert!(matches!(sock.set_no_delay(false), Ok(_))); assert!(matches!(sock.unicast_hop_limit(), Ok(_))); - assert!(matches!( - sock.set_unicast_hop_limit(255), - Ok(_) - )); + assert!(matches!(sock.set_unicast_hop_limit(255), Ok(_))); assert!(matches!(sock.receive_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_receive_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_receive_buffer_size(16000), Ok(_))); assert!(matches!(sock.send_buffer_size(), Ok(_))); - assert!(matches!( - sock.set_send_buffer_size(16000), - Ok(_) - )); + assert!(matches!(sock.set_send_buffer_size(16000), Ok(_))); } fn main() { diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 985c2fd74f4b..521b26fca84c 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -229,12 +229,18 @@ impl TcpSocket { /// Create a `TcpSocket` from an existing socket. /// /// The socket must be in non-blocking mode. - pub fn from_tcp_stream(tcp_socket: cap_std::net::TcpStream, family: AddressFamily) -> io::Result { + pub fn from_tcp_stream( + tcp_socket: cap_std::net::TcpStream, + family: AddressFamily, + ) -> io::Result { let tcp_listener = TcpListener::from(rustix::fd::OwnedFd::from(tcp_socket)); Self::from_tcp_listener(tcp_listener, family) } - pub fn from_tcp_listener(tcp_listener: cap_std::net::TcpListener, family: AddressFamily) -> io::Result { + pub fn from_tcp_listener( + tcp_listener: cap_std::net::TcpListener, + family: AddressFamily, + ) -> io::Result { let fd = tcp_listener.into_raw_socketlike(); let std_stream = unsafe { std::net::TcpStream::from_raw_socketlike(fd) }; let stream = with_ambient_tokio_runtime(|| tokio::net::TcpStream::try_from(std_stream))?; From 54bee5d0f40c7899d7a020c508a6d40c310f8a53 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 1 Oct 2023 10:11:01 +0200 Subject: [PATCH 22/42] Duplicate .wit changes to wasi-http --- .../wit/deps/sockets/ip-name-lookup.wit | 6 +- crates/wasi-http/wit/deps/sockets/network.wit | 61 +++------- .../wit/deps/sockets/tcp-create-socket.wit | 5 +- crates/wasi-http/wit/deps/sockets/tcp.wit | 104 ++++++++++-------- .../wit/deps/sockets/udp-create-socket.wit | 5 +- crates/wasi-http/wit/deps/sockets/udp.wit | 53 +++++---- 6 files changed, 109 insertions(+), 125 deletions(-) diff --git a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit index f998aae140ab..15c7041c9274 100644 --- a/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit +++ b/crates/wasi-http/wit/deps/sockets/ip-name-lookup.wit @@ -25,9 +25,9 @@ interface ip-name-lookup { /// to `resolve-next-address` never returns `ok(none)`. This may change in the future. /// /// # Typical errors - /// - `invalid-name`: `name` is a syntactically invalid domain name. - /// - `invalid-name`: `name` is an IP address. - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) + /// - `invalid-argument`: `name` is a syntactically invalid domain name. + /// - `invalid-argument`: `name` is an IP address. + /// - `not-supported`: The specified `address-family` is not supported. (EAI_FAMILY) /// /// # References: /// - diff --git a/crates/wasi-http/wit/deps/sockets/network.wit b/crates/wasi-http/wit/deps/sockets/network.wit index 8214eaaf7211..61341398111f 100644 --- a/crates/wasi-http/wit/deps/sockets/network.wit +++ b/crates/wasi-http/wit/deps/sockets/network.wit @@ -14,6 +14,7 @@ interface network { /// - `access-denied` /// - `not-supported` /// - `out-of-memory` + /// - `concurrency-conflict` /// /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. enum error-code { @@ -32,6 +33,11 @@ interface network { /// POSIX equivalent: EOPNOTSUPP not-supported, + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + /// Not enough memory to complete the operation. /// /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY @@ -41,6 +47,8 @@ interface network { timeout, /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY concurrency-conflict, /// Trying to finish an asynchronous operation that: @@ -56,71 +64,35 @@ interface network { would-block, - // ### IP ERRORS ### - - /// The specified address-family is not supported. - address-family-not-supported, - - /// An IPv4 address was passed to an IPv6 resource, or vice versa. - address-family-mismatch, - - /// The socket address is not a valid remote address. E.g. the IP address is set to INADDR_ANY, or the port is set to 0. - invalid-remote-address, - - /// The operation is only supported on IPv4 resources. - ipv4-only-operation, - - /// The operation is only supported on IPv6 resources. - ipv6-only-operation, - - // ### TCP & UDP SOCKET ERRORS ### + /// The operation is not valid in the socket's current state. + invalid-state, + /// A new socket resource could not be created because of a system limit. new-socket-limit, - - /// The socket is already attached to another network. - already-attached, - - /// The socket is already bound. - already-bound, - - /// The socket is already in the Connection state. - already-connected, - - /// The socket is not bound to any local address. - not-bound, - - /// The socket is not in the Connection state. - not-connected, /// A bind operation failed because the provided address is not an address that the `network` can bind to. address-not-bindable, - /// A bind operation failed because the provided address is already in use. + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. address-in-use, - /// A bind operation failed because there are no ephemeral ports available. - ephemeral-ports-exhausted, - /// The remote address is not reachable remote-unreachable, // ### TCP SOCKET ERRORS ### - - /// The socket is already in the Listener state. - already-listening, - - /// The socket is already in the Listener state. - not-listening, /// The connection was forcefully rejected connection-refused, /// The connection was reset. connection-reset, + + /// A connection was aborted. + connection-aborted, // ### UDP SOCKET ERRORS ### @@ -128,9 +100,6 @@ interface network { // ### NAME LOOKUP ERRORS ### - - /// The provided name is a syntactically invalid domain name. - invalid-name, /// Name does not exist or has no suitable associated IP addresses. name-unresolvable, diff --git a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit index f43bc8979047..977b8cbf7cd3 100644 --- a/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi-http/wit/deps/sockets/tcp-create-socket.wit @@ -14,9 +14,8 @@ interface tcp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `not-supported`: The host does not support TCP sockets. (EOPNOTSUPP) - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) - /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References /// - diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit index 175626cc7620..d672395f362b 100644 --- a/crates/wasi-http/wit/deps/sockets/tcp.wit +++ b/crates/wasi-http/wit/deps/sockets/tcp.wit @@ -30,12 +30,13 @@ interface tcp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) - /// - `already-bound`: The socket is already bound. (EINVAL) - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. @@ -55,21 +56,33 @@ interface tcp { /// - the socket is transitioned into the Connection state /// - a pair of streams is returned that can be used to read & write to the connection /// + /// POSIX mentions: + /// > If connect() fails, the state of the socket is unspecified. Conforming applications should + /// > close the file descriptor and create a new socket before attempting to reconnect. + /// + /// WASI prescribes the following behavior: + /// - If `connect` fails because an input/state validation error, the socket should remain usable. + /// - If a connection was actually attempted but failed, the socket should become unusable for further network communication. + /// Besides `drop`, any method after such a failure may return an error. + /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) - /// - `already-attached`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - /// - `already-connected`: The socket is already in the Connection state. (EISCONN) - /// - `already-listening`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address, but the socket has `ipv6-only` enabled. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the Connection state. (EISCONN) + /// - `invalid-state`: The socket is already in the Listener state. (EOPNOTSUPP, EINVAL on Windows) /// /// # Typical `finish` errors /// - `timeout`: Connection timed out. (ETIMEDOUT) /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// @@ -90,13 +103,12 @@ interface tcp { /// - the socket must already be explicitly bound. /// /// # Typical `start` errors - /// - `not-bound`: The socket is not bound to any local address. (EDESTADDRREQ) - /// - `already-connected`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) - /// - `already-listening`: The socket is already in the Listener state. - /// - `concurrency-conflict`: Another `bind`, `connect` or `listen` operation is already in progress. (EINVAL on BSD) + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the Connection state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the Listener state. /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) /// - `not-in-progress`: A `listen` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// @@ -110,16 +122,23 @@ interface tcp { /// Accept a new client socket. /// - /// The returned socket is bound and in the Connection state. + /// The returned socket is bound and in the Connection state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `ipv6-only` + /// - `keep-alive` + /// - `no-delay` + /// - `unicast-hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` /// /// On success, this function returns the newly accepted client socket along with /// a pair of streams that can be used to read & write to the connection. - /// + /// /// # Typical errors - /// - `not-listening`: Socket is not in the Listener state. (EINVAL) - /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) - /// - /// Host implementations must skip over transient errors returned by the native accept syscall. + /// - `invalid-state`: Socket is not in the Listener state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References /// - @@ -130,8 +149,14 @@ interface tcp { /// Get the bound local address. /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. + /// - `invalid-state`: The socket is not bound to any local address. /// /// # References /// - @@ -140,10 +165,10 @@ interface tcp { /// - local-address: func() -> result - /// Get the bound remote address. + /// Get the remote address. /// /// # Typical errors - /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) /// /// # References /// - @@ -162,40 +187,34 @@ interface tcp { /// Equivalent to the IPV6_V6ONLY socket option. /// /// # Typical errors - /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. - /// - `already-bound`: (set) The socket is already bound. + /// - `invalid-state`: (set) The socket is already bound. + /// - `not-supported`: (get/set) `this` socket is an IPv4 socket. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) ipv6-only: func() -> result set-ipv6-only: func(value: bool) -> result<_, error-code> /// Hints the desired listen queue size. Implementations are free to ignore this. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// - `invalid-state`: (set) The socket is already in the Connection state. set-listen-backlog-size: func(value: u64) -> result<_, error-code> /// Equivalent to the SO_KEEPALIVE socket option. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) keep-alive: func() -> result set-keep-alive: func(value: bool) -> result<_, error-code> /// Equivalent to the TCP_NODELAY socket option. /// - /// # Typical errors - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// The default value is `false`. no-delay: func() -> result set-no-delay: func(value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `already-listening`: (set) The socket is already in the Listener state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + /// - `invalid-state`: (set) The socket is already in the Connection state. + /// - `invalid-state`: (set) The socket is already in the Listener state. unicast-hop-limit: func() -> result set-unicast-hop-limit: func(value: u8) -> result<_, error-code> @@ -211,9 +230,8 @@ interface tcp { /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. /// /// # Typical errors - /// - `already-connected`: (set) The socket is already in the Connection state. - /// - `already-listening`: (set) The socket is already in the Listener state. - /// - `concurrency-conflict`: (set) A `bind`, `connect` or `listen` operation is already in progress. (EALREADY) + /// - `invalid-state`: (set) The socket is already in the Connection state. + /// - `invalid-state`: (set) The socket is already in the Listener state. receive-buffer-size: func() -> result set-receive-buffer-size: func(value: u64) -> result<_, error-code> send-buffer-size: func() -> result @@ -237,7 +255,7 @@ interface tcp { /// The shutdown function does not close (drop) the socket. /// /// # Typical errors - /// - `not-connected`: The socket is not in the Connection state. (ENOTCONN) + /// - `invalid-state`: The socket is not in the Connection state. (ENOTCONN) /// /// # References /// - diff --git a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit index cd4c08fb1000..ada6ef07723b 100644 --- a/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi-http/wit/deps/sockets/udp-create-socket.wit @@ -14,9 +14,8 @@ interface udp-create-socket { /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. /// /// # Typical errors - /// - `not-supported`: The host does not support UDP sockets. (EOPNOTSUPP) - /// - `address-family-not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) - /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) /// /// # References: /// - diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit index 01e5b95b97b7..fd76c739335b 100644 --- a/crates/wasi-http/wit/deps/sockets/udp.wit +++ b/crates/wasi-http/wit/deps/sockets/udp.wit @@ -31,12 +31,11 @@ interface udp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `local-address` has the wrong address family. (EINVAL) - /// - `already-bound`: The socket is already bound. (EINVAL) - /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) /// - `address-in-use`: Address is already in use. (EADDRINUSE) /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) /// - `not-in-progress`: A `bind` operation is not in progress. @@ -63,14 +62,14 @@ interface udp { /// Unlike in POSIX, this function is async. This enables interactive WASI hosts to inject permission prompts. /// /// # Typical `start` errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `already-attached`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. - /// - `concurrency-conflict`: Another `bind` or `connect` operation is already in progress. (EALREADY) + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is already bound to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. /// /// # Typical `finish` errors - /// - `ephemeral-ports-exhausted`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) /// - `not-in-progress`: A `connect` operation is not in progress. /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) /// @@ -89,7 +88,7 @@ interface udp { /// If `max-results` is 0, this function returns successfully with an empty list. /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. (EINVAL) + /// - `invalid-state`: The socket is not bound to any local address. (EINVAL) /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `would-block`: There is no pending data available to be read at the moment. (EWOULDBLOCK, EAGAIN) /// @@ -119,11 +118,12 @@ interface udp { /// call `remote-address` to get their address. /// /// # Typical errors - /// - `address-family-mismatch`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) - /// - `invalid-remote-address`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `invalid-remote-address`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) - /// - `already-connected`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) - /// - `not-bound`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is a non-IPv4-mapped IPv6 address, but the socket was bound to a specific IPv4-mapped IPv6 address. (or vice versa) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and the `datagram.remote-address` does not match the address passed to `connect`. (EISCONN) + /// - `invalid-state`: The socket is not bound to any local address. Unlike POSIX, this function does not perform an implicit bind. /// - `remote-unreachable`: The remote address is not reachable. (ECONNREFUSED, ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN) /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) /// - `would-block`: The send buffer is currently full. (EWOULDBLOCK, EAGAIN) @@ -141,8 +141,14 @@ interface udp { /// Get the current bound address. /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// /// # Typical errors - /// - `not-bound`: The socket is not bound to any local address. + /// - `invalid-state`: The socket is not bound to any local address. /// /// # References /// - @@ -154,7 +160,7 @@ interface udp { /// Get the address set with `connect`. /// /// # Typical errors - /// - `not-connected`: The socket is not connected to a remote address. (ENOTCONN) + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) /// /// # References /// - @@ -173,17 +179,13 @@ interface udp { /// Equivalent to the IPV6_V6ONLY socket option. /// /// # Typical errors - /// - `ipv6-only-operation`: (get/set) `this` socket is an IPv4 socket. - /// - `already-bound`: (set) The socket is already bound. + /// - `not-supported`: (get/set) `this` socket is an IPv4 socket. + /// - `invalid-state`: (set) The socket is already bound. /// - `not-supported`: (set) Host does not support dual-stack sockets. (Implementations are not required to.) - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) ipv6-only: func() -> result set-ipv6-only: func(value: bool) -> result<_, error-code> /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) unicast-hop-limit: func() -> result set-unicast-hop-limit: func(value: u8) -> result<_, error-code> @@ -197,9 +199,6 @@ interface udp { /// for internal metadata structures. /// /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. - /// - /// # Typical errors - /// - `concurrency-conflict`: (set) Another `bind` or `connect` operation is already in progress. (EALREADY) receive-buffer-size: func() -> result set-receive-buffer-size: func(value: u64) -> result<_, error-code> send-buffer-size: func() -> result From fd91898d330029381624c40b390c31eea67c5f7c Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Mon, 2 Oct 2023 20:23:53 +0200 Subject: [PATCH 23/42] prtest:full From 21b1b8de99956050b99a5889fb6c47fb836052e5 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Wed, 4 Oct 2023 17:07:38 +0200 Subject: [PATCH 24/42] Fix BSD behavior of SO_SNDBUF/SO_RCVBUF --- crates/wasi/src/preview2/host/tcp.rs | 30 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index b927b53010f4..b514d07f7ed5 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -469,11 +469,22 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { ) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_resource(&this)?; + let value = normalize_setsockopt_buffer_size(value); + + match sockopt::set_socket_recv_buffer_size(socket.tcp_socket(), value) { + // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value + // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds + // their capability. + // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the + // value is out of range. We normalize this behavior in favor of the more commonly understood + // "performance hint" semantics. In other words; even ENOBUFS is "Ok". + // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input + // `size` ourselves, to completely close the gap with other platforms. + Err(Errno::NOBUFS) => Ok(()), + r => r, + }?; - Ok(sockopt::set_socket_recv_buffer_size( - socket.tcp_socket(), - normalize_setsockopt_buffer_size(value), - )?) + Ok(()) } fn send_buffer_size(&mut self, this: Resource) -> Result { @@ -491,11 +502,14 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { ) -> Result<(), network::Error> { let table = self.table(); let socket = table.get_resource(&this)?; + let value = normalize_setsockopt_buffer_size(value); + + match sockopt::set_socket_send_buffer_size(socket.tcp_socket(), value) { + Err(Errno::NOBUFS) => Ok(()), // See `set_receive_buffer_size` + r => r, + }?; - Ok(sockopt::set_socket_send_buffer_size( - socket.tcp_socket(), - normalize_setsockopt_buffer_size(value), - )?) + Ok(()) } fn subscribe(&mut self, this: Resource) -> anyhow::Result> { From ceb0319b086081fa91837c72b503f3d3b89f0095 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Wed, 4 Oct 2023 18:16:10 +0200 Subject: [PATCH 25/42] fmt --- crates/wasi/src/preview2/tcp.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 12d2896d27a9..54e3af943319 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -60,7 +60,8 @@ pub struct TcpSocket { /// The desired listen queue size. Set to None to use the system's default. pub(crate) listen_backlog_size: Option, - pub(crate) family: AddressFamily,} + pub(crate) family: AddressFamily, +} pub(crate) struct TcpReadStream { stream: Arc, From d92099daf0ca774e77ec6886e8fe292ad3868ded Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 5 Oct 2023 09:41:49 +0200 Subject: [PATCH 26/42] Fix type error --- crates/wasi/src/preview2/ip_name_lookup.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/wasi/src/preview2/ip_name_lookup.rs b/crates/wasi/src/preview2/ip_name_lookup.rs index d0acfdeca36f..c33b23440bb9 100644 --- a/crates/wasi/src/preview2/ip_name_lookup.rs +++ b/crates/wasi/src/preview2/ip_name_lookup.rs @@ -5,7 +5,6 @@ use crate::preview2::bindings::sockets::network::{ use crate::preview2::poll::{subscribe, Pollable, Subscribe}; use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, WasiView}; use anyhow::Result; -use std::io; use std::mem; use std::net::{SocketAddr, ToSocketAddrs}; use std::pin::Pin; @@ -13,8 +12,8 @@ use std::vec; use wasmtime::component::Resource; pub enum ResolveAddressStream { - Waiting(AbortOnDropJoinHandle>>), - Done(io::Result>), + Waiting(AbortOnDropJoinHandle, Error>>), + Done(Result, Error>), } #[async_trait::async_trait] @@ -50,7 +49,7 @@ impl Host for T { // the usage of the `ToSocketAddrs` trait. This blocks the current // thread, so use `spawn_blocking`. Finally note that this is only // resolving names, not ports, so force the port to be 0. - let task = spawn_blocking(move || -> io::Result> { + let task = spawn_blocking(move || -> Result, Error> { let result = (name.as_str(), 0) .to_socket_addrs() .map_err(|_| ErrorCode::NameUnresolvable)?; // If/when we use `getaddrinfo` directly, map the error properly. @@ -102,12 +101,6 @@ impl HostResolveAddressStream for T { } } ResolveAddressStream::Done(slot @ Err(_)) => { - // TODO: this `?` is what converts `io::Error` into `Error` - // and the conversion is not great right now. The standard - // library doesn't expose a ton of information through the - // return value of `getaddrinfo` right now so supporting a - // richer conversion here will probably require calling - // `getaddrinfo` directly. mem::replace(slot, Ok(Vec::new().into_iter()))?; unreachable!(); } From 9b5886c91955f8ab56a93b2985b833f4bcaa00a2 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 5 Oct 2023 17:42:54 +0200 Subject: [PATCH 27/42] Got lost during merge --- .../wasi-sockets-tests/src/bin/tcp_sample_application.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs index 94fcb5856893..9f182f2c4660 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs @@ -12,6 +12,7 @@ fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddres let listener = TcpSocket::new(family).unwrap(); listener.blocking_bind(&net, bind_address).unwrap(); + listener.set_listen_backlog_size(32).unwrap(); listener.blocking_listen().unwrap(); let addr = listener.local_address().unwrap(); From 5a2da6e44e0a5c427779f963ddd6ec935122a56a Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 5 Oct 2023 17:53:40 +0200 Subject: [PATCH 28/42] Implement listen backlog tests --- .../wasi-sockets-tests/src/bin/tcp_sockopts.rs | 5 ++--- .../wasi-sockets-tests/src/bin/tcp_states.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs index cc2086fcca46..c1a5df8ea7a5 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -26,9 +26,8 @@ fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { assert!(matches!(sock.set_ipv6_only(false), Ok(_))); } - // FIXME: #7034 - // assert!(matches!(sock.set_listen_backlog_size(0), Ok(_))); // Unsupported sizes should be silently capped. - // assert!(matches!(sock.set_listen_backlog_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_listen_backlog_size(0), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_listen_backlog_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. assert!(matches!(sock.set_keep_alive(true), Ok(_))); assert!(matches!(sock.set_keep_alive(false), Ok(_))); diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs index 3a2761f412ec..8547749e5e68 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs @@ -47,7 +47,7 @@ fn test_tcp_unbound_state_invariants(family: IpAddressFamily) { )); } - // assert!(matches!(sock.set_listen_backlog_size(32), Ok(_))); // FIXME + assert!(matches!(sock.set_listen_backlog_size(32), Ok(_))); assert!(matches!(sock.keep_alive(), Ok(_))); assert!(matches!(sock.set_keep_alive(false), Ok(_))); assert!(matches!(sock.no_delay(), Ok(_))); @@ -107,7 +107,7 @@ fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { )); } - // assert!(matches!(sock.set_listen_backlog_size(32), Err(ErrorCode::AlreadyBound))); // FIXME + assert!(matches!(sock.set_listen_backlog_size(32), Ok(_))); assert!(matches!(sock.keep_alive(), Ok(_))); assert!(matches!(sock.set_keep_alive(false), Ok(_))); assert!(matches!(sock.no_delay(), Ok(_))); @@ -171,7 +171,10 @@ fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { )); } - // assert!(matches!(sock.set_listen_backlog_size(32), Err(ErrorCode::AlreadyBound))); // FIXME + assert!(matches!( + sock.set_listen_backlog_size(32), + Ok(_) | Err(ErrorCode::NotSupported) + )); assert!(matches!(sock.keep_alive(), Ok(_))); assert!(matches!(sock.set_keep_alive(false), Ok(_))); assert!(matches!(sock.no_delay(), Ok(_))); From cba24c08fced8b63d4f0a34c2145a7daf116fd59 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Thu, 5 Oct 2023 21:20:01 +0200 Subject: [PATCH 29/42] Manually inherit buffer size from listener on MacOS. I suspect that these changes apply to any BSD platform, but I can't test that. --- crates/wasi/src/preview2/host/tcp.rs | 34 ++++++++++++++++++++++++---- crates/wasi/src/preview2/tcp.rs | 11 +++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index f73bf15d6cfb..a2c7be2be202 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -270,6 +270,22 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { _ => Into::::into(error), })?; + + #[cfg(target_os = "macos")] + { + // Manually inherit buffer size from listener. We only have to + // do this on platforms that don't already do this automatically + // and only if a specific buffer size was explicitly set on the listener. + + if let Some(size) = socket.receive_buffer_size { + _ = sockopt::set_socket_recv_buffer_size(&connection, size); // Ignore potential error. + } + + if let Some(size) = socket.send_buffer_size { + _ = sockopt::set_socket_send_buffer_size(&connection, size); // Ignore potential error. + } + } + let mut tcp_socket = TcpSocket::from_tcp_stream(connection, socket.family)?; // Mark the socket as connected so that we can exit early from methods like `start-bind`. @@ -491,8 +507,8 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, value: u64, ) -> Result<(), network::Error> { - let table = self.table(); - let socket = table.get_resource(&this)?; + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; let value = normalize_setsockopt_buffer_size(value); match sockopt::set_socket_recv_buffer_size(socket.tcp_socket(), value) { @@ -508,6 +524,11 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { r => r, }?; + #[cfg(target_os = "macos")] + { + socket.receive_buffer_size = Some(value); + } + Ok(()) } @@ -524,8 +545,8 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, value: u64, ) -> Result<(), network::Error> { - let table = self.table(); - let socket = table.get_resource(&this)?; + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; let value = normalize_setsockopt_buffer_size(value); match sockopt::set_socket_send_buffer_size(socket.tcp_socket(), value) { @@ -533,6 +554,11 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { r => r, }?; + #[cfg(target_os = "macos")] + { + socket.send_buffer_size = Some(value); + } + Ok(()) } diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 54e3af943319..ded40131669f 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -61,6 +61,13 @@ pub struct TcpSocket { pub(crate) listen_backlog_size: Option, pub(crate) family: AddressFamily, + + /// The manually configured buffer size. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) receive_buffer_size: Option, + /// The manually configured buffer size. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) send_buffer_size: Option, } pub(crate) struct TcpReadStream { @@ -275,6 +282,10 @@ impl TcpSocket { tcp_state: TcpState::Default, listen_backlog_size: None, family, + #[cfg(target_os = "macos")] + receive_buffer_size: None, + #[cfg(target_os = "macos")] + send_buffer_size: None, }) } From 565583df8021a79773066089021c39f64d349e33 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 7 Oct 2023 10:34:12 +0200 Subject: [PATCH 30/42] Keep track of IPV6_V6ONLY ourselves. - This provides cross-platform behaviour for `ipv6-only` - This eliminates the syscall in `validate_address_family` --- crates/wasi/src/preview2/host/tcp.rs | 68 ++++++++++++++++------------ crates/wasi/src/preview2/tcp.rs | 27 ++++++++--- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index a2c7be2be202..74c5f34c3c46 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -1,12 +1,15 @@ use super::network::SystemError; -use crate::preview2::bindings::{ - io::streams::{InputStream, OutputStream}, - sockets::network::{self, ErrorCode, IpAddressFamily, IpSocketAddress, Network}, - sockets::tcp::{self, ShutdownType}, -}; use crate::preview2::tcp::{TcpSocket, TcpState}; +use crate::preview2::{ + bindings::{ + io::streams::{InputStream, OutputStream}, + sockets::network::{self, ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::tcp::{self, ShutdownType}, + }, + tcp::SocketAddressFamily, +}; use crate::preview2::{Pollable, WasiView}; -use cap_net_ext::{AddressFamily, Blocking, PoolExt, TcpListenerExt}; +use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; use rustix::io::Errno; @@ -347,16 +350,22 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { let table = self.table(); let socket = table.get_resource(&this)?; - Ok(socket.family.into()) + match socket.family { + SocketAddressFamily::Ipv4 => Ok(IpAddressFamily::Ipv4), + SocketAddressFamily::Ipv6 { .. } => Ok(IpAddressFamily::Ipv6), + } } fn ipv6_only(&mut self, this: Resource) -> Result { let table = self.table(); let socket = table.get_resource(&this)?; + // Instead of just calling the OS we return our own internal state, because + // MacOS doesn't propogate the V6ONLY state on to accepted client sockets. + match socket.family { - AddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), - AddressFamily::Ipv6 => Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?), + SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), + SocketAddressFamily::Ipv6 { v6only } => Ok(v6only), } } @@ -365,13 +374,17 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, value: bool, ) -> Result<(), network::Error> { - let table = self.table(); - let socket = table.get_resource(&this)?; + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; match socket.family { - AddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), - AddressFamily::Ipv6 => match socket.tcp_state { - TcpState::Default => Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?), + SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), + SocketAddressFamily::Ipv6 { .. } => match socket.tcp_state { + TcpState::Default => { + sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?; + socket.family = SocketAddressFamily::Ipv6 { v6only: value }; + Ok(()) + } TcpState::BindStarted => Err(ErrorCode::ConcurrencyConflict.into()), _ => Err(ErrorCode::InvalidState.into()), }, @@ -457,10 +470,12 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { let socket = table.get_resource(&this)?; let ttl = match socket.family { - AddressFamily::Ipv4 => sockopt::get_ip_ttl(socket.tcp_socket())? + SocketAddressFamily::Ipv4 => sockopt::get_ip_ttl(socket.tcp_socket())? .try_into() .unwrap(), - AddressFamily::Ipv6 => sockopt::get_ipv6_unicast_hops(socket.tcp_socket())?, + SocketAddressFamily::Ipv6 { .. } => { + sockopt::get_ipv6_unicast_hops(socket.tcp_socket())? + } }; Ok(ttl) @@ -482,8 +497,8 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } match socket.family { - AddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?, - AddressFamily::Ipv6 => { + SocketAddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?, + SocketAddressFamily::Ipv6 { .. } => { sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value))? } } @@ -671,19 +686,16 @@ fn validate_remote_address(addr: &SocketAddr) -> Result<(), network::Error> { fn validate_address_family(socket: &TcpSocket, addr: &SocketAddr) -> Result<(), network::Error> { match (socket.family, addr.ip()) { - (AddressFamily::Ipv4, IpAddr::V4(_)) => {} - (AddressFamily::Ipv6, IpAddr::V6(ipv6)) => { - if let Some(_) = ipv6.to_ipv4_mapped() { - if sockopt::get_ipv6_v6only(socket.tcp_socket())? { - // Address is IPv4-mapped IPv6 address, but socket is IPv6-only. - return Err(ErrorCode::InvalidArgument.into()); - } + (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()), + (SocketAddressFamily::Ipv6 { v6only }, IpAddr::V6(ipv6)) => { + if v6only && ipv6.to_ipv4_mapped().is_some() { + Err(ErrorCode::InvalidArgument.into()) + } else { + Ok(()) } } - _ => return Err(ErrorCode::InvalidArgument.into()), + _ => Err(ErrorCode::InvalidArgument.into()), } - - Ok(()) } fn to_canonical_compat(addr: &IpAddr) -> IpAddr { diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index ded40131669f..88caf31078ba 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -6,6 +6,7 @@ use anyhow::{Error, Result}; use cap_net_ext::{AddressFamily, Blocking, TcpListenerExt}; use cap_std::net::TcpListener; use io_lifetimes::raw::{FromRawSocketlike, IntoRawSocketlike}; +use rustix::net::sockopt; use std::io; use std::mem; use std::sync::Arc; @@ -60,7 +61,7 @@ pub struct TcpSocket { /// The desired listen queue size. Set to None to use the system's default. pub(crate) listen_backlog_size: Option, - pub(crate) family: AddressFamily, + pub(crate) family: SocketAddressFamily, /// The manually configured buffer size. `None` means: no preference, use system default. #[cfg(target_os = "macos")] @@ -70,6 +71,12 @@ pub struct TcpSocket { pub(crate) send_buffer_size: Option, } +#[derive(Copy, Clone)] +pub(crate) enum SocketAddressFamily { + Ipv4, + Ipv6 { v6only: bool }, +} + pub(crate) struct TcpReadStream { stream: Arc, closed: bool, @@ -255,23 +262,31 @@ impl TcpSocket { // Create a new host socket and set it to non-blocking, which is needed // by our async implementation. let tcp_listener = TcpListener::new(family, Blocking::No)?; - Self::from_tcp_listener(tcp_listener, family) + + let socket_address_family = match family { + AddressFamily::Ipv4 => SocketAddressFamily::Ipv4, + AddressFamily::Ipv6 => SocketAddressFamily::Ipv6 { + v6only: sockopt::get_ipv6_v6only(&tcp_listener)?, + }, + }; + + Self::from_tcp_listener(tcp_listener, socket_address_family) } /// Create a `TcpSocket` from an existing socket. /// /// The socket must be in non-blocking mode. - pub fn from_tcp_stream( + pub(crate) fn from_tcp_stream( tcp_socket: cap_std::net::TcpStream, - family: AddressFamily, + family: SocketAddressFamily, ) -> io::Result { let tcp_listener = TcpListener::from(rustix::fd::OwnedFd::from(tcp_socket)); Self::from_tcp_listener(tcp_listener, family) } - pub fn from_tcp_listener( + pub(crate) fn from_tcp_listener( tcp_listener: cap_std::net::TcpListener, - family: AddressFamily, + family: SocketAddressFamily, ) -> io::Result { let fd = tcp_listener.into_raw_socketlike(); let std_stream = unsafe { std::net::TcpStream::from_raw_socketlike(fd) }; From 3767e18e213c1bad3130c5321514074bec080385 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sat, 7 Oct 2023 11:35:59 +0200 Subject: [PATCH 31/42] Reject IPv4-compatible IPv6 addresses. --- crates/wasi/src/preview2/host/tcp.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 74c5f34c3c46..48f01598dd5a 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -14,7 +14,7 @@ use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; use rustix::io::Errno; use rustix::net::sockopt; -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use tokio::io::Interest; use wasmtime::component::Resource; @@ -654,7 +654,7 @@ const INPROGRESS: Errno = Errno::INPROGRESS; const INPROGRESS: Errno = Errno::WOULDBLOCK; fn validate_unicast(addr: &SocketAddr) -> Result<(), network::Error> { - match to_canonical_compat(&addr.ip()) { + match to_canonical(&addr.ip()) { IpAddr::V4(ipv4) => { if ipv4.is_multicast() || ipv4.is_broadcast() { Err(ErrorCode::InvalidArgument.into()) @@ -673,7 +673,7 @@ fn validate_unicast(addr: &SocketAddr) -> Result<(), network::Error> { } fn validate_remote_address(addr: &SocketAddr) -> Result<(), network::Error> { - if to_canonical_compat(&addr.ip()).is_unspecified() { + if to_canonical(&addr.ip()).is_unspecified() { return Err(ErrorCode::InvalidArgument.into()); } @@ -688,7 +688,13 @@ fn validate_address_family(socket: &TcpSocket, addr: &SocketAddr) -> Result<(), match (socket.family, addr.ip()) { (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()), (SocketAddressFamily::Ipv6 { v6only }, IpAddr::V6(ipv6)) => { - if v6only && ipv6.to_ipv4_mapped().is_some() { + if is_deprecated_ipv4_compatible(&ipv6) { + // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated + // since 2006, OS handling of them is inconsistent and our own + // validations don't take them into account either. + // Note that these are not the same as IPv4-*mapped* IPv6 addresses. + Err(ErrorCode::InvalidArgument.into()) + } else if v6only && ipv6.to_ipv4_mapped().is_some() { Err(ErrorCode::InvalidArgument.into()) } else { Ok(()) @@ -698,14 +704,13 @@ fn validate_address_family(socket: &TcpSocket, addr: &SocketAddr) -> Result<(), } } -fn to_canonical_compat(addr: &IpAddr) -> IpAddr { +// Can be removed once `IpAddr::to_canonical` becomes stable. +fn to_canonical(addr: &IpAddr) -> IpAddr { match addr { IpAddr::V4(ipv4) => IpAddr::V4(*ipv4), IpAddr::V6(ipv6) => { if let Some(ipv4) = ipv6.to_ipv4_mapped() { IpAddr::V4(ipv4) - } else if let Some(ipv4) = ipv6.to_ipv4() { - IpAddr::V4(ipv4) } else { IpAddr::V6(*ipv6) } @@ -713,6 +718,12 @@ fn to_canonical_compat(addr: &IpAddr) -> IpAddr { } } +fn is_deprecated_ipv4_compatible(addr: &Ipv6Addr) -> bool { + matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _]) + && *addr != Ipv6Addr::UNSPECIFIED + && *addr != Ipv6Addr::LOCALHOST +} + fn normalize_setsockopt_buffer_size(value: u64) -> usize { value.clamp(1, i32::MAX as u64).try_into().unwrap() } From 0e41f747346ff8691ef5a509ffb65ef3d487134d Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Wed, 4 Oct 2023 13:09:31 +0200 Subject: [PATCH 32/42] feat(wasi-sockets): implement UDP This is based on TCP implementation Signed-off-by: Roman Volosatovs --- crates/test-programs/tests/wasi-sockets.rs | 10 + .../wasi-sockets-tests/src/bin/tcp_v4.rs | 2 +- .../wasi-sockets-tests/src/bin/tcp_v6.rs | 4 +- .../wasi-sockets-tests/src/bin/udp_v4.rs | 28 + .../wasi-sockets-tests/src/bin/udp_v6.rs | 30 ++ .../wasi-sockets-tests/src/lib.rs | 91 +++- crates/wasi-http/wit/test.wit | 2 + crates/wasi/src/preview2/command.rs | 5 + crates/wasi/src/preview2/host/mod.rs | 2 + crates/wasi/src/preview2/host/udp.rs | 488 ++++++++++++++++++ .../src/preview2/host/udp_create_socket.rs | 15 + crates/wasi/src/preview2/mod.rs | 2 + crates/wasi/src/preview2/udp.rs | 87 ++++ crates/wasi/wit/test.wit | 2 + 14 files changed, 763 insertions(+), 5 deletions(-) create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs create mode 100644 crates/wasi/src/preview2/host/udp.rs create mode 100644 crates/wasi/src/preview2/host/udp_create_socket.rs create mode 100644 crates/wasi/src/preview2/udp.rs diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index 8484a4f98f77..3ff9367d610c 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -76,6 +76,16 @@ async fn tcp_v6() { run("tcp_v6").await.unwrap(); } +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn udp_v4() { + run("udp_v4").await.unwrap(); +} + +#[test_log::test(tokio::test(flavor = "multi_thread"))] +async fn udp_v6() { + run("udp_v6").await.unwrap(); +} + #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn ip_name_lookup() { run("ip_name_lookup").await.unwrap(); diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs index cf02fdd79663..21daef42691b 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs @@ -24,5 +24,5 @@ fn main() { sock.finish_bind().unwrap(); - example_body(net, sock, IpAddressFamily::Ipv4) + example_body_tcp(net, sock, IpAddressFamily::Ipv4) } diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs index 807db9825f1e..696e6cc0f5b8 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs @@ -1,4 +1,4 @@ -//! Like v4.rs, but with IPv6. +//! Like tcp_v4.rs, but with IPv6. use wasi::io::poll; use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; @@ -26,5 +26,5 @@ fn main() { sock.finish_bind().unwrap(); - example_body(net, sock, IpAddressFamily::Ipv6) + example_body_tcp(net, sock, IpAddressFamily::Ipv6) } diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs new file mode 100644 index 000000000000..86d606cad96f --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs @@ -0,0 +1,28 @@ +//! A simple UDP testcase, using IPv4. + +use wasi::io::poll; +use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress}; +use wasi::sockets::{instance_network, udp_create_socket}; +use wasi_sockets_tests::*; + +fn main() { + let net = instance_network::instance_network(); + + let sock = udp_create_socket::create_udp_socket(IpAddressFamily::Ipv4).unwrap(); + + let addr = IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, // use any free port + address: (127, 0, 0, 1), // localhost + }); + + let sub = sock.subscribe(); + + sock.start_bind(&net, addr).unwrap(); + + poll::poll_one(&sub); + drop(sub); + + sock.finish_bind().unwrap(); + + example_body_udp(net, sock, IpAddressFamily::Ipv4) +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs new file mode 100644 index 000000000000..58d455997283 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs @@ -0,0 +1,30 @@ +//! Like udp_v4.rs, but with IPv6. + +use wasi::io::poll; +use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; +use wasi::sockets::{instance_network, udp_create_socket}; +use wasi_sockets_tests::*; + +fn main() { + let net = instance_network::instance_network(); + + let sock = udp_create_socket::create_udp_socket(IpAddressFamily::Ipv6).unwrap(); + + let addr = IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: 0, // use any free port + address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost + flow_info: 0, + scope_id: 0, + }); + + let sub = sock.subscribe(); + + sock.start_bind(&net, addr).unwrap(); + + poll::poll_one(&sub); + drop(sub); + + sock.finish_bind().unwrap(); + + example_body_udp(net, sock, IpAddressFamily::Ipv6) +} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index a46cff830c0f..c3ac2669917d 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -2,7 +2,7 @@ wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); use wasi::io::poll; use wasi::io::streams; -use wasi::sockets::{network, tcp, tcp_create_socket}; +use wasi::sockets::{network, tcp, tcp_create_socket, udp, udp_create_socket}; pub fn write(output: &streams::OutputStream, mut bytes: &[u8]) -> Result<(), streams::StreamError> { let pollable = output.subscribe(); @@ -24,7 +24,7 @@ pub fn write(output: &streams::OutputStream, mut bytes: &[u8]) -> Result<(), str Ok(()) } -pub fn example_body(net: tcp::Network, sock: tcp::TcpSocket, family: network::IpAddressFamily) { +pub fn example_body_tcp(net: tcp::Network, sock: tcp::TcpSocket, family: network::IpAddressFamily) { let first_message = b"Hello, world!"; let second_message = b"Greetings, planet!"; @@ -95,3 +95,90 @@ pub fn example_body(net: tcp::Network, sock: tcp::TcpSocket, family: network::Ip // Check that we sent and recieved our message! assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. } + +pub fn example_body_udp(net: udp::Network, sock: udp::UdpSocket, family: network::IpAddressFamily) { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let sub = sock.subscribe(); + + let addr = sock.local_address().unwrap(); + + let client = udp_create_socket::create_udp_socket(family).unwrap(); + let client_sub = client.subscribe(); + + client.start_connect(&net, addr).unwrap(); + poll::poll_one(&client_sub); + client.finish_connect().unwrap(); + + let _client_addr = client.local_address().unwrap(); + + let n = client + .send(&[ + udp::Datagram { + data: vec![], + remote_address: addr, + }, + udp::Datagram { + data: first_message.to_vec(), + remote_address: addr, + }, + ]) + .unwrap(); + assert_eq!(n, 2); + + drop(client_sub); + drop(client); + + poll::poll_one(&sub); + let datagrams = sock.receive(2).unwrap(); + let mut datagrams = datagrams.into_iter(); + let (first, second) = match (datagrams.next(), datagrams.next(), datagrams.next()) { + (Some(first), Some(second), None) => (first, second), + (Some(_first), None, None) => panic!("only one datagram received"), + (None, None, None) => panic!("no datagrams received"), + _ => panic!("invalid datagram sequence received"), + }; + + assert!(first.data.is_empty()); + + // TODO: Verify the `remote_address` + //assert_eq!(first.remote_address, client_addr); + + // Check that we sent and recieved our message! + assert_eq!(second.data, first_message); // Not guaranteed to work but should work in practice. + + // TODO: Verify the `remote_address` + //assert_eq!(second.remote_address, client_addr); + + // Another client + let client = udp_create_socket::create_udp_socket(family).unwrap(); + let client_sub = client.subscribe(); + + client.start_connect(&net, addr).unwrap(); + poll::poll_one(&client_sub); + client.finish_connect().unwrap(); + + let n = client + .send(&[udp::Datagram { + data: second_message.to_vec(), + remote_address: addr, + }]) + .unwrap(); + assert_eq!(n, 1); + + drop(client_sub); + drop(client); + + poll::poll_one(&sub); + let datagrams = sock.receive(2).unwrap(); + let mut datagrams = datagrams.into_iter(); + let first = match (datagrams.next(), datagrams.next()) { + (Some(first), None) => first, + (None, None) => panic!("no datagrams received"), + _ => panic!("invalid datagram sequence received"), + }; + + // Check that we sent and recieved our message! + assert_eq!(first.data, second_message); // Not guaranteed to work but should work in practice. +} diff --git a/crates/wasi-http/wit/test.wit b/crates/wasi-http/wit/test.wit index fc9c357522bf..a0d1d07a6c64 100644 --- a/crates/wasi-http/wit/test.wit +++ b/crates/wasi-http/wit/test.wit @@ -37,6 +37,8 @@ world test-command-with-sockets { import wasi:cli/stderr; import wasi:sockets/tcp; import wasi:sockets/tcp-create-socket; + import wasi:sockets/udp; + import wasi:sockets/udp-create-socket; import wasi:sockets/network; import wasi:sockets/instance-network; import wasi:sockets/ip-name-lookup; diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs index 811e3cf18e2c..898311157354 100644 --- a/crates/wasi/src/preview2/command.rs +++ b/crates/wasi/src/preview2/command.rs @@ -48,6 +48,8 @@ pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> any crate::preview2::bindings::cli::terminal_stderr::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::tcp::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::udp::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::udp_create_socket::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?; @@ -65,6 +67,7 @@ pub mod sync { "wasi:filesystem/types": crate::preview2::bindings::sync_io::filesystem::types, "wasi:filesystem/preopens": crate::preview2::bindings::filesystem::preopens, "wasi:sockets/tcp": crate::preview2::bindings::sockets::tcp, + "wasi:sockets/udp": crate::preview2::bindings::sockets::udp, "wasi:clocks/monotonic_clock": crate::preview2::bindings::clocks::monotonic_clock, "wasi:io/poll": crate::preview2::bindings::sync_io::io::poll, "wasi:io/streams": crate::preview2::bindings::sync_io::io::streams, @@ -107,6 +110,8 @@ pub mod sync { crate::preview2::bindings::cli::terminal_stderr::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::tcp::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::udp::add_to_linker(l, |t| t)?; + crate::preview2::bindings::sockets::udp_create_socket::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?; crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?; diff --git a/crates/wasi/src/preview2/host/mod.rs b/crates/wasi/src/preview2/host/mod.rs index 138166731565..651d2cd38e0c 100644 --- a/crates/wasi/src/preview2/host/mod.rs +++ b/crates/wasi/src/preview2/host/mod.rs @@ -8,3 +8,5 @@ mod network; mod random; mod tcp; mod tcp_create_socket; +mod udp; +mod udp_create_socket; diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs new file mode 100644 index 000000000000..38dadc938808 --- /dev/null +++ b/crates/wasi/src/preview2/host/udp.rs @@ -0,0 +1,488 @@ +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; + +use crate::preview2::{ + bindings::{ + sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::udp, + }, + udp::UdpState, + Table, +}; +use crate::preview2::{Pollable, SocketResult, WasiView}; +use cap_net_ext::PoolExt; +use io_lifetimes::AsSocketlike; +use rustix::io::Errno; +use rustix::net::sockopt; +use wasmtime::component::Resource; + +/// Theoretical maximum byte size of a UDP datagram, the real limit is lower, +/// but we do not account for e.g. the transport layer here for simplicity. +/// In practice, datagrams are typically less than 1500 bytes. +const MAX_UDP_DATAGRAM_SIZE: usize = 65535; + +fn start_bind( + table: &mut Table, + this: Resource, + network: Resource, + local_address: IpSocketAddress, +) -> SocketResult<()> { + let socket = table.get_resource(&this)?; + match socket.udp_state { + UdpState::Default => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let network = table.get_resource(&network)?; + let binder = network.pool.udp_binder(local_address)?; + + // Perform the OS bind call. + binder.bind_existing_udp_socket( + &*socket + .udp_socket() + .as_socketlike_view::(), + )?; + + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::BindStarted; + + Ok(()) +} + +fn finish_bind(table: &mut Table, this: Resource) -> SocketResult<()> { + let socket = table.get_resource_mut(&this)?; + match socket.udp_state { + UdpState::BindStarted => {} + _ => return Err(ErrorCode::NotInProgress.into()), + } + + socket.udp_state = UdpState::Bound; + + Ok(()) +} + +fn address_family(table: &Table, this: Resource) -> SocketResult { + let socket = table.get_resource(&this)?; + + // If `SO_DOMAIN` is available, use it. + // + // TODO: OpenBSD also supports this; upstream PRs are posted. + #[cfg(not(any( + windows, + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + )))] + { + use rustix::net::AddressFamily; + + let family = sockopt::get_socket_domain(socket.udp_socket())?; + let family = match family { + AddressFamily::INET => IpAddressFamily::Ipv4, + AddressFamily::INET6 => IpAddressFamily::Ipv6, + _ => return Err(ErrorCode::NotSupported.into()), + }; + Ok(family) + } + + // When `SO_DOMAIN` is not available, emulate it. + #[cfg(any( + windows, + target_os = "ios", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + if let Ok(_) = sockopt::get_ipv6_unicast_hops(socket.udp_socket()) { + return Ok(IpAddressFamily::Ipv6); + } + if let Ok(_) = sockopt::get_ip_ttl(socket.udp_socket()) { + return Ok(IpAddressFamily::Ipv4); + } + Err(ErrorCode::NotSupported.into()) + } +} + +impl udp::Host for T {} + +impl crate::preview2::host::udp::udp::HostUdpSocket for T { + fn start_bind( + &mut self, + this: Resource, + network: Resource, + local_address: IpSocketAddress, + ) -> SocketResult<()> { + start_bind(self.table_mut(), this, network, local_address) + } + + fn finish_bind(&mut self, this: Resource) -> SocketResult<()> { + finish_bind(self.table_mut(), this) + } + + fn start_connect( + &mut self, + this: Resource, + network: Resource, + remote_address: IpSocketAddress, + ) -> SocketResult<()> { + let table = self.table_mut(); + let r = { + let socket = table.get_resource(&this)?; + match socket.udp_state { + UdpState::Default => { + let family = address_family(table, Resource::new_borrow(this.rep()))?; + let addr = match family { + IpAddressFamily::Ipv4 => Ipv4Addr::UNSPECIFIED.into(), + IpAddressFamily::Ipv6 => Ipv6Addr::UNSPECIFIED.into(), + }; + start_bind( + table, + Resource::new_borrow(this.rep()), + Resource::new_borrow(network.rep()), + SocketAddr::new(addr, 0).into(), + )?; + finish_bind(table, Resource::new_borrow(this.rep()))?; + } + UdpState::BindStarted => { + finish_bind(table, Resource::new_borrow(this.rep()))?; + } + UdpState::Bound => {} + UdpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + _ => return Err(ErrorCode::NotInProgress.into()), + } + + let socket = table.get_resource(&this)?; + let network = table.get_resource(&network)?; + let connecter = network.pool.udp_connecter(remote_address)?; + + // Do an OS `connect`. Our socket is non-blocking, so it'll either... + { + let view = &*socket + .udp_socket() + .as_socketlike_view::(); + let r = connecter.connect_existing_udp_socket(view); + r + } + }; + + match r { + // succeed immediately, + Ok(()) => { + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::ConnectReady; + return Ok(()); + } + // continue in progress, + Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} + // or fail immediately. + Err(err) => return Err(err.into()), + } + + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::Connecting; + + Ok(()) + } + + fn finish_connect(&mut self, this: Resource) -> SocketResult<()> { + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; + + match socket.udp_state { + UdpState::ConnectReady => {} + UdpState::Connecting => { + // Do a `poll` to test for completion, using a timeout of zero + // to avoid blocking. + match rustix::event::poll( + &mut [rustix::event::PollFd::new( + socket.udp_socket(), + rustix::event::PollFlags::OUT, + )], + 0, + ) { + Ok(0) => return Err(ErrorCode::WouldBlock.into()), + Ok(_) => (), + Err(err) => Err(err).unwrap(), + } + + // Check whether the connect succeeded. + match sockopt::get_socket_error(socket.udp_socket()) { + Ok(Ok(())) => {} + Err(err) | Ok(Err(err)) => return Err(err.into()), + } + } + _ => return Err(ErrorCode::NotInProgress.into()), + }; + + socket.udp_state = UdpState::Connected; + Ok(()) + } + + fn receive( + &mut self, + this: Resource, + max_results: u64, + ) -> SocketResult> { + if max_results == 0 { + return Ok(vec![]); + } + + let table = self.table(); + let socket = table.get_resource(&this)?; + + let udp_socket = socket.udp_socket(); + let mut datagrams = Vec::with_capacity(max_results.try_into().unwrap_or(usize::MAX)); + let mut buf = [0; MAX_UDP_DATAGRAM_SIZE]; + match socket.udp_state { + UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), + UdpState::Bound | UdpState::Connecting | UdpState::ConnectReady => { + for i in 0..max_results { + match udp_socket.try_recv_from(&mut buf) { + Ok((size, remote_address)) => datagrams.push(udp::Datagram { + data: buf[..size].into(), + remote_address: remote_address.into(), + }), + Err(_e) if i > 0 => { + return Ok(datagrams); + } + Err(e) => return Err(e.into()), + } + } + } + UdpState::Connected => { + let remote_address = udp_socket.peer_addr().map(Into::into)?; + for i in 0..max_results { + match udp_socket.try_recv(&mut buf) { + Ok(size) => datagrams.push(udp::Datagram { + data: buf[..size].into(), + remote_address, + }), + Err(_e) if i > 0 => { + return Ok(datagrams); + } + Err(e) => return Err(e.into()), + } + } + } + } + Ok(datagrams) + } + + fn send( + &mut self, + this: Resource, + datagrams: Vec, + ) -> SocketResult { + if datagrams.is_empty() { + return Ok(0); + }; + let table = self.table(); + let socket = table.get_resource(&this)?; + + let udp_socket = socket.udp_socket(); + let mut count = 0; + match socket.udp_state { + UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), + UdpState::Bound | UdpState::Connecting | UdpState::ConnectReady => { + for udp::Datagram { + data, + remote_address, + } in datagrams + { + match udp_socket.try_send_to(&data, remote_address.into()) { + Ok(_size) => count += 1, + Err(_e) if count > 0 => { + return Ok(count); + } + Err(e) => return Err(e.into()), + } + } + } + UdpState::Connected => { + let peer_addr = udp_socket.peer_addr()?; + for udp::Datagram { + data, + remote_address, + } in datagrams + { + if SocketAddr::from(remote_address) != peer_addr { + // From WIT documentation: + // If at least one datagram has been sent successfully, this function never returns an error. + if count == 0 { + return Err(ErrorCode::AlreadyConnected.into()); + } else { + return Ok(count); + } + } + match udp_socket.try_send(&data) { + Ok(_size) => count += 1, + Err(_e) if count > 0 => { + return Ok(count); + } + Err(e) => return Err(e.into()), + } + } + } + } + Ok(count) + } + + fn local_address(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + let addr = socket + .udp_socket() + .as_socketlike_view::() + .local_addr()?; + Ok(addr.into()) + } + + fn remote_address(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + let addr = socket + .udp_socket() + .as_socketlike_view::() + .peer_addr()?; + Ok(addr.into()) + } + + fn address_family( + &mut self, + this: Resource, + ) -> Result { + let family = address_family(self.table(), this)?; + Ok(family) + } + + fn ipv6_only(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + Ok(sockopt::get_ipv6_v6only(socket.udp_socket())?) + } + + fn set_ipv6_only(&mut self, this: Resource, value: bool) -> SocketResult<()> { + let table = self.table(); + let socket = table.get_resource(&this)?; + Ok(sockopt::set_ipv6_v6only(socket.udp_socket(), value)?) + } + + fn unicast_hop_limit(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + + // We don't track whether the socket is IPv4 or IPv6 so try one and + // fall back to the other. + match sockopt::get_ipv6_unicast_hops(socket.udp_socket()) { + Ok(value) => Ok(value), + Err(Errno::NOPROTOOPT) => { + let value = sockopt::get_ip_ttl(socket.udp_socket())?; + let value = value.try_into().unwrap(); + Ok(value) + } + Err(err) => Err(err.into()), + } + } + + fn set_unicast_hop_limit( + &mut self, + this: Resource, + value: u8, + ) -> SocketResult<()> { + let table = self.table(); + let socket = table.get_resource(&this)?; + + // We don't track whether the socket is IPv4 or IPv6 so try one and + // fall back to the other. + match sockopt::set_ipv6_unicast_hops(socket.udp_socket(), Some(value)) { + Ok(()) => Ok(()), + Err(Errno::NOPROTOOPT) => Ok(sockopt::set_ip_ttl(socket.udp_socket(), value.into())?), + Err(err) => Err(err.into()), + } + } + + fn receive_buffer_size(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + Ok(sockopt::get_socket_recv_buffer_size(socket.udp_socket())? as u64) + } + + fn set_receive_buffer_size( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let table = self.table(); + let socket = table.get_resource(&this)?; + let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_recv_buffer_size( + socket.udp_socket(), + value, + )?) + } + + fn send_buffer_size(&mut self, this: Resource) -> SocketResult { + let table = self.table(); + let socket = table.get_resource(&this)?; + Ok(sockopt::get_socket_send_buffer_size(socket.udp_socket())? as u64) + } + + fn set_send_buffer_size( + &mut self, + this: Resource, + value: u64, + ) -> SocketResult<()> { + let table = self.table(); + let socket = table.get_resource(&this)?; + let value = value.try_into().map_err(|_| ErrorCode::OutOfMemory)?; + Ok(sockopt::set_socket_send_buffer_size( + socket.udp_socket(), + value, + )?) + } + + fn subscribe(&mut self, this: Resource) -> anyhow::Result> { + crate::preview2::poll::subscribe(self.table_mut(), this) + } + + fn drop(&mut self, this: Resource) -> Result<(), anyhow::Error> { + let table = self.table_mut(); + + // As in the filesystem implementation, we assume closing a socket + // doesn't block. + let dropped = table.delete_resource(this)?; + + // If we might have an `event::poll` waiting on the socket, wake it up. + #[cfg(not(unix))] + { + match dropped.udp_state { + UdpState::Default + | UdpState::BindStarted + | UdpState::Bound + | UdpState::ConnectReady => {} + + UdpState::Connecting | UdpState::Connected => { + match rustix::net::shutdown(&*dropped.inner, rustix::net::Shutdown::ReadWrite) { + Ok(()) | Err(Errno::NOTCONN) => {} + Err(err) => Err(err).unwrap(), + } + } + } + } + + drop(dropped); + + Ok(()) + } +} + +// On POSIX, non-blocking UDP socket `connect` uses `EINPROGRESS`. +// +#[cfg(not(windows))] +const INPROGRESS: Errno = Errno::INPROGRESS; + +// On Windows, non-blocking UDP socket `connect` uses `WSAEWOULDBLOCK`. +// +#[cfg(windows)] +const INPROGRESS: Errno = Errno::WOULDBLOCK; diff --git a/crates/wasi/src/preview2/host/udp_create_socket.rs b/crates/wasi/src/preview2/host/udp_create_socket.rs new file mode 100644 index 000000000000..7e57e19d5297 --- /dev/null +++ b/crates/wasi/src/preview2/host/udp_create_socket.rs @@ -0,0 +1,15 @@ +use crate::preview2::bindings::{sockets::network::IpAddressFamily, sockets::udp_create_socket}; +use crate::preview2::udp::UdpSocket; +use crate::preview2::{SocketResult, WasiView}; +use wasmtime::component::Resource; + +impl udp_create_socket::Host for T { + fn create_udp_socket( + &mut self, + address_family: IpAddressFamily, + ) -> SocketResult> { + let socket = UdpSocket::new(address_family.into())?; + let socket = self.table_mut().push_resource(socket)?; + Ok(socket) + } +} diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 8b188e686873..02e6ed405686 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -36,6 +36,7 @@ mod stdio; mod stream; mod table; mod tcp; +mod udp; mod write_stream; pub use self::clocks::{HostMonotonicClock, HostWallClock}; @@ -157,6 +158,7 @@ pub mod bindings { with: { "wasi:sockets/network/network": super::network::Network, "wasi:sockets/tcp/tcp-socket": super::tcp::TcpSocket, + "wasi:sockets/udp/udp-socket": super::udp::UdpSocket, "wasi:sockets/ip-name-lookup/resolve-address-stream": super::ip_name_lookup::ResolveAddressStream, "wasi:filesystem/types/directory-entry-stream": super::filesystem::ReaddirIterator, "wasi:filesystem/types/descriptor": super::filesystem::Descriptor, diff --git a/crates/wasi/src/preview2/udp.rs b/crates/wasi/src/preview2/udp.rs new file mode 100644 index 000000000000..b146a47ccd14 --- /dev/null +++ b/crates/wasi/src/preview2/udp.rs @@ -0,0 +1,87 @@ +use crate::preview2::poll::Subscribe; +use crate::preview2::with_ambient_tokio_runtime; +use async_trait::async_trait; +use cap_net_ext::{AddressFamily, Blocking, UdpSocketExt}; +use io_lifetimes::raw::{FromRawSocketlike, IntoRawSocketlike}; +use std::io; +use std::sync::Arc; +use tokio::io::Interest; + +/// The state of a UDP socket. +/// +/// This represents the various states a socket can be in during the +/// activities of binding, and connecting. +pub(crate) enum UdpState { + /// The initial state for a newly-created socket. + Default, + + /// Binding started via `start_bind`. + BindStarted, + + /// Binding finished via `finish_bind`. The socket has an address but + /// is not yet listening for connections. + Bound, + + /// An outgoing connection is started via `start_connect`. + Connecting, + + /// An outgoing connection is ready to be established. + ConnectReady, + + /// An outgoing connection has been established. + Connected, +} + +/// A host UDP socket, plus associated bookkeeping. +/// +/// The inner state is wrapped in an Arc because the same underlying socket is +/// used for implementing the stream types. +pub struct UdpSocket { + /// The part of a `UdpSocket` which is reference-counted so that we + /// can pass it to async tasks. + pub(crate) inner: Arc, + + /// The current state in the bind/connect progression. + pub(crate) udp_state: UdpState, +} + +#[async_trait] +impl Subscribe for UdpSocket { + async fn ready(&mut self) { + // Some states are ready immediately. + match self.udp_state { + UdpState::BindStarted | UdpState::ConnectReady => return, + _ => {} + } + + // FIXME: Add `Interest::ERROR` when we update to tokio 1.32. + self.inner + .ready(Interest::READABLE | Interest::WRITABLE) + .await + .expect("failed to await UDP socket readiness"); + } +} + +impl UdpSocket { + /// Create a new socket in the given family. + pub fn new(family: AddressFamily) -> io::Result { + // Create a new host socket and set it to non-blocking, which is needed + // by our async implementation. + let udp_socket = cap_std::net::UdpSocket::new(family, Blocking::No)?; + Self::from_udp_socket(udp_socket) + } + + pub fn from_udp_socket(udp_socket: cap_std::net::UdpSocket) -> io::Result { + let fd = udp_socket.into_raw_socketlike(); + let std_socket = unsafe { std::net::UdpSocket::from_raw_socketlike(fd) }; + let socket = with_ambient_tokio_runtime(|| tokio::net::UdpSocket::try_from(std_socket))?; + Ok(Self { + inner: Arc::new(socket), + udp_state: UdpState::Default, + }) + } + + pub fn udp_socket(&self) -> &tokio::net::UdpSocket { + &self.inner + } +} diff --git a/crates/wasi/wit/test.wit b/crates/wasi/wit/test.wit index fc9c357522bf..a0d1d07a6c64 100644 --- a/crates/wasi/wit/test.wit +++ b/crates/wasi/wit/test.wit @@ -37,6 +37,8 @@ world test-command-with-sockets { import wasi:cli/stderr; import wasi:sockets/tcp; import wasi:sockets/tcp-create-socket; + import wasi:sockets/udp; + import wasi:sockets/udp-create-socket; import wasi:sockets/network; import wasi:sockets/instance-network; import wasi:sockets/ip-name-lookup; From 62e5d7a669ca81488a90431db3b6befefd7eda90 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 6 Oct 2023 19:34:09 +0200 Subject: [PATCH 33/42] refactor(wasi-sockets): simplify UDP implementation This introduces quite a few changes compared to TCP, which should most probably be integrated there as well Signed-off-by: Roman Volosatovs --- crates/wasi/src/preview2/host/udp.rs | 194 +++++++++++---------------- crates/wasi/src/preview2/udp.rs | 13 +- 2 files changed, 85 insertions(+), 122 deletions(-) diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs index 38dadc938808..8e1c420770f0 100644 --- a/crates/wasi/src/preview2/host/udp.rs +++ b/crates/wasi/src/preview2/host/udp.rs @@ -9,7 +9,7 @@ use crate::preview2::{ Table, }; use crate::preview2::{Pollable, SocketResult, WasiView}; -use cap_net_ext::PoolExt; +use cap_net_ext::{AddressFamily, PoolExt}; use io_lifetimes::AsSocketlike; use rustix::io::Errno; use rustix::net::sockopt; @@ -29,7 +29,10 @@ fn start_bind( let socket = table.get_resource(&this)?; match socket.udp_state { UdpState::Default => {} - _ => return Err(ErrorCode::NotInProgress.into()), + UdpState::BindStarted | UdpState::Connecting | UdpState::ConnectReady => { + return Err(ErrorCode::ConcurrencyConflict.into()) + } + UdpState::Bound | UdpState::Connected => return Err(ErrorCode::AlreadyBound.into()), } let network = table.get_resource(&network)?; @@ -51,56 +54,11 @@ fn start_bind( fn finish_bind(table: &mut Table, this: Resource) -> SocketResult<()> { let socket = table.get_resource_mut(&this)?; match socket.udp_state { - UdpState::BindStarted => {} - _ => return Err(ErrorCode::NotInProgress.into()), - } - - socket.udp_state = UdpState::Bound; - - Ok(()) -} - -fn address_family(table: &Table, this: Resource) -> SocketResult { - let socket = table.get_resource(&this)?; - - // If `SO_DOMAIN` is available, use it. - // - // TODO: OpenBSD also supports this; upstream PRs are posted. - #[cfg(not(any( - windows, - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - )))] - { - use rustix::net::AddressFamily; - - let family = sockopt::get_socket_domain(socket.udp_socket())?; - let family = match family { - AddressFamily::INET => IpAddressFamily::Ipv4, - AddressFamily::INET6 => IpAddressFamily::Ipv6, - _ => return Err(ErrorCode::NotSupported.into()), - }; - Ok(family) - } - - // When `SO_DOMAIN` is not available, emulate it. - #[cfg(any( - windows, - target_os = "ios", - target_os = "macos", - target_os = "netbsd", - target_os = "openbsd" - ))] - { - if let Ok(_) = sockopt::get_ipv6_unicast_hops(socket.udp_socket()) { - return Ok(IpAddressFamily::Ipv6); - } - if let Ok(_) = sockopt::get_ip_ttl(socket.udp_socket()) { - return Ok(IpAddressFamily::Ipv4); + UdpState::BindStarted => { + socket.udp_state = UdpState::Bound; + Ok(()) } - Err(ErrorCode::NotSupported.into()) + _ => Err(ErrorCode::NotInProgress.into()), } } @@ -127,70 +85,64 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { remote_address: IpSocketAddress, ) -> SocketResult<()> { let table = self.table_mut(); - let r = { - let socket = table.get_resource(&this)?; - match socket.udp_state { - UdpState::Default => { - let family = address_family(table, Resource::new_borrow(this.rep()))?; - let addr = match family { - IpAddressFamily::Ipv4 => Ipv4Addr::UNSPECIFIED.into(), - IpAddressFamily::Ipv6 => Ipv6Addr::UNSPECIFIED.into(), - }; - start_bind( - table, - Resource::new_borrow(this.rep()), - Resource::new_borrow(network.rep()), - SocketAddr::new(addr, 0).into(), - )?; - finish_bind(table, Resource::new_borrow(this.rep()))?; - } - UdpState::BindStarted => { - finish_bind(table, Resource::new_borrow(this.rep()))?; - } - UdpState::Bound => {} - UdpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + let socket = table.get_resource(&this)?; + match socket.udp_state { + UdpState::Default => { + let addr = match socket.family { + AddressFamily::Ipv4 => Ipv4Addr::UNSPECIFIED.into(), + AddressFamily::Ipv6 => Ipv6Addr::UNSPECIFIED.into(), + }; + start_bind( + table, + Resource::new_borrow(this.rep()), + Resource::new_borrow(network.rep()), + SocketAddr::new(addr, 0).into(), + )?; + finish_bind(table, Resource::new_borrow(this.rep()))?; } - - let socket = table.get_resource(&this)?; - let network = table.get_resource(&network)?; - let connecter = network.pool.udp_connecter(remote_address)?; - - // Do an OS `connect`. Our socket is non-blocking, so it'll either... - { - let view = &*socket - .udp_socket() - .as_socketlike_view::(); - let r = connecter.connect_existing_udp_socket(view); - r + UdpState::Bound => {} + UdpState::BindStarted | UdpState::Connecting | UdpState::ConnectReady => { + return Err(ErrorCode::ConcurrencyConflict.into()) } - }; + UdpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + } - match r { + let socket = table.get_resource(&this)?; + let network = table.get_resource(&network)?; + let connecter = network.pool.udp_connecter(remote_address)?; + + // Do an OS `connect`. Our socket is non-blocking, so it'll either... + let res = connecter.connect_existing_udp_socket( + &*socket + .udp_socket() + .as_socketlike_view::(), + ); + match res { // succeed immediately, Ok(()) => { let socket = table.get_resource_mut(&this)?; socket.udp_state = UdpState::ConnectReady; - return Ok(()); + Ok(()) } // continue in progress, - Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} + Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => { + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::Connecting; + Ok(()) + } // or fail immediately. - Err(err) => return Err(err.into()), + Err(err) => Err(err.into()), } - - let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::Connecting; - - Ok(()) } fn finish_connect(&mut self, this: Resource) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; - match socket.udp_state { - UdpState::ConnectReady => {} + UdpState::ConnectReady => { + socket.udp_state = UdpState::Connected; + Ok(()) + } UdpState::Connecting => { // Do a `poll` to test for completion, using a timeout of zero // to avoid blocking. @@ -202,21 +154,21 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { 0, ) { Ok(0) => return Err(ErrorCode::WouldBlock.into()), - Ok(_) => (), - Err(err) => Err(err).unwrap(), + Ok(_) => {} + Err(err) => return Err(err.into()), } // Check whether the connect succeeded. match sockopt::get_socket_error(socket.udp_socket()) { - Ok(Ok(())) => {} - Err(err) | Ok(Err(err)) => return Err(err.into()), + Ok(Ok(())) => { + socket.udp_state = UdpState::Connected; + Ok(()) + } + Err(err) | Ok(Err(err)) => Err(err.into()), } } - _ => return Err(ErrorCode::NotInProgress.into()), - }; - - socket.udp_state = UdpState::Connected; - Ok(()) + _ => Err(ErrorCode::NotInProgress.into()), + } } fn receive( @@ -232,7 +184,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let socket = table.get_resource(&this)?; let udp_socket = socket.udp_socket(); - let mut datagrams = Vec::with_capacity(max_results.try_into().unwrap_or(usize::MAX)); + let mut datagrams = vec![]; let mut buf = [0; MAX_UDP_DATAGRAM_SIZE]; match socket.udp_state { UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), @@ -352,8 +304,12 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { &mut self, this: Resource, ) -> Result { - let family = address_family(self.table(), this)?; - Ok(family) + let table = self.table(); + let socket = table.get_resource(&this)?; + match socket.family { + AddressFamily::Ipv4 => Ok(IpAddressFamily::Ipv4), + AddressFamily::Ipv6 => Ok(IpAddressFamily::Ipv6), + } } fn ipv6_only(&mut self, this: Resource) -> SocketResult { @@ -477,12 +433,12 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { } } -// On POSIX, non-blocking UDP socket `connect` uses `EINPROGRESS`. -// -#[cfg(not(windows))] -const INPROGRESS: Errno = Errno::INPROGRESS; - -// On Windows, non-blocking UDP socket `connect` uses `WSAEWOULDBLOCK`. -// -#[cfg(windows)] -const INPROGRESS: Errno = Errno::WOULDBLOCK; +const INPROGRESS: Errno = if cfg!(windows) { + // On Windows, non-blocking UDP socket `connect` uses `WSAEWOULDBLOCK`. + // + Errno::WOULDBLOCK +} else { + // On POSIX, non-blocking UDP socket `connect` uses `EINPROGRESS`. + // + Errno::INPROGRESS +}; diff --git a/crates/wasi/src/preview2/udp.rs b/crates/wasi/src/preview2/udp.rs index b146a47ccd14..53a20572c19c 100644 --- a/crates/wasi/src/preview2/udp.rs +++ b/crates/wasi/src/preview2/udp.rs @@ -43,6 +43,9 @@ pub struct UdpSocket { /// The current state in the bind/connect progression. pub(crate) udp_state: UdpState, + + /// Socket address family. + pub(crate) family: AddressFamily, } #[async_trait] @@ -50,7 +53,7 @@ impl Subscribe for UdpSocket { async fn ready(&mut self) { // Some states are ready immediately. match self.udp_state { - UdpState::BindStarted | UdpState::ConnectReady => return, + UdpState::BindStarted => return, _ => {} } @@ -68,16 +71,20 @@ impl UdpSocket { // Create a new host socket and set it to non-blocking, which is needed // by our async implementation. let udp_socket = cap_std::net::UdpSocket::new(family, Blocking::No)?; - Self::from_udp_socket(udp_socket) + Self::from_udp_socket(udp_socket, family) } - pub fn from_udp_socket(udp_socket: cap_std::net::UdpSocket) -> io::Result { + pub fn from_udp_socket( + udp_socket: cap_std::net::UdpSocket, + family: AddressFamily, + ) -> io::Result { let fd = udp_socket.into_raw_socketlike(); let std_socket = unsafe { std::net::UdpSocket::from_raw_socketlike(fd) }; let socket = with_ambient_tokio_runtime(|| tokio::net::UdpSocket::try_from(std_socket))?; Ok(Self { inner: Arc::new(socket), udp_state: UdpState::Default, + family, }) } From 4dd48e7690c3b09f1d1cf5bf3765339703391bfd Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Fri, 6 Oct 2023 20:38:11 +0200 Subject: [PATCH 34/42] feat(wasi-sockets): store UDP connect address in state Signed-off-by: Roman Volosatovs --- crates/wasi/src/preview2/host/udp.rs | 33 ++++++++++++++-------------- crates/wasi/src/preview2/udp.rs | 7 +++--- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs index 8e1c420770f0..acf67a41e678 100644 --- a/crates/wasi/src/preview2/host/udp.rs +++ b/crates/wasi/src/preview2/host/udp.rs @@ -29,10 +29,10 @@ fn start_bind( let socket = table.get_resource(&this)?; match socket.udp_state { UdpState::Default => {} - UdpState::BindStarted | UdpState::Connecting | UdpState::ConnectReady => { + UdpState::BindStarted | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { return Err(ErrorCode::ConcurrencyConflict.into()) } - UdpState::Bound | UdpState::Connected => return Err(ErrorCode::AlreadyBound.into()), + UdpState::Bound | UdpState::Connected(..) => return Err(ErrorCode::AlreadyBound.into()), } let network = table.get_resource(&network)?; @@ -101,10 +101,10 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { finish_bind(table, Resource::new_borrow(this.rep()))?; } UdpState::Bound => {} - UdpState::BindStarted | UdpState::Connecting | UdpState::ConnectReady => { + UdpState::BindStarted | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { return Err(ErrorCode::ConcurrencyConflict.into()) } - UdpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), + UdpState::Connected(..) => return Err(ErrorCode::AlreadyConnected.into()), } let socket = table.get_resource(&this)?; @@ -121,13 +121,13 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { // succeed immediately, Ok(()) => { let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::ConnectReady; + socket.udp_state = UdpState::ConnectReady(remote_address); Ok(()) } // continue in progress, Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => { let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::Connecting; + socket.udp_state = UdpState::Connecting(remote_address); Ok(()) } // or fail immediately. @@ -139,11 +139,11 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; match socket.udp_state { - UdpState::ConnectReady => { - socket.udp_state = UdpState::Connected; + UdpState::ConnectReady(addr) => { + socket.udp_state = UdpState::Connected(addr); Ok(()) } - UdpState::Connecting => { + UdpState::Connecting(addr) => { // Do a `poll` to test for completion, using a timeout of zero // to avoid blocking. match rustix::event::poll( @@ -161,7 +161,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { // Check whether the connect succeeded. match sockopt::get_socket_error(socket.udp_socket()) { Ok(Ok(())) => { - socket.udp_state = UdpState::Connected; + socket.udp_state = UdpState::Connected(addr); Ok(()) } Err(err) | Ok(Err(err)) => Err(err.into()), @@ -188,7 +188,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let mut buf = [0; MAX_UDP_DATAGRAM_SIZE]; match socket.udp_state { UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), - UdpState::Bound | UdpState::Connecting | UdpState::ConnectReady => { + UdpState::Bound | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { for i in 0..max_results { match udp_socket.try_recv_from(&mut buf) { Ok((size, remote_address)) => datagrams.push(udp::Datagram { @@ -202,8 +202,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { } } } - UdpState::Connected => { - let remote_address = udp_socket.peer_addr().map(Into::into)?; + UdpState::Connected(remote_address) => { for i in 0..max_results { match udp_socket.try_recv(&mut buf) { Ok(size) => datagrams.push(udp::Datagram { @@ -236,7 +235,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let mut count = 0; match socket.udp_state { UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), - UdpState::Bound | UdpState::Connecting | UdpState::ConnectReady => { + UdpState::Bound | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { for udp::Datagram { data, remote_address, @@ -251,14 +250,14 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { } } } - UdpState::Connected => { - let peer_addr = udp_socket.peer_addr()?; + UdpState::Connected(addr) => { + let addr = SocketAddr::from(addr); for udp::Datagram { data, remote_address, } in datagrams { - if SocketAddr::from(remote_address) != peer_addr { + if SocketAddr::from(remote_address) != addr { // From WIT documentation: // If at least one datagram has been sent successfully, this function never returns an error. if count == 0 { diff --git a/crates/wasi/src/preview2/udp.rs b/crates/wasi/src/preview2/udp.rs index 53a20572c19c..07eae01c342c 100644 --- a/crates/wasi/src/preview2/udp.rs +++ b/crates/wasi/src/preview2/udp.rs @@ -1,3 +1,4 @@ +use crate::preview2::bindings::sockets::network::IpSocketAddress; use crate::preview2::poll::Subscribe; use crate::preview2::with_ambient_tokio_runtime; use async_trait::async_trait; @@ -23,13 +24,13 @@ pub(crate) enum UdpState { Bound, /// An outgoing connection is started via `start_connect`. - Connecting, + Connecting(IpSocketAddress), /// An outgoing connection is ready to be established. - ConnectReady, + ConnectReady(IpSocketAddress), /// An outgoing connection has been established. - Connected, + Connected(IpSocketAddress), } /// A host UDP socket, plus associated bookkeeping. From 201c8b4bc4a7ffe46a31fdd313098165f190bd5e Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Sat, 7 Oct 2023 00:55:35 +0200 Subject: [PATCH 35/42] fix(wasi-sockets): avoid `shutdown` on `drop` Signed-off-by: Roman Volosatovs --- crates/wasi/src/preview2/host/tcp.rs | 20 -------------------- crates/wasi/src/preview2/host/udp.rs | 19 ------------------- 2 files changed, 39 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 0cf88b9100a4..09ec97009352 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -470,26 +470,6 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { // As in the filesystem implementation, we assume closing a socket // doesn't block. let dropped = table.delete_resource(this)?; - - // If we might have an `event::poll` waiting on the socket, wake it up. - #[cfg(not(unix))] - { - match dropped.tcp_state { - TcpState::Default - | TcpState::BindStarted - | TcpState::Bound - | TcpState::ListenStarted - | TcpState::ConnectReady => {} - - TcpState::Listening | TcpState::Connecting | TcpState::Connected => { - match rustix::net::shutdown(&*dropped.inner, rustix::net::Shutdown::ReadWrite) { - Ok(()) | Err(Errno::NOTCONN) => {} - Err(err) => Err(err).unwrap(), - } - } - } - } - drop(dropped); Ok(()) diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs index acf67a41e678..b6dba8be158e 100644 --- a/crates/wasi/src/preview2/host/udp.rs +++ b/crates/wasi/src/preview2/host/udp.rs @@ -407,25 +407,6 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { // As in the filesystem implementation, we assume closing a socket // doesn't block. let dropped = table.delete_resource(this)?; - - // If we might have an `event::poll` waiting on the socket, wake it up. - #[cfg(not(unix))] - { - match dropped.udp_state { - UdpState::Default - | UdpState::BindStarted - | UdpState::Bound - | UdpState::ConnectReady => {} - - UdpState::Connecting | UdpState::Connected => { - match rustix::net::shutdown(&*dropped.inner, rustix::net::Shutdown::ReadWrite) { - Ok(()) | Err(Errno::NOTCONN) => {} - Err(err) => Err(err).unwrap(), - } - } - } - } - drop(dropped); Ok(()) From da243be7072cc795dc96bae45a54da7b1e045f96 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 12:09:57 +0200 Subject: [PATCH 36/42] Remove intermediate SystemError trait --- crates/wasi/src/preview2/host/network.rs | 74 +++++++++--------------- crates/wasi/src/preview2/host/tcp.rs | 11 ++-- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 9ee7fb4d2fab..bd97df613820 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -23,59 +23,39 @@ impl crate::preview2::bindings::sockets::network::HostNetwork for T } } -/// Unfortunately, Rust's io::ErrorKind is missing more than half of the relevant error codes. -/// This trait provides access to a unified error code. -pub(crate) trait SystemError: std::error::Error { - fn errno(&self) -> Option; -} - -impl SystemError for Errno { - fn errno(&self) -> Option { - Some(*self) - } -} - -impl SystemError for std::io::Error { - fn errno(&self) -> Option { - if let Some(errno) = Errno::from_io_error(self) { - return Some(errno); +impl From for ErrorCode { + fn from(value: io::Error) -> Self { + // Attempt the more detailed native error code first: + if let Some(errno) = Errno::from_io_error(&value) { + return errno.into(); } - // Error is probably synthesized in Rust code. Luckily, the errors kinds map pretty straightforward back to native error codes. - match self.kind() { - std::io::ErrorKind::AddrInUse => Some(Errno::ADDRINUSE), - std::io::ErrorKind::AddrNotAvailable => Some(Errno::ADDRNOTAVAIL), - std::io::ErrorKind::ConnectionAborted => Some(Errno::CONNABORTED), - std::io::ErrorKind::ConnectionRefused => Some(Errno::CONNREFUSED), - std::io::ErrorKind::ConnectionReset => Some(Errno::CONNRESET), - std::io::ErrorKind::Interrupted => Some(Errno::INTR), - std::io::ErrorKind::InvalidInput => Some(Errno::INVAL), - std::io::ErrorKind::NotConnected => Some(Errno::NOTCONN), - #[cfg(windows)] - std::io::ErrorKind::OutOfMemory => Some(Errno::NOBUFS), - #[cfg(not(windows))] - std::io::ErrorKind::OutOfMemory => Some(Errno::NOMEM), - std::io::ErrorKind::PermissionDenied => Some(Errno::ACCESS), // Alternative: EPERM - std::io::ErrorKind::TimedOut => Some(Errno::TIMEDOUT), - std::io::ErrorKind::Unsupported => Some(Errno::OPNOTSUPP), - std::io::ErrorKind::WouldBlock => Some(Errno::WOULDBLOCK), // Alternative: EAGAIN + match value.kind() { + std::io::ErrorKind::AddrInUse => ErrorCode::AddressInUse, + std::io::ErrorKind::AddrNotAvailable => ErrorCode::AddressNotBindable, + std::io::ErrorKind::ConnectionAborted => ErrorCode::ConnectionAborted, + std::io::ErrorKind::ConnectionRefused => ErrorCode::ConnectionRefused, + std::io::ErrorKind::ConnectionReset => ErrorCode::ConnectionReset, + std::io::ErrorKind::Interrupted => ErrorCode::WouldBlock, + std::io::ErrorKind::InvalidInput => ErrorCode::InvalidArgument, + std::io::ErrorKind::NotConnected => ErrorCode::InvalidState, + std::io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory, + std::io::ErrorKind::PermissionDenied => ErrorCode::AccessDenied, + std::io::ErrorKind::TimedOut => ErrorCode::Timeout, + std::io::ErrorKind::Unsupported => ErrorCode::NotSupported, + std::io::ErrorKind::WouldBlock => ErrorCode::WouldBlock, - _ => None, + _ => { + log::debug!("unknown I/O error: {value}"); + ErrorCode::Unknown + } } } } -impl From for ErrorCode { - fn from(error: T) -> Self { - let errno = match error.errno() { - Some(errno) => errno, - None => { - log::debug!("unknown I/O error: {error}"); - return ErrorCode::Unknown; - } - }; - - match errno { +impl From for ErrorCode { + fn from(value: Errno) -> Self { + match value { Errno::WOULDBLOCK => ErrorCode::WouldBlock, #[allow(unreachable_patterns)] // EWOULDBLOCK and EAGAIN can have the same value. Errno::AGAIN => ErrorCode::WouldBlock, @@ -117,7 +97,7 @@ impl From for ErrorCode { // FYI, EINPROGRESS should have already been handled by connect. _ => { - log::debug!("unknown I/O error: {error}"); + log::debug!("unknown I/O error: {value}"); ErrorCode::Unknown } } diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index b82f2cad7b21..c34b91551e83 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -1,4 +1,3 @@ -use super::network::SystemError; use crate::preview2::tcp::{TcpSocket, TcpState}; use crate::preview2::{ bindings::{ @@ -46,7 +45,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { // Perform the OS bind call. binder .bind_existing_tcp_listener(&*socket.tcp_socket().as_socketlike_view::()) - .map_err(|error| match error.errno() { + .map_err(|error| match Errno::from_io_error(&error) { Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient. #[cfg(windows)] Some(Errno::NOBUFS) => ErrorCode::AddressInUse, // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. @@ -119,10 +118,10 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { return Ok(()); } // continue in progress, - Err(err) if err.errno() == Some(INPROGRESS) => {} + Err(err) if Errno::from_io_error(&err) == Some(INPROGRESS) => {} // or fail immediately. Err(err) => { - return Err(match err.errno() { + return Err(match Errno::from_io_error(&err) { Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument.into(), // Just in case our own validations weren't sufficient. _ => err.into(), }); @@ -199,7 +198,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { .tcp_socket() .as_socketlike_view::() .listen(socket.listen_backlog_size) - .map_err(|error| match error.errno() { + .map_err(|error| match Errno::from_io_error(&error) { #[cfg(windows)] Some(Errno::MFILE) => ErrorCode::OutOfMemory, // We're not trying to create a new socket. Rewrite it to less surprising error code. _ => error.into(), @@ -248,7 +247,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { .as_socketlike_view::() .accept_with(Blocking::No) }) - .map_err(|error| match error.errno() { + .map_err(|error| match Errno::from_io_error(&error) { #[cfg(windows)] Some(Errno::INPROGRESS) => ErrorCode::WouldBlock, // "A blocking Windows Sockets 1.1 call is in progress, or the service provider is still processing a callback function." From 67fae1a6dc852098f83c73803f33428cfaa884e4 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 12:48:47 +0200 Subject: [PATCH 37/42] Fix ambiguous .into()'s --- crates/wasi/src/preview2/host/tcp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index c34b91551e83..8c42281b8ca4 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -49,7 +49,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument, // Just in case our own validations weren't sufficient. #[cfg(windows)] Some(Errno::NOBUFS) => ErrorCode::AddressInUse, // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted. - _ => error.into(), + _ => ErrorCode::from(error), })?; let socket = table.get_resource_mut(&this)?; @@ -201,7 +201,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { .map_err(|error| match Errno::from_io_error(&error) { #[cfg(windows)] Some(Errno::MFILE) => ErrorCode::OutOfMemory, // We're not trying to create a new socket. Rewrite it to less surprising error code. - _ => error.into(), + _ => ErrorCode::from(error), })?; socket.tcp_state = TcpState::ListenStarted; @@ -267,7 +267,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { | Errno::OPNOTSUPP, ) => ErrorCode::ConnectionAborted, - _ => error.into(), + _ => ErrorCode::from(error), })?; #[cfg(target_os = "macos")] From d00e12a2fa0f0de4dcd3406d38e2e59fa643637f Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 14:41:06 +0200 Subject: [PATCH 38/42] Fix IPV6_UNICAST_HOPS inheritance on MacOS --- crates/wasi/src/preview2/host/tcp.rs | 20 ++++++++++++++++---- crates/wasi/src/preview2/tcp.rs | 5 +++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 8c42281b8ca4..82e824b3a141 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -272,9 +272,9 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { #[cfg(target_os = "macos")] { - // Manually inherit buffer size from listener. We only have to + // Manually inherit socket options from listener. We only have to // do this on platforms that don't already do this automatically - // and only if a specific buffer size was explicitly set on the listener. + // and only if a specific value was explicitly set on the listener. if let Some(size) = socket.receive_buffer_size { _ = sockopt::set_socket_recv_buffer_size(&connection, size); // Ignore potential error. @@ -283,6 +283,13 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { if let Some(size) = socket.send_buffer_size { _ = sockopt::set_socket_send_buffer_size(&connection, size); // Ignore potential error. } + + // For some reason, IP_TTL is inherited, but IPV6_UNICAST_HOPS isn't. + if let (SocketAddressFamily::Ipv6 { .. }, Some(ttl)) = (socket.family, socket.hop_limit) + { + _ = sockopt::set_ipv6_unicast_hops(&connection, Some(ttl)); + // Ignore potential error. + } } let mut tcp_socket = TcpSocket::from_tcp_stream(connection, socket.family)?; @@ -464,8 +471,8 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource, value: u8, ) -> SocketResult<()> { - let table = self.table(); - let socket = table.get_resource(&this)?; + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; if value == 0 { // A well-behaved IP application should never send out new packets with TTL 0. @@ -481,6 +488,11 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } } + #[cfg(target_os = "macos")] + { + socket.hop_limit = Some(value); + } + Ok(()) } diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 88caf31078ba..dd782c4c66ce 100644 --- a/crates/wasi/src/preview2/tcp.rs +++ b/crates/wasi/src/preview2/tcp.rs @@ -69,6 +69,9 @@ pub struct TcpSocket { /// The manually configured buffer size. `None` means: no preference, use system default. #[cfg(target_os = "macos")] pub(crate) send_buffer_size: Option, + /// The manually configured TTL. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) hop_limit: Option, } #[derive(Copy, Clone)] @@ -301,6 +304,8 @@ impl TcpSocket { receive_buffer_size: None, #[cfg(target_os = "macos")] send_buffer_size: None, + #[cfg(target_os = "macos")] + hop_limit: None, }) } From df958feb446bb200ab31ca4f98b987dec30263b0 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 17:05:13 +0200 Subject: [PATCH 39/42] Remove explicit bind --- crates/wasi/src/preview2/host/udp.rs | 94 +++++++++++----------------- 1 file changed, 36 insertions(+), 58 deletions(-) diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs index b6dba8be158e..80de43485074 100644 --- a/crates/wasi/src/preview2/host/udp.rs +++ b/crates/wasi/src/preview2/host/udp.rs @@ -20,48 +20,6 @@ use wasmtime::component::Resource; /// In practice, datagrams are typically less than 1500 bytes. const MAX_UDP_DATAGRAM_SIZE: usize = 65535; -fn start_bind( - table: &mut Table, - this: Resource, - network: Resource, - local_address: IpSocketAddress, -) -> SocketResult<()> { - let socket = table.get_resource(&this)?; - match socket.udp_state { - UdpState::Default => {} - UdpState::BindStarted | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { - return Err(ErrorCode::ConcurrencyConflict.into()) - } - UdpState::Bound | UdpState::Connected(..) => return Err(ErrorCode::AlreadyBound.into()), - } - - let network = table.get_resource(&network)?; - let binder = network.pool.udp_binder(local_address)?; - - // Perform the OS bind call. - binder.bind_existing_udp_socket( - &*socket - .udp_socket() - .as_socketlike_view::(), - )?; - - let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::BindStarted; - - Ok(()) -} - -fn finish_bind(table: &mut Table, this: Resource) -> SocketResult<()> { - let socket = table.get_resource_mut(&this)?; - match socket.udp_state { - UdpState::BindStarted => { - socket.udp_state = UdpState::Bound; - Ok(()) - } - _ => Err(ErrorCode::NotInProgress.into()), - } -} - impl udp::Host for T {} impl crate::preview2::host::udp::udp::HostUdpSocket for T { @@ -71,11 +29,44 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { network: Resource, local_address: IpSocketAddress, ) -> SocketResult<()> { - start_bind(self.table_mut(), this, network, local_address) + let table = self.table_mut(); + let socket = table.get_resource(&this)?; + + match socket.udp_state { + UdpState::Default => {} + UdpState::BindStarted | UdpState::Connecting(..) => { + return Err(ErrorCode::ConcurrencyConflict.into()) + } + UdpState::Bound | UdpState::Connected(..) => return Err(ErrorCode::AlreadyBound.into()), + } + + let network = table.get_resource(&network)?; + let binder = network.pool.udp_binder(local_address)?; + + // Perform the OS bind call. + binder.bind_existing_udp_socket( + &*socket + .udp_socket() + .as_socketlike_view::(), + )?; + + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::BindStarted; + + Ok(()) } fn finish_bind(&mut self, this: Resource) -> SocketResult<()> { - finish_bind(self.table_mut(), this) + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; + + match socket.udp_state { + UdpState::BindStarted => { + socket.udp_state = UdpState::Bound; + Ok(()) + } + _ => Err(ErrorCode::NotInProgress.into()), + } } fn start_connect( @@ -87,20 +78,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let table = self.table_mut(); let socket = table.get_resource(&this)?; match socket.udp_state { - UdpState::Default => { - let addr = match socket.family { - AddressFamily::Ipv4 => Ipv4Addr::UNSPECIFIED.into(), - AddressFamily::Ipv6 => Ipv6Addr::UNSPECIFIED.into(), - }; - start_bind( - table, - Resource::new_borrow(this.rep()), - Resource::new_borrow(network.rep()), - SocketAddr::new(addr, 0).into(), - )?; - finish_bind(table, Resource::new_borrow(this.rep()))?; - } - UdpState::Bound => {} + UdpState::Default | UdpState::Bound => {} UdpState::BindStarted | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { return Err(ErrorCode::ConcurrencyConflict.into()) } From 412347e7ef9724e1c63420bcbdb754010a06d744 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 17:07:19 +0200 Subject: [PATCH 40/42] Simplify start_connect&finish_connect. On UDP sockets, `connect` never blocks. --- crates/wasi/src/preview2/host/udp.rs | 76 ++++++---------------------- crates/wasi/src/preview2/udp.rs | 7 +-- 2 files changed, 17 insertions(+), 66 deletions(-) diff --git a/crates/wasi/src/preview2/host/udp.rs b/crates/wasi/src/preview2/host/udp.rs index 80de43485074..e9cbd158ab12 100644 --- a/crates/wasi/src/preview2/host/udp.rs +++ b/crates/wasi/src/preview2/host/udp.rs @@ -1,4 +1,4 @@ -use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::net::SocketAddr; use crate::preview2::{ bindings::{ @@ -6,7 +6,6 @@ use crate::preview2::{ sockets::udp, }, udp::UdpState, - Table, }; use crate::preview2::{Pollable, SocketResult, WasiView}; use cap_net_ext::{AddressFamily, PoolExt}; @@ -77,74 +76,39 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { ) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource(&this)?; + let network = table.get_resource(&network)?; + match socket.udp_state { UdpState::Default | UdpState::Bound => {} - UdpState::BindStarted | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { + UdpState::BindStarted | UdpState::Connecting(..) => { return Err(ErrorCode::ConcurrencyConflict.into()) } UdpState::Connected(..) => return Err(ErrorCode::AlreadyConnected.into()), } - let socket = table.get_resource(&this)?; - let network = table.get_resource(&network)?; let connecter = network.pool.udp_connecter(remote_address)?; - // Do an OS `connect`. Our socket is non-blocking, so it'll either... - let res = connecter.connect_existing_udp_socket( + // Do an OS `connect`. + connecter.connect_existing_udp_socket( &*socket .udp_socket() .as_socketlike_view::(), - ); - match res { - // succeed immediately, - Ok(()) => { - let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::ConnectReady(remote_address); - Ok(()) - } - // continue in progress, - Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => { - let socket = table.get_resource_mut(&this)?; - socket.udp_state = UdpState::Connecting(remote_address); - Ok(()) - } - // or fail immediately. - Err(err) => Err(err.into()), - } + )?; + + let socket = table.get_resource_mut(&this)?; + socket.udp_state = UdpState::Connecting(remote_address); + Ok(()) } fn finish_connect(&mut self, this: Resource) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource_mut(&this)?; + match socket.udp_state { - UdpState::ConnectReady(addr) => { + UdpState::Connecting(addr) => { socket.udp_state = UdpState::Connected(addr); Ok(()) } - UdpState::Connecting(addr) => { - // Do a `poll` to test for completion, using a timeout of zero - // to avoid blocking. - match rustix::event::poll( - &mut [rustix::event::PollFd::new( - socket.udp_socket(), - rustix::event::PollFlags::OUT, - )], - 0, - ) { - Ok(0) => return Err(ErrorCode::WouldBlock.into()), - Ok(_) => {} - Err(err) => return Err(err.into()), - } - - // Check whether the connect succeeded. - match sockopt::get_socket_error(socket.udp_socket()) { - Ok(Ok(())) => { - socket.udp_state = UdpState::Connected(addr); - Ok(()) - } - Err(err) | Ok(Err(err)) => Err(err.into()), - } - } _ => Err(ErrorCode::NotInProgress.into()), } } @@ -166,7 +130,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let mut buf = [0; MAX_UDP_DATAGRAM_SIZE]; match socket.udp_state { UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), - UdpState::Bound | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { + UdpState::Bound | UdpState::Connecting(..) => { for i in 0..max_results { match udp_socket.try_recv_from(&mut buf) { Ok((size, remote_address)) => datagrams.push(udp::Datagram { @@ -213,7 +177,7 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { let mut count = 0; match socket.udp_state { UdpState::Default | UdpState::BindStarted => return Err(ErrorCode::NotBound.into()), - UdpState::Bound | UdpState::Connecting(..) | UdpState::ConnectReady(..) => { + UdpState::Bound | UdpState::Connecting(..) => { for udp::Datagram { data, remote_address, @@ -390,13 +354,3 @@ impl crate::preview2::host::udp::udp::HostUdpSocket for T { Ok(()) } } - -const INPROGRESS: Errno = if cfg!(windows) { - // On Windows, non-blocking UDP socket `connect` uses `WSAEWOULDBLOCK`. - // - Errno::WOULDBLOCK -} else { - // On POSIX, non-blocking UDP socket `connect` uses `EINPROGRESS`. - // - Errno::INPROGRESS -}; diff --git a/crates/wasi/src/preview2/udp.rs b/crates/wasi/src/preview2/udp.rs index 07eae01c342c..2c879f99a6c9 100644 --- a/crates/wasi/src/preview2/udp.rs +++ b/crates/wasi/src/preview2/udp.rs @@ -23,13 +23,10 @@ pub(crate) enum UdpState { /// is not yet listening for connections. Bound, - /// An outgoing connection is started via `start_connect`. + /// A connect call is in progress. Connecting(IpSocketAddress), - /// An outgoing connection is ready to be established. - ConnectReady(IpSocketAddress), - - /// An outgoing connection has been established. + /// The socket is "connected" to a peer address. Connected(IpSocketAddress), } From 717a5dd35241424e00d13a95d05f8d53d0bf17a6 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 20:13:19 +0200 Subject: [PATCH 41/42] Move UDP test to single file, similar to `tcp_sample_application.rs` --- crates/test-programs/tests/wasi-sockets.rs | 9 +- .../src/bin/udp_sample_application.rs | 125 ++++++++++++++++++ .../wasi-sockets-tests/src/bin/udp_v4.rs | 28 ---- .../wasi-sockets-tests/src/bin/udp_v6.rs | 30 ----- .../wasi-sockets-tests/src/lib.rs | 89 ------------- 5 files changed, 127 insertions(+), 154 deletions(-) create mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs delete mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs delete mode 100644 crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index 6588bdc730a9..806d21c6c5db 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -92,13 +92,8 @@ async fn tcp_sockopts() { } #[test_log::test(tokio::test(flavor = "multi_thread"))] -async fn udp_v4() { - run("udp_v4").await.unwrap(); -} - -#[test_log::test(tokio::test(flavor = "multi_thread"))] -async fn udp_v6() { - run("udp_v6").await.unwrap(); +async fn udp_sample_application() { + run("udp_sample_application").await.unwrap(); } #[test_log::test(tokio::test(flavor = "multi_thread"))] diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs new file mode 100644 index 000000000000..9322683197f6 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs @@ -0,0 +1,125 @@ +use wasi::io::poll; +use wasi::sockets::network::{ + IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, +}; +use wasi::sockets::{instance_network, udp, udp_create_socket}; +use wasi_sockets_tests::*; + +fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) { + let first_message = b"Hello, world!"; + let second_message = b"Greetings, planet!"; + + let net = instance_network::instance_network(); + + let sock = udp_create_socket::create_udp_socket(family).unwrap(); + + let sub = sock.subscribe(); + + sock.start_bind(&net, bind_address).unwrap(); + + poll::poll_one(&sub); + drop(sub); + + sock.finish_bind().unwrap(); + + let sub = sock.subscribe(); + + let addr = sock.local_address().unwrap(); + + let client = udp_create_socket::create_udp_socket(family).unwrap(); + let client_sub = client.subscribe(); + + client.start_connect(&net, addr).unwrap(); + poll::poll_one(&client_sub); + client.finish_connect().unwrap(); + + let _client_addr = client.local_address().unwrap(); + + let n = client + .send(&[ + udp::Datagram { + data: vec![], + remote_address: addr, + }, + udp::Datagram { + data: first_message.to_vec(), + remote_address: addr, + }, + ]) + .unwrap(); + assert_eq!(n, 2); + + drop(client_sub); + drop(client); + + poll::poll_one(&sub); + let datagrams = sock.receive(2).unwrap(); + let mut datagrams = datagrams.into_iter(); + let (first, second) = match (datagrams.next(), datagrams.next(), datagrams.next()) { + (Some(first), Some(second), None) => (first, second), + (Some(_first), None, None) => panic!("only one datagram received"), + (None, None, None) => panic!("no datagrams received"), + _ => panic!("invalid datagram sequence received"), + }; + + assert!(first.data.is_empty()); + + // TODO: Verify the `remote_address` + //assert_eq!(first.remote_address, client_addr); + + // Check that we sent and recieved our message! + assert_eq!(second.data, first_message); // Not guaranteed to work but should work in practice. + + // TODO: Verify the `remote_address` + //assert_eq!(second.remote_address, client_addr); + + // Another client + let client = udp_create_socket::create_udp_socket(family).unwrap(); + let client_sub = client.subscribe(); + + client.start_connect(&net, addr).unwrap(); + poll::poll_one(&client_sub); + client.finish_connect().unwrap(); + + let n = client + .send(&[udp::Datagram { + data: second_message.to_vec(), + remote_address: addr, + }]) + .unwrap(); + assert_eq!(n, 1); + + drop(client_sub); + drop(client); + + poll::poll_one(&sub); + let datagrams = sock.receive(2).unwrap(); + let mut datagrams = datagrams.into_iter(); + let first = match (datagrams.next(), datagrams.next()) { + (Some(first), None) => first, + (None, None) => panic!("no datagrams received"), + _ => panic!("invalid datagram sequence received"), + }; + + // Check that we sent and recieved our message! + assert_eq!(first.data, second_message); // Not guaranteed to work but should work in practice. +} + +fn main() { + test_sample_application( + IpAddressFamily::Ipv4, + IpSocketAddress::Ipv4(Ipv4SocketAddress { + port: 0, // use any free port + address: (127, 0, 0, 1), // localhost + }), + ); + test_sample_application( + IpAddressFamily::Ipv6, + IpSocketAddress::Ipv6(Ipv6SocketAddress { + port: 0, // use any free port + address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost + flow_info: 0, + scope_id: 0, + }), + ); +} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs deleted file mode 100644 index 86d606cad96f..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v4.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! A simple UDP testcase, using IPv4. - -use wasi::io::poll; -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress}; -use wasi::sockets::{instance_network, udp_create_socket}; -use wasi_sockets_tests::*; - -fn main() { - let net = instance_network::instance_network(); - - let sock = udp_create_socket::create_udp_socket(IpAddressFamily::Ipv4).unwrap(); - - let addr = IpSocketAddress::Ipv4(Ipv4SocketAddress { - port: 0, // use any free port - address: (127, 0, 0, 1), // localhost - }); - - let sub = sock.subscribe(); - - sock.start_bind(&net, addr).unwrap(); - - poll::poll_one(&sub); - drop(sub); - - sock.finish_bind().unwrap(); - - example_body_udp(net, sock, IpAddressFamily::Ipv4) -} diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs deleted file mode 100644 index 58d455997283..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/udp_v6.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Like udp_v4.rs, but with IPv6. - -use wasi::io::poll; -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; -use wasi::sockets::{instance_network, udp_create_socket}; -use wasi_sockets_tests::*; - -fn main() { - let net = instance_network::instance_network(); - - let sock = udp_create_socket::create_udp_socket(IpAddressFamily::Ipv6).unwrap(); - - let addr = IpSocketAddress::Ipv6(Ipv6SocketAddress { - port: 0, // use any free port - address: (0, 0, 0, 0, 0, 0, 0, 1), // localhost - flow_info: 0, - scope_id: 0, - }); - - let sub = sock.subscribe(); - - sock.start_bind(&net, addr).unwrap(); - - poll::poll_one(&sub); - drop(sub); - - sock.finish_bind().unwrap(); - - example_body_udp(net, sock, IpAddressFamily::Ipv6) -} diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index 337c42fa52dc..4d90e914ddda 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -7,7 +7,6 @@ use wasi::sockets::network::{ ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, Network, }; -use wasi::sockets::{network, udp, udp_create_socket}; use wasi::sockets::tcp::TcpSocket; use wasi::sockets::tcp_create_socket; @@ -190,91 +189,3 @@ impl IpSocketAddress { } } } - - -pub fn example_body_udp(net: udp::Network, sock: udp::UdpSocket, family: network::IpAddressFamily) { - let first_message = b"Hello, world!"; - let second_message = b"Greetings, planet!"; - - let sub = sock.subscribe(); - - let addr = sock.local_address().unwrap(); - - let client = udp_create_socket::create_udp_socket(family).unwrap(); - let client_sub = client.subscribe(); - - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - client.finish_connect().unwrap(); - - let _client_addr = client.local_address().unwrap(); - - let n = client - .send(&[ - udp::Datagram { - data: vec![], - remote_address: addr, - }, - udp::Datagram { - data: first_message.to_vec(), - remote_address: addr, - }, - ]) - .unwrap(); - assert_eq!(n, 2); - - drop(client_sub); - drop(client); - - poll::poll_one(&sub); - let datagrams = sock.receive(2).unwrap(); - let mut datagrams = datagrams.into_iter(); - let (first, second) = match (datagrams.next(), datagrams.next(), datagrams.next()) { - (Some(first), Some(second), None) => (first, second), - (Some(_first), None, None) => panic!("only one datagram received"), - (None, None, None) => panic!("no datagrams received"), - _ => panic!("invalid datagram sequence received"), - }; - - assert!(first.data.is_empty()); - - // TODO: Verify the `remote_address` - //assert_eq!(first.remote_address, client_addr); - - // Check that we sent and recieved our message! - assert_eq!(second.data, first_message); // Not guaranteed to work but should work in practice. - - // TODO: Verify the `remote_address` - //assert_eq!(second.remote_address, client_addr); - - // Another client - let client = udp_create_socket::create_udp_socket(family).unwrap(); - let client_sub = client.subscribe(); - - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - client.finish_connect().unwrap(); - - let n = client - .send(&[udp::Datagram { - data: second_message.to_vec(), - remote_address: addr, - }]) - .unwrap(); - assert_eq!(n, 1); - - drop(client_sub); - drop(client); - - poll::poll_one(&sub); - let datagrams = sock.receive(2).unwrap(); - let mut datagrams = datagrams.into_iter(); - let first = match (datagrams.next(), datagrams.next()) { - (Some(first), None) => first, - (None, None) => panic!("no datagrams received"), - _ => panic!("invalid datagram sequence received"), - }; - - // Check that we sent and recieved our message! - assert_eq!(first.data, second_message); // Not guaranteed to work but should work in practice. -} From af648c1fb09858b85a876b7069fb7035f8ad22f7 Mon Sep 17 00:00:00 2001 From: Dave Bakker Date: Sun, 8 Oct 2023 22:07:41 +0200 Subject: [PATCH 42/42] Update UDP tests --- .../src/bin/udp_sample_application.rs | 124 +++++++----------- .../wasi-sockets-tests/src/lib.rs | 124 ++++++++++++++++++ 2 files changed, 168 insertions(+), 80 deletions(-) diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs b/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs index 9322683197f6..7dc64fa586a6 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/udp_sample_application.rs @@ -1,108 +1,72 @@ -use wasi::io::poll; use wasi::sockets::network::{ - IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, + IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, Network, }; -use wasi::sockets::{instance_network, udp, udp_create_socket}; +use wasi::sockets::udp::{Datagram, UdpSocket}; use wasi_sockets_tests::*; fn test_sample_application(family: IpAddressFamily, bind_address: IpSocketAddress) { - let first_message = b"Hello, world!"; - let second_message = b"Greetings, planet!"; + let first_message = &[]; + let second_message = b"Hello, world!"; + let third_message = b"Greetings, planet!"; - let net = instance_network::instance_network(); + let net = Network::default(); - let sock = udp_create_socket::create_udp_socket(family).unwrap(); + let server = UdpSocket::new(family).unwrap(); - let sub = sock.subscribe(); + server.blocking_bind(&net, bind_address).unwrap(); + let addr = server.local_address().unwrap(); - sock.start_bind(&net, bind_address).unwrap(); + let client_addr = { + let client = UdpSocket::new(family).unwrap(); + client.blocking_connect(&net, addr).unwrap(); - poll::poll_one(&sub); - drop(sub); - - sock.finish_bind().unwrap(); - - let sub = sock.subscribe(); - - let addr = sock.local_address().unwrap(); - - let client = udp_create_socket::create_udp_socket(family).unwrap(); - let client_sub = client.subscribe(); - - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - client.finish_connect().unwrap(); - - let _client_addr = client.local_address().unwrap(); - - let n = client - .send(&[ - udp::Datagram { - data: vec![], + let datagrams = [ + Datagram { + data: first_message.to_vec(), remote_address: addr, }, - udp::Datagram { - data: first_message.to_vec(), + Datagram { + data: second_message.to_vec(), remote_address: addr, }, - ]) - .unwrap(); - assert_eq!(n, 2); + ]; + client.blocking_send(&datagrams).unwrap(); - drop(client_sub); - drop(client); - - poll::poll_one(&sub); - let datagrams = sock.receive(2).unwrap(); - let mut datagrams = datagrams.into_iter(); - let (first, second) = match (datagrams.next(), datagrams.next(), datagrams.next()) { - (Some(first), Some(second), None) => (first, second), - (Some(_first), None, None) => panic!("only one datagram received"), - (None, None, None) => panic!("no datagrams received"), - _ => panic!("invalid datagram sequence received"), + client.local_address().unwrap() }; - assert!(first.data.is_empty()); - - // TODO: Verify the `remote_address` - //assert_eq!(first.remote_address, client_addr); + { + // Check that we've received our sent messages. + // Not guaranteed to work but should work in practice. + let datagrams = server.blocking_receive(2..100).unwrap(); + assert_eq!(datagrams.len(), 2); - // Check that we sent and recieved our message! - assert_eq!(second.data, first_message); // Not guaranteed to work but should work in practice. + assert_eq!(datagrams[0].data, first_message); + assert_eq!(datagrams[0].remote_address, client_addr); - // TODO: Verify the `remote_address` - //assert_eq!(second.remote_address, client_addr); + assert_eq!(datagrams[1].data, second_message); + assert_eq!(datagrams[1].remote_address, client_addr); + } // Another client - let client = udp_create_socket::create_udp_socket(family).unwrap(); - let client_sub = client.subscribe(); + { + let client = UdpSocket::new(family).unwrap(); + client.blocking_connect(&net, addr).unwrap(); - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - client.finish_connect().unwrap(); - - let n = client - .send(&[udp::Datagram { - data: second_message.to_vec(), + let datagrams = [Datagram { + data: third_message.to_vec(), remote_address: addr, - }]) - .unwrap(); - assert_eq!(n, 1); - - drop(client_sub); - drop(client); + }]; + client.blocking_send(&datagrams).unwrap(); + } - poll::poll_one(&sub); - let datagrams = sock.receive(2).unwrap(); - let mut datagrams = datagrams.into_iter(); - let first = match (datagrams.next(), datagrams.next()) { - (Some(first), None) => first, - (None, None) => panic!("no datagrams received"), - _ => panic!("invalid datagram sequence received"), - }; + { + // Check that we sent and received our message! + let datagrams = server.blocking_receive(1..100).unwrap(); + assert_eq!(datagrams.len(), 1); - // Check that we sent and recieved our message! - assert_eq!(first.data, second_message); // Not guaranteed to work but should work in practice. + assert_eq!(datagrams[0].data, third_message); // Not guaranteed to work but should work in practice. + } } fn main() { diff --git a/crates/test-programs/wasi-sockets-tests/src/lib.rs b/crates/test-programs/wasi-sockets-tests/src/lib.rs index 4d90e914ddda..7dc85340a6ea 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -1,5 +1,7 @@ wit_bindgen::generate!("test-command-with-sockets" in "../../wasi/wit"); +use std::ops::Range; +use wasi::clocks::monotonic_clock; use wasi::io::poll::{self, Pollable}; use wasi::io::streams::{InputStream, OutputStream, StreamError}; use wasi::sockets::instance_network; @@ -9,11 +11,25 @@ use wasi::sockets::network::{ }; use wasi::sockets::tcp::TcpSocket; use wasi::sockets::tcp_create_socket; +use wasi::sockets::udp::{Datagram, UdpSocket}; +use wasi::sockets::udp_create_socket; + +const TIMEOUT_NS: u64 = 1_000_000_000; impl Pollable { pub fn wait(&self) { poll::poll_one(self); } + + pub fn wait_until(&self, timeout: &Pollable) -> Result<(), ErrorCode> { + let ready = poll::poll_list(&[self, timeout]); + assert!(ready.len() > 0); + match ready[0] { + 0 => Ok(()), + 1 => Err(ErrorCode::Timeout), + _ => unreachable!(), + } + } } impl OutputStream { @@ -108,6 +124,89 @@ impl TcpSocket { } } +impl UdpSocket { + pub fn new(address_family: IpAddressFamily) -> Result { + udp_create_socket::create_udp_socket(address_family) + } + + pub fn blocking_bind( + &self, + network: &Network, + local_address: IpSocketAddress, + ) -> Result<(), ErrorCode> { + let sub = self.subscribe(); + + self.start_bind(&network, local_address)?; + + loop { + match self.finish_bind() { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } + + pub fn blocking_connect( + &self, + network: &Network, + remote_address: IpSocketAddress, + ) -> Result<(), ErrorCode> { + let sub = self.subscribe(); + + self.start_connect(&network, remote_address)?; + + loop { + match self.finish_connect() { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } + + pub fn blocking_send(&self, mut datagrams: &[Datagram]) -> Result<(), ErrorCode> { + let timeout = monotonic_clock::subscribe(TIMEOUT_NS, false); + let pollable = self.subscribe(); + + while !datagrams.is_empty() { + match self.send(datagrams) { + Ok(packets_sent) => { + datagrams = &datagrams[(packets_sent as usize)..]; + } + Err(ErrorCode::WouldBlock) => pollable.wait_until(&timeout)?, + Err(err) => return Err(err), + } + } + + Ok(()) + } + + pub fn blocking_receive(&self, count: Range) -> Result, ErrorCode> { + let timeout = monotonic_clock::subscribe(TIMEOUT_NS, false); + let pollable = self.subscribe(); + let mut datagrams = vec![]; + + loop { + match self.receive(count.end - datagrams.len() as u64) { + Ok(mut chunk) => { + datagrams.append(&mut chunk); + + if datagrams.len() >= count.start as usize { + return Ok(datagrams); + } + } + Err(ErrorCode::WouldBlock) => { + if datagrams.len() >= count.start as usize { + return Ok(datagrams); + } else { + pollable.wait_until(&timeout)?; + } + } + Err(err) => return Err(err), + } + } + } +} + impl IpAddress { pub const IPV4_BROADCAST: IpAddress = IpAddress::Ipv4((255, 255, 255, 255)); @@ -189,3 +288,28 @@ impl IpSocketAddress { } } } + +impl PartialEq for Ipv4SocketAddress { + fn eq(&self, other: &Self) -> bool { + self.port == other.port && self.address == other.address + } +} + +impl PartialEq for Ipv6SocketAddress { + fn eq(&self, other: &Self) -> bool { + self.port == other.port + && self.flow_info == other.flow_info + && self.address == other.address + && self.scope_id == other.scope_id + } +} + +impl PartialEq for IpSocketAddress { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Ipv4(l0), Self::Ipv4(r0)) => l0 == r0, + (Self::Ipv6(l0), Self::Ipv6(r0)) => l0 == r0, + _ => false, + } + } +}