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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 50 additions & 15 deletions crates/test-programs/src/bin/preview2_tcp_bind.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use test_programs::sockets::attempt_random_port;
use test_programs::wasi::sockets::network::{
ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network,
};
Expand All @@ -18,23 +19,15 @@ fn test_tcp_bind_ephemeral_port(net: &Network, ip: IpAddress) {

/// Bind a socket on a specified port.
fn test_tcp_bind_specific_port(net: &Network, ip: IpAddress) {
const PORT: u16 = 54321;
let sock = TcpSocket::new(ip.family()).unwrap();

let bind_addr = IpSocketAddress::new(ip, PORT);
let bind_addr =
attempt_random_port(ip, |bind_addr| sock.blocking_bind(net, bind_addr)).unwrap();

let sock = TcpSocket::new(ip.family()).unwrap();
match sock.blocking_bind(net, bind_addr) {
Ok(()) => {
let bound_addr = sock.local_address().unwrap();

assert_eq!(bind_addr.ip(), bound_addr.ip());
assert_eq!(bind_addr.port(), bound_addr.port());
}
// Concurrent invocations of this test can yield `AddressInUse` and that
// same error can show up on Windows as `AccessDenied`.
Err(ErrorCode::AddressInUse | ErrorCode::AccessDenied) => {}
Err(e) => panic!("error: {e}"),
}
let bound_addr = sock.local_address().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.
Expand All @@ -54,6 +47,45 @@ fn test_tcp_bind_addrinuse(net: &Network, ip: IpAddress) {
);
}

// The WASI runtime should set SO_REUSEADDR for us
fn test_tcp_bind_reuseaddr(net: &Network, ip: IpAddress) {
let client = TcpSocket::new(ip.family()).unwrap();

let bind_addr = {
let listener1 = TcpSocket::new(ip.family()).unwrap();

let bind_addr =
attempt_random_port(ip, |bind_addr| listener1.blocking_bind(net, bind_addr)).unwrap();

listener1.blocking_listen().unwrap();

let connect_addr =
IpSocketAddress::new(IpAddress::new_loopback(ip.family()), bind_addr.port());
client.blocking_connect(net, connect_addr).unwrap();

let (accepted_connection, accepted_input, accepted_output) =
listener1.blocking_accept().unwrap();
accepted_output.blocking_write_zeroes_and_flush(10).unwrap();
drop(accepted_input);
drop(accepted_output);
drop(accepted_connection);
drop(listener1);

bind_addr
};

{
let listener2 = TcpSocket::new(ip.family()).unwrap();

// If SO_REUSEADDR was configured correctly, the following lines shouldn't be
// affected by the TIME_WAIT state of the just closed `listener1` socket:
listener2.blocking_bind(net, bind_addr).unwrap();
listener2.blocking_listen().unwrap();
}

drop(client);
}

// Try binding to an address that is not configured on the system.
fn test_tcp_bind_addrnotavail(net: &Network, ip: IpAddress) {
let bind_addr = IpSocketAddress::new(ip, 0);
Expand Down Expand Up @@ -141,6 +173,9 @@ fn main() {
test_tcp_bind_specific_port(&net, IpAddress::IPV4_UNSPECIFIED);
test_tcp_bind_specific_port(&net, IpAddress::IPV6_UNSPECIFIED);

test_tcp_bind_reuseaddr(&net, IpAddress::IPV4_LOOPBACK);
test_tcp_bind_reuseaddr(&net, IpAddress::IPV6_LOOPBACK);

test_tcp_bind_addrinuse(&net, IpAddress::IPV4_LOOPBACK);
test_tcp_bind_addrinuse(&net, IpAddress::IPV6_LOOPBACK);
test_tcp_bind_addrinuse(&net, IpAddress::IPV4_UNSPECIFIED);
Expand Down
16 changes: 4 additions & 12 deletions crates/test-programs/src/bin/preview2_udp_bind.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use test_programs::sockets::attempt_random_port;
use test_programs::wasi::sockets::network::{
ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network,
};
Expand All @@ -18,19 +19,10 @@ fn test_udp_bind_ephemeral_port(net: &Network, ip: IpAddress) {

/// Bind a socket on a specified port.
fn test_udp_bind_specific_port(net: &Network, ip: IpAddress) {
const PORT: u16 = 54321;

let bind_addr = IpSocketAddress::new(ip, PORT);

let sock = UdpSocket::new(ip.family()).unwrap();
match sock.blocking_bind(net, bind_addr) {
Ok(()) => {}

// Concurrent invocations of this test can yield `AddressInUse` and that
// same error can show up on Windows as `AccessDenied`.
Err(ErrorCode::AddressInUse | ErrorCode::AccessDenied) => return,
r => r.unwrap(),
}

let bind_addr =
attempt_random_port(ip, |bind_addr| sock.blocking_bind(net, bind_addr)).unwrap();

let bound_addr = sock.local_address().unwrap();

Expand Down
35 changes: 35 additions & 0 deletions crates/test-programs/src/sockets.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::wasi::clocks::monotonic_clock;
use crate::wasi::io::poll::{self, Pollable};
use crate::wasi::io::streams::{InputStream, OutputStream, StreamError};
use crate::wasi::random;
use crate::wasi::sockets::instance_network;
use crate::wasi::sockets::ip_name_lookup;
use crate::wasi::sockets::network::{
Expand Down Expand Up @@ -357,3 +358,37 @@ impl PartialEq for IpSocketAddress {
}
}
}

fn generate_random_u16(range: Range<u16>) -> u16 {
let start = range.start as u64;
let end = range.end as u64;
let port = start + (random::random::get_random_u64() % (end - start));
port as u16
}

/// Execute the inner function with a randomly generated port.
/// To prevent random failures, we make a few attempts before giving up.
pub fn attempt_random_port<F>(
local_address: IpAddress,
mut f: F,
) -> Result<IpSocketAddress, ErrorCode>
where
F: FnMut(IpSocketAddress) -> Result<(), ErrorCode>,
{
const MAX_ATTEMPTS: u32 = 10;
let mut i = 0;
loop {
i += 1;

let port: u16 = generate_random_u16(1024..u16::MAX);
let sock_addr = IpSocketAddress::new(local_address, port);

match f(sock_addr) {
Ok(_) => return Ok(sock_addr),
Err(e) if i >= MAX_ATTEMPTS => return Err(e),
// Try again if the port is already taken. This can sometimes show up as `AccessDenied` on Windows.
Err(ErrorCode::AddressInUse | ErrorCode::AccessDenied) => {}
Err(e) => return Err(e),
}
}
}
5 changes: 5 additions & 0 deletions crates/wasi-http/wit/deps/sockets/tcp.wit
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ interface tcp {
/// - `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.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT
/// state of a recently closed socket on the same local address (i.e. the SO_REUSEADDR socket
/// option should be set implicitly on platforms that require it).
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
Expand Down
28 changes: 28 additions & 0 deletions crates/wasi/src/preview2/host/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ pub(crate) mod util {
}
}

// Even though SO_REUSEADDR is a SOL_* level option, this function contain a
// compatibility fix specific to TCP. That's why it contains the `_tcp_` infix instead of `_socket_`.
#[allow(unused_variables)] // Parameters are not used on Windows
pub fn set_tcp_reuseaddr<Fd: AsFd>(sockfd: Fd, value: bool) -> rustix::io::Result<()> {
// When a TCP socket is closed, the system may
// temporarily reserve that specific address+port pair in a so called
// TIME_WAIT state. During that period, any attempt to rebind to that pair
// will fail. Setting SO_REUSEADDR to true bypasses that behaviour. Unlike
// the name "SO_REUSEADDR" might suggest, it does not allow multiple
// active sockets to share the same local address.

// On Windows that behavior is the default, so there is no need to manually
// configure such an option. But (!), Windows _does_ have an identically
// named socket option which allows users to "hijack" active sockets.
// This is definitely not what we want to do here.

// Microsoft's own documentation[1] states that we should set SO_EXCLUSIVEADDRUSE
// instead (to the inverse value), however the github issue below[2] seems
// to indicate that that may no longer be correct.
// [1]: https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
// [2]: https://github.com/python-trio/trio/issues/928

#[cfg(not(windows))]
sockopt::set_socket_reuseaddr(sockfd, value)?;

Ok(())
}

pub fn set_tcp_keepidle<Fd: AsFd>(sockfd: Fd, value: Duration) -> rustix::io::Result<()> {
if value <= Duration::ZERO {
// WIT: "If the provided value is 0, an `invalid-argument` error is returned."
Expand Down
9 changes: 9 additions & 0 deletions crates/wasi/src/preview2/host/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
network.check_socket_addr(&local_address, SocketAddrUse::TcpBind)?;
let listener = &*socket.tcp_socket().as_socketlike_view::<TcpListener>();

// Automatically bypass the TIME_WAIT state when the user is trying
// to bind to a specific port:
let reuse_addr = local_address.port() > 0;

// Unconditionally (re)set SO_REUSEADDR, even when the value is false.
// This ensures we're not accidentally affected by any socket option
// state left behind by a previous failed call to this method (start_bind).
util::set_tcp_reuseaddr(socket.tcp_socket(), reuse_addr)?;

// Perform the OS bind call.
util::tcp_bind(listener, &local_address).map_err(|error| {
match Errno::from_io_error(&error) {
Expand Down
5 changes: 5 additions & 0 deletions crates/wasi/wit/deps/sockets/tcp.wit
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ interface tcp {
/// - `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.
/// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN)
///
/// # Implementors note
/// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT
/// state of a recently closed socket on the same local address (i.e. the SO_REUSEADDR socket
/// option should be set implicitly on platforms that require it).
///
/// # References
/// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html>
Expand Down