Skip to content

Commit

Permalink
wasi-sockets: Add SO_REUSEADDR back in (#7690)
Browse files Browse the repository at this point in the history
* Restore SO_REUSEADDR.

This inadvertently removed in 8ca8056 when switching from `bind_existing_tcp_listener` to `rustix::net::bind`

* Remove AddressInUse workarounds by generating a random port.

* fmt

* Ignore unused_variables warning

* Prevent spurious test failures by trying again a few times on EADDRINUSE

* Fix grammar in .wit documentation
  • Loading branch information
badeend authored Dec 17, 2023
1 parent e4ce5a5 commit e6a9fa1
Show file tree
Hide file tree
Showing 7 changed files with 136 additions and 27 deletions.
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 @@ -446,6 +446,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 @@ -47,6 +47,15 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
// Ensure that we're allowed to connect to this address.
network.check_socket_addr(&local_address, SocketAddrUse::TcpBind)?;

// 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(socket.tcp_socket(), &local_address).map_err(|error| match error {
// From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html:
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

0 comments on commit e6a9fa1

Please sign in to comment.