diff --git a/crates/test-programs/src/bin/preview2_tcp_bind.rs b/crates/test-programs/src/bin/preview2_tcp_bind.rs index faea346a155f..8523dbe475df 100644 --- a/crates/test-programs/src/bin/preview2_tcp_bind.rs +++ b/crates/test-programs/src/bin/preview2_tcp_bind.rs @@ -1,3 +1,4 @@ +use test_programs::sockets::attempt_random_port; use test_programs::wasi::sockets::network::{ ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network, }; @@ -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. @@ -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); @@ -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); diff --git a/crates/test-programs/src/bin/preview2_udp_bind.rs b/crates/test-programs/src/bin/preview2_udp_bind.rs index 4c5ccc91c48f..f320a043026b 100644 --- a/crates/test-programs/src/bin/preview2_udp_bind.rs +++ b/crates/test-programs/src/bin/preview2_udp_bind.rs @@ -1,3 +1,4 @@ +use test_programs::sockets::attempt_random_port; use test_programs::wasi::sockets::network::{ ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network, }; @@ -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(); diff --git a/crates/test-programs/src/sockets.rs b/crates/test-programs/src/sockets.rs index e4dc2d58b78f..c77ad8d7f819 100644 --- a/crates/test-programs/src/sockets.rs +++ b/crates/test-programs/src/sockets.rs @@ -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::{ @@ -357,3 +358,37 @@ impl PartialEq for IpSocketAddress { } } } + +fn generate_random_u16(range: Range) -> 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( + local_address: IpAddress, + mut f: F, +) -> Result +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), + } + } +} diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit index b01b65e6c4b5..976b272c00a8 100644 --- a/crates/wasi-http/wit/deps/sockets/tcp.wit +++ b/crates/wasi-http/wit/deps/sockets/tcp.wit @@ -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 /// - diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index 55dedb1b145d..b698eb5d57df 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -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(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(sockfd: Fd, value: Duration) -> rustix::io::Result<()> { if value <= Duration::ZERO { // WIT: "If the provided value is 0, an `invalid-argument` error is returned." diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index b1d4ba31f28c..928bd552219a 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -49,6 +49,15 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { network.check_socket_addr(&local_address, SocketAddrUse::TcpBind)?; let listener = &*socket.tcp_socket().as_socketlike_view::(); + // 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) { diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index b01b65e6c4b5..976b272c00a8 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/wit/deps/sockets/tcp.wit @@ -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 /// -