Skip to content

Commit

Permalink
wasi-sockets: Simplify & clarify TCP errors (#7120)
Browse files Browse the repository at this point in the history
* Use Rustix::Errno to unify error code mapping.

* Clarify Connect failure state

* 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: WebAssembly/wasi-sockets#22

* Fold `ephemeral-ports-exhausted` into `address-in-use`

There is no cross-platform way to know the distinction between them

* Clarify `local-address` behavior on unbound socket

* Remove `concurrency-conflict` clutter,
and just document it to be always possible.

* 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.

* Document that connect may return ECONNABORTED

* 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.

* 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.

* Move example_body out of lib.rs into its own test-case make room for other tests.

* Refactor TCP integration tests:
- Ad-hoc skeleton implementation of resources.
- Add blocking wrappers around async operations.

* Fix get/set_unicast_hop_limit on Linux

* Test TCP socket states

* Keep track of address family ourselves.

Because I need the family for input validation.
And the resulting code is more straightforward.

* Add more tests and make it work on Linux

* Fix Windows

* Simplify integration tests.

All platforms supported by wasmtime also support dualstack sockets.

* Test ipv6_only inheritence

* Test that socket options keep being respected, even if listen() has already been called

* cargo fmt

* Duplicate .wit changes to wasi-http

* prtest:full

* Fix BSD behavior of SO_SNDBUF/SO_RCVBUF

* fmt

* Fix type error

* Got lost during merge

* Implement listen backlog tests

* Manually inherit buffer size from listener on MacOS.

I suspect that these changes apply to any BSD platform, but I can't test that.

* Keep track of IPV6_V6ONLY ourselves.

- This provides cross-platform behaviour for `ipv6-only`
- This eliminates the syscall in `validate_address_family`

* Reject IPv4-compatible IPv6 addresses.

* Remove intermediate SystemError trait

* Fix ambiguous .into()'s

* Fix IPV6_UNICAST_HOPS inheritance on MacOS
  • Loading branch information
badeend authored Oct 9, 2023
1 parent 9fc4a71 commit 89449b6
Show file tree
Hide file tree
Showing 26 changed files with 1,654 additions and 532 deletions.
23 changes: 19 additions & 4 deletions crates/test-programs/tests/wasi-sockets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,28 @@ async fn run(name: &str) -> anyhow::Result<()> {
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn tcp_v4() {
run("tcp_v4").await.unwrap();
async fn tcp_sample_application() {
run("tcp_sample_application").await.unwrap();
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn tcp_v6() {
run("tcp_v6").await.unwrap();
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();
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ fn main() {
assert!(addresses.resolve_next_address().is_ok());

let result = ip_name_lookup::resolve_addresses(&network, "a.b<&>", None, false);
assert!(matches!(result, Err(network::ErrorCode::InvalidName)));
assert!(matches!(result, Err(network::ErrorCode::InvalidArgument)));

// Try resolving a valid address and ensure that it eventually terminates.
// To help prevent this test from being flaky this additionally times out
Expand Down
151 changes: 151 additions & 0 deletions crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network};
use wasi::sockets::tcp::TcpSocket;
use wasi_sockets_tests::*;

/// Bind a socket and let the system determine a port.
fn test_tcp_bind_ephemeral_port(net: &Network, ip: IpAddress) {
let bind_addr = IpSocketAddress::new(ip, 0);

let sock = TcpSocket::new(ip.family()).unwrap();
sock.blocking_bind(net, bind_addr).unwrap();

let bound_addr = sock.local_address().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: &Network, ip: IpAddress) {
const PORT: u16 = 54321;

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

let sock = TcpSocket::new(ip.family()).unwrap();
sock.blocking_bind(net, bind_addr).unwrap();

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.
fn test_tcp_bind_addrinuse(net: &Network, ip: IpAddress) {
let bind_addr = IpSocketAddress::new(ip, 0);

let sock1 = TcpSocket::new(ip.family()).unwrap();
sock1.blocking_bind(net, bind_addr).unwrap();
sock1.blocking_listen().unwrap();

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

let sock2 = TcpSocket::new(ip.family()).unwrap();
assert_eq!(
sock2.blocking_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: &Network, ip: IpAddress) {
let bind_addr = IpSocketAddress::new(ip, 0);

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

assert_eq!(
sock.blocking_bind(net, bind_addr),
Err(ErrorCode::AddressNotBindable)
);
}

/// Bind should validate the address family.
fn test_tcp_bind_wrong_family(net: &Network, family: IpAddressFamily) {
let wrong_ip = match family {
IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK,
IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK,
};

let sock = TcpSocket::new(family).unwrap();
let result = sock.blocking_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: &Network) {
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 = TcpSocket::new(IpAddressFamily::Ipv4).unwrap();
let sock_v6 = TcpSocket::new(IpAddressFamily::Ipv6).unwrap();

assert!(matches!(
sock_v4.blocking_bind(net, ipv4_broadcast),
Err(ErrorCode::InvalidArgument)
));
assert!(matches!(
sock_v4.blocking_bind(net, ipv4_multicast),
Err(ErrorCode::InvalidArgument)
));
assert!(matches!(
sock_v6.blocking_bind(net, ipv6_multicast),
Err(ErrorCode::InvalidArgument)
));
}

fn test_tcp_bind_dual_stack(net: &Network) {
let sock = TcpSocket::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.
sock.set_ipv6_only(true).unwrap();

// Binding an IPv4-mapped-IPv6 address on a ipv6-only socket should fail:
assert!(matches!(
sock.blocking_bind(net, addr),
Err(ErrorCode::InvalidArgument)
));

sock.set_ipv6_only(false).unwrap();

sock.blocking_bind(net, addr).unwrap();

let bound_addr = sock.local_address().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 = Network::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);
}
119 changes: 119 additions & 0 deletions crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use wasi::sockets::network::{ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Network};
use wasi::sockets::tcp::TcpSocket;
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: &Network, family: IpAddressFamily) {
let addr = IpSocketAddress::new(IpAddress::new_unspecified(family), SOME_PORT);
let sock = TcpSocket::new(family).unwrap();

assert!(matches!(
sock.blocking_connect(net, addr),
Err(ErrorCode::InvalidArgument)
));
}

/// 0 is not a valid remote port.
fn test_tcp_connect_port_0(net: &Network, family: IpAddressFamily) {
let addr = IpSocketAddress::new(IpAddress::new_loopback(family), 0);
let sock = TcpSocket::new(family).unwrap();

assert!(matches!(
sock.blocking_connect(net, addr),
Err(ErrorCode::InvalidArgument)
));
}

/// Bind should validate the address family.
fn test_tcp_connect_wrong_family(net: &Network, 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 = TcpSocket::new(family).unwrap();

assert!(matches!(
sock.blocking_connect(net, remote_addr),
Err(ErrorCode::InvalidArgument)
));
}

/// Can only connect to unicast addresses.
fn test_tcp_connect_non_unicast(net: &Network) {
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 = TcpSocket::new(IpAddressFamily::Ipv4).unwrap();
let sock_v6 = TcpSocket::new(IpAddressFamily::Ipv6).unwrap();

assert!(matches!(
sock_v4.blocking_connect(net, ipv4_broadcast),
Err(ErrorCode::InvalidArgument)
));
assert!(matches!(
sock_v4.blocking_connect(net, ipv4_multicast),
Err(ErrorCode::InvalidArgument)
));
assert!(matches!(
sock_v6.blocking_connect(net, ipv6_multicast),
Err(ErrorCode::InvalidArgument)
));
}

fn test_tcp_connect_dual_stack(net: &Network) {
// Set-up:
let v4_listener = TcpSocket::new(IpAddressFamily::Ipv4).unwrap();
v4_listener
.blocking_bind(&net, IpSocketAddress::new(IpAddress::IPV4_LOOPBACK, 0))
.unwrap();
v4_listener.blocking_listen().unwrap();

let v4_listener_addr = v4_listener.local_address().unwrap();
let v6_listener_addr =
IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, v4_listener_addr.port());

let v6_client = TcpSocket::new(IpAddressFamily::Ipv6).unwrap();

// Tests:

// Even on platforms that don't support dualstack sockets,
// setting ipv6_only to true (disabling dualstack mode) should work.
v6_client.set_ipv6_only(true).unwrap();

// Connecting to an IPv4-mapped-IPv6 address on an ipv6-only socket should fail:
assert!(matches!(
v6_client.blocking_connect(net, v6_listener_addr),
Err(ErrorCode::InvalidArgument)
));

v6_client.set_ipv6_only(false).unwrap();

v6_client.blocking_connect(net, v6_listener_addr).unwrap();

let connected_addr = v6_client.local_address().unwrap();

assert_eq!(connected_addr.family(), IpAddressFamily::Ipv6);
}

fn main() {
let net = Network::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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use wasi::sockets::network::{
IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, Network,
};
use wasi::sockets::tcp::TcpSocket;
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 = Network::default();
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();

{
let client = TcpSocket::new(family).unwrap();
let (_client_input, client_output) = client.blocking_connect(&net, addr).unwrap();

client_output.blocking_write_util(&[]).unwrap();
client_output.blocking_write_util(first_message).unwrap();
}

{
let (_accepted, input, _output) = listener.accept().unwrap();

let empty_data = input.read(0).unwrap();
assert!(empty_data.is_empty());

let data = input.blocking_read(first_message.len() as u64).unwrap();

// 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 = TcpSocket::new(family).unwrap();
let (_client_input, client_output) = client.blocking_connect(&net, addr).unwrap();

client_output.blocking_write_util(second_message).unwrap();
}

{
let (_accepted, input, _output) = listener.accept().unwrap();
let data = input.blocking_read(second_message.len() as u64).unwrap();

// Check that we sent and recieved our message!
assert_eq!(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,
}),
);
}
Loading

0 comments on commit 89449b6

Please sign in to comment.