diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index 8484a4f98f77..c94243a2a866 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -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"))] diff --git a/crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs b/crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs index 50932e057c65..8e8869109d1c 100644 --- a/crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs +++ b/crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs @@ -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 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..8c6ce3604d56 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_bind.rs @@ -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); +} 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..3629ad2bfca1 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_connect.rs @@ -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); +} 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..9f182f2c4660 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sample_application.rs @@ -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, + }), + ); +} 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..c1a5df8ea7a5 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_sockopts.rs @@ -0,0 +1,189 @@ +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); + + if family == IpAddressFamily::Ipv6 { + sock.ipv6_only().unwrap(); // Only verify that it has a default value at all, but either value is valid. + } + + sock.keep_alive().unwrap(); // Only verify that it has a default value at all, but either value is valid. + assert_eq!(sock.no_delay().unwrap(), false); + assert!(sock.unicast_hop_limit().unwrap() > 0); + assert!(sock.receive_buffer_size().unwrap() > 0); + assert!(sock.send_buffer_size().unwrap() > 0); +} + +fn test_tcp_sockopt_input_ranges(family: IpAddressFamily) { + let sock = TcpSocket::new(family).unwrap(); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(sock.set_ipv6_only(true), Ok(_))); + assert!(matches!(sock.set_ipv6_only(false), Ok(_))); + } + + 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(_))); + + 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(1), Ok(_))); + assert!(matches!(sock.set_unicast_hop_limit(u8::MAX), Ok(_))); + + assert!(matches!(sock.set_receive_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_receive_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_send_buffer_size(0), Ok(_))); // Unsupported sizes should be silently capped. + assert!(matches!(sock.set_send_buffer_size(u64::MAX), Ok(_))); // Unsupported sizes should be silently capped. +} + +fn test_tcp_sockopt_readback(family: IpAddressFamily) { + let sock = TcpSocket::new(family).unwrap(); + + if family == IpAddressFamily::Ipv6 { + sock.set_ipv6_only(true).unwrap(); + assert_eq!(sock.ipv6_only().unwrap(), true); + sock.set_ipv6_only(false).unwrap(); + assert_eq!(sock.ipv6_only().unwrap(), false); + } + + sock.set_keep_alive(true).unwrap(); + assert_eq!(sock.keep_alive().unwrap(), true); + sock.set_keep_alive(false).unwrap(); + assert_eq!(sock.keep_alive().unwrap(), false); + + sock.set_no_delay(true).unwrap(); + assert_eq!(sock.no_delay().unwrap(), true); + sock.set_no_delay(false).unwrap(); + assert_eq!(sock.no_delay().unwrap(), false); + + sock.set_unicast_hop_limit(42).unwrap(); + assert_eq!(sock.unicast_hop_limit().unwrap(), 42); + + sock.set_receive_buffer_size(0x10000).unwrap(); + assert_eq!(sock.receive_buffer_size().unwrap(), 0x10000); + + sock.set_send_buffer_size(0x10000).unwrap(); + assert_eq!(sock.send_buffer_size().unwrap(), 0x10000); +} + +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(); + + let default_ipv6_only = listener.ipv6_only().unwrap_or(false); + let default_keep_alive = listener.keep_alive().unwrap(); + + // Configure options on listener: + { + if family == IpAddressFamily::Ipv6 { + listener.set_ipv6_only(!default_ipv6_only).unwrap(); + } + + 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(); + } + + listener.blocking_bind(&net, bind_addr).unwrap(); + listener.blocking_listen().unwrap(); + let bound_addr = listener.local_address().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: + { + if family == IpAddressFamily::Ipv6 { + assert_eq!(accepted_client.ipv6_only().unwrap(), !default_ipv6_only); + } + + 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); + } + + // Update options on listener to something else: + { + listener.set_keep_alive(default_keep_alive).unwrap(); + listener.set_no_delay(false).unwrap(); + listener.set_unicast_hop_limit(43).unwrap(); + listener.set_receive_buffer_size(0x20000).unwrap(); + listener.set_send_buffer_size(0x20000).unwrap(); + } + + // Verify that the already accepted socket was not affected: + { + 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 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(); + + 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); + + test_tcp_sockopt_after_listen(&net, IpAddressFamily::Ipv4); + test_tcp_sockopt_after_listen(&net, IpAddressFamily::Ipv6); +} 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..8547749e5e68 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_states.rs @@ -0,0 +1,264 @@ +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))); + // Skipping: tcp::start_connect + assert!(matches!( + sock.finish_connect(), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!( + sock.start_listen(), + Err(ErrorCode::InvalidState) // Unlike POSIX, trying to listen without an explicit bind should fail in WASI. + )); + assert!(matches!( + sock.finish_listen(), + Err(ErrorCode::NotInProgress) + )); + 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.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.address_family(), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(sock.ipv6_only(), Ok(_))); + + // Even on platforms that don't support dualstack sockets, + // 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.set_ipv6_only(true), + Err(ErrorCode::NotSupported) + )); + } + + 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(_))); + 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.receive_buffer_size(), 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(_))); +} + +fn test_tcp_bound_state_invariants(net: &Network, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocket::new(family).unwrap(); + sock.blocking_bind(net, bind_address).unwrap(); + + assert!(matches!( + sock.start_bind(net, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); + // Skipping: tcp::start_connect + assert!(matches!( + sock.finish_connect(), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::start_listen + assert!(matches!( + sock.finish_listen(), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!(sock.accept(), Err(ErrorCode::InvalidState))); + assert!(matches!( + sock.shutdown(ShutdownType::Both), + Err(ErrorCode::InvalidState) + )); + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!( + sock.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.address_family(), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(sock.ipv6_only(), Ok(_))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::NotSupported) + )); + } + + 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(_))); + 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.receive_buffer_size(), 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(_))); +} + +fn test_tcp_listening_state_invariants(net: &Network, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock = TcpSocket::new(family).unwrap(); + sock.blocking_bind(net, bind_address).unwrap(); + sock.blocking_listen().unwrap(); + + assert!(matches!( + sock.start_bind(net, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); + assert!(matches!( + sock.start_connect(net, bind_address), // Actual address shouldn't matter + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + sock.finish_connect(), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!(sock.start_listen(), Err(ErrorCode::InvalidState))); + assert!(matches!( + sock.finish_listen(), + Err(ErrorCode::NotInProgress) + )); + // Skipping: tcp::accept + assert!(matches!( + sock.shutdown(ShutdownType::Both), + Err(ErrorCode::InvalidState) + )); + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!( + sock.remote_address(), + Err(ErrorCode::InvalidState) + )); + assert_eq!(sock.address_family(), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(sock.ipv6_only(), Ok(_))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::NotSupported) + )); + } + + 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(_))); + 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.receive_buffer_size(), 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(_))); +} + +fn test_tcp_connected_state_invariants(net: &Network, family: IpAddressFamily) { + let bind_address = IpSocketAddress::new(IpAddress::new_loopback(family), 0); + let sock_listener = TcpSocket::new(family).unwrap(); + sock_listener.blocking_bind(net, bind_address).unwrap(); + sock_listener.blocking_listen().unwrap(); + let addr_listener = sock_listener.local_address().unwrap(); + let sock = TcpSocket::new(family).unwrap(); + let (_input, _output) = sock.blocking_connect(net, addr_listener).unwrap(); + + assert!(matches!( + sock.start_bind(net, bind_address), + Err(ErrorCode::InvalidState) + )); + assert!(matches!(sock.finish_bind(), Err(ErrorCode::NotInProgress))); + assert!(matches!( + sock.start_connect(net, addr_listener), + Err(ErrorCode::InvalidState) + )); + assert!(matches!( + sock.finish_connect(), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!(sock.start_listen(), Err(ErrorCode::InvalidState))); + assert!(matches!( + sock.finish_listen(), + Err(ErrorCode::NotInProgress) + )); + assert!(matches!(sock.accept(), Err(ErrorCode::InvalidState))); + // Skipping: tcp::shutdown + + assert!(matches!(sock.local_address(), Ok(_))); + assert!(matches!(sock.remote_address(), Ok(_))); + assert_eq!(sock.address_family(), family); + + if family == IpAddressFamily::Ipv6 { + assert!(matches!(sock.ipv6_only(), Ok(_))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::InvalidState) + )); + } else { + assert!(matches!(sock.ipv6_only(), Err(ErrorCode::NotSupported))); + assert!(matches!( + sock.set_ipv6_only(true), + Err(ErrorCode::NotSupported) + )); + } + + assert!(matches!(sock.keep_alive(), Ok(_))); + assert!(matches!(sock.set_keep_alive(false), Ok(_))); + 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.receive_buffer_size(), 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(_))); +} + +fn main() { + let net = Network::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/bin/tcp_v4.rs b/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs deleted file mode 100644 index cf02fdd79663..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v4.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! A simple TCP testcase, using IPv4. - -use wasi::io::poll; -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv4SocketAddress}; -use wasi::sockets::{instance_network, 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 = sock.subscribe(); - - sock.start_bind(&net, addr).unwrap(); - - poll::poll_one(&sub); - drop(sub); - - sock.finish_bind().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 807db9825f1e..000000000000 --- a/crates/test-programs/wasi-sockets-tests/src/bin/tcp_v6.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! Like v4.rs, but with IPv6. - -use wasi::io::poll; -use wasi::sockets::network::{IpAddressFamily, IpSocketAddress, Ipv6SocketAddress}; -use wasi::sockets::{instance_network, 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 = sock.subscribe(); - - sock.start_bind(&net, addr).unwrap(); - - poll::poll_one(&sub); - drop(sub); - - sock.finish_bind().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 a46cff830c0f..4d90e914ddda 100644 --- a/crates/test-programs/wasi-sockets-tests/src/lib.rs +++ b/crates/test-programs/wasi-sockets-tests/src/lib.rs @@ -1,97 +1,191 @@ 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::io::poll::{self, Pollable}; +use wasi::io::streams::{InputStream, OutputStream, StreamError}; +use wasi::sockets::instance_network; +use wasi::sockets::network::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, + Network, +}; +use wasi::sockets::tcp::TcpSocket; +use wasi::sockets::tcp_create_socket; + +impl Pollable { + pub fn wait(&self) { + poll::poll_one(self); + } +} -pub fn write(output: &streams::OutputStream, mut bytes: &[u8]) -> Result<(), streams::StreamError> { - let pollable = output.subscribe(); +impl OutputStream { + pub fn blocking_write_util(&self, mut bytes: &[u8]) -> Result<(), StreamError> { + let pollable = self.subscribe(); - while !bytes.is_empty() { - poll::poll_list(&[&pollable]); + while !bytes.is_empty() { + pollable.wait(); - let permit = output.check_write()?; + let permit = self.check_write()?; - 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); - output.write(chunk)?; + self.write(chunk)?; - output.blocking_flush()?; + self.blocking_flush()?; - bytes = rest; + bytes = rest; + } + Ok(()) } - Ok(()) } -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 = sock.subscribe(); +impl Network { + pub fn default() -> Network { + instance_network::instance_network() + } +} - sock.set_listen_backlog_size(32).unwrap(); +impl TcpSocket { + pub fn new(address_family: IpAddressFamily) -> Result<TcpSocket, ErrorCode> { + tcp_create_socket::create_tcp_socket(address_family) + } - sock.start_listen().unwrap(); - poll::poll_one(&sub); - sock.finish_listen().unwrap(); + 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, + } + } + } - let addr = sock.local_address().unwrap(); + pub fn blocking_listen(&self) -> Result<(), ErrorCode> { + let sub = self.subscribe(); - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = client.subscribe(); + self.start_listen()?; - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - let (client_input, client_output) = client.finish_connect().unwrap(); + loop { + match self.finish_listen() { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } - write(&client_output, &[]).unwrap(); + pub fn blocking_connect( + &self, + network: &Network, + remote_address: IpSocketAddress, + ) -> Result<(InputStream, OutputStream), ErrorCode> { + let sub = self.subscribe(); + + self.start_connect(&network, remote_address)?; + + loop { + match self.finish_connect() { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } - write(&client_output, first_message).unwrap(); + pub fn blocking_accept(&self) -> Result<(TcpSocket, InputStream, OutputStream), ErrorCode> { + let sub = self.subscribe(); - drop(client_input); - drop(client_output); - drop(client_sub); - drop(client); + loop { + match self.accept() { + Err(ErrorCode::WouldBlock) => sub.wait(), + result => return result, + } + } + } +} - poll::poll_one(&sub); - let (accepted, input, output) = sock.accept().unwrap(); +impl IpAddress { + pub const IPV4_BROADCAST: IpAddress = IpAddress::Ipv4((255, 255, 255, 255)); - let empty_data = input.read(0).unwrap(); - assert!(empty_data.is_empty()); + 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)); - let data = input.blocking_read(first_message.len() as u64).unwrap(); + 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)); - drop(input); - drop(output); - drop(accepted); + pub const IPV4_MAPPED_LOOPBACK: IpAddress = + IpAddress::Ipv6((0, 0, 0, 0, 0, 0xFFFF, 0x7F00, 0x0001)); - // Check that we sent and recieved our message! - assert_eq!(data, first_message); // Not guaranteed to work but should work in practice. + pub const fn new_loopback(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_LOOPBACK, + IpAddressFamily::Ipv6 => Self::IPV6_LOOPBACK, + } + } - // Another client - let client = tcp_create_socket::create_tcp_socket(family).unwrap(); - let client_sub = client.subscribe(); + pub const fn new_unspecified(family: IpAddressFamily) -> IpAddress { + match family { + IpAddressFamily::Ipv4 => Self::IPV4_UNSPECIFIED, + IpAddressFamily::Ipv6 => Self::IPV6_UNSPECIFIED, + } + } - client.start_connect(&net, addr).unwrap(); - poll::poll_one(&client_sub); - let (client_input, client_output) = client.finish_connect().unwrap(); + pub const fn family(&self) -> IpAddressFamily { + match self { + IpAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } +} - write(&client_output, second_message).unwrap(); +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, + } + } +} - drop(client_input); - drop(client_output); - drop(client_sub); - drop(client); +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, + }), + } + } - poll::poll_one(&sub); - let (accepted, input, output) = sock.accept().unwrap(); - let data = input.blocking_read(second_message.len() as u64).unwrap(); + pub const fn ip(&self) -> IpAddress { + match self { + IpSocketAddress::Ipv4(addr) => IpAddress::Ipv4(addr.address), + IpSocketAddress::Ipv6(addr) => IpAddress::Ipv6(addr.address), + } + } - drop(input); - drop(output); - drop(accepted); + pub const fn port(&self) -> u16 { + match self { + IpSocketAddress::Ipv4(addr) => addr.port, + IpSocketAddress::Ipv6(addr) => addr.port, + } + } - // Check that we sent and recieved our message! - assert_eq!(data, second_message); // Not guaranteed to work but should work in practice. + pub const fn family(&self) -> IpAddressFamily { + match self { + IpSocketAddress::Ipv4(_) => IpAddressFamily::Ipv4, + IpSocketAddress::Ipv6(_) => IpAddressFamily::Ipv6, + } + } } 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 da9b435d9ef9..8fc3074af6d5 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: /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html> diff --git a/crates/wasi-http/wit/deps/sockets/network.wit b/crates/wasi-http/wit/deps/sockets/network.wit index 03755253b294..861ec673de68 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,72 +64,36 @@ 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 ### datagram-too-large, @@ -129,9 +101,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 b64cabba7993..a9a33738b20d 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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html> diff --git a/crates/wasi-http/wit/deps/sockets/tcp.wit b/crates/wasi-http/wit/deps/sockets/tcp.wit index 0ae7c05e8fcb..62a9068716b2 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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html> @@ -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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html> @@ -140,10 +165,10 @@ interface tcp { /// - <https://man.freebsd.org/cgi/man.cgi?getsockname> local-address: func() -> result<ip-socket-address, error-code>; - /// 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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html> @@ -162,40 +187,35 @@ 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<bool, error-code>; 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) + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `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<bool, error-code>; 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<bool, error-code>; 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<u8, error-code>; set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; @@ -211,9 +231,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<u64, error-code>; set-receive-buffer-size: func(value: u64) -> result<_, error-code>; send-buffer-size: func() -> result<u64, error-code>; @@ -237,7 +256,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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shutdown.html> 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 64d899456ca8..e026359fd90f 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: /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html> diff --git a/crates/wasi-http/wit/deps/sockets/udp.wit b/crates/wasi-http/wit/deps/sockets/udp.wit index a29250caa9af..7a9d7f72c4fa 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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html> @@ -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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html> @@ -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<bool, error-code>; 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<u8, error-code>; 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<u64, error-code>; set-receive-buffer-size: func(value: u64) -> result<_, error-code>; send-buffer-size: func() -> result<u64, error-code>; diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index b2a8ff59a700..bd97df613820 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -3,6 +3,7 @@ use crate::preview2::bindings::sockets::network::{ Ipv6SocketAddress, }; use crate::preview2::{SocketError, WasiView}; +use rustix::io::Errno; use std::io; use wasmtime::component::Resource; @@ -23,47 +24,83 @@ impl<T: WasiView> crate::preview2::bindings::sockets::network::HostNetwork for T } impl From<io::Error> for ErrorCode { - 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 that don't correspond to a Rust `io::ErrorKind`. - io::ErrorKind::Other => match error.raw_os_error() { - 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(_) | None => { - log::debug!("unknown I/O error: {error}"); - ErrorCode::Unknown - } - }, + 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(); + } + + 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, _ => { - log::debug!("unknown I/O error: {error}"); + log::debug!("unknown I/O error: {value}"); ErrorCode::Unknown } } } } -impl From<rustix::io::Errno> for ErrorCode { - fn from(error: rustix::io::Errno) -> Self { - std::io::Error::from(error).into() +impl From<Errno> 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, + 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::ConnectionAborted, + Errno::INVAL => ErrorCode::InvalidArgument, + 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::InvalidState, + Errno::NOTCONN => ErrorCode::InvalidState, + Errno::DESTADDRREQ => ErrorCode::InvalidState, + #[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, + + // FYI, EINPROGRESS should have already been handled by connect. + _ => { + log::debug!("unknown I/O error: {value}"); + ErrorCode::Unknown + } + } } } @@ -174,3 +211,12 @@ impl From<IpAddressFamily> for cap_net_ext::AddressFamily { } } } + +impl From<cap_net_ext::AddressFamily> 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 0cf88b9100a4..82e824b3a141 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -1,15 +1,19 @@ -use crate::preview2::bindings::{ - io::streams::{InputStream, OutputStream}, - sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}, - sockets::tcp::{self, ShutdownType}, -}; use crate::preview2::tcp::{TcpSocket, TcpState}; +use crate::preview2::{ + bindings::{ + io::streams::{InputStream, OutputStream}, + sockets::network::{ErrorCode, IpAddressFamily, IpSocketAddress, Network}, + sockets::tcp::{self, ShutdownType}, + }, + tcp::SocketAddressFamily, +}; use crate::preview2::{Pollable, SocketResult, WasiView}; use cap_net_ext::{Blocking, PoolExt, TcpListenerExt}; use cap_std::net::TcpListener; use io_lifetimes::AsSocketlike; use rustix::io::Errno; use rustix::net::sockopt; +use std::net::{IpAddr, Ipv6Addr, SocketAddr}; use tokio::io::Interest; use wasmtime::component::Resource; @@ -24,19 +28,29 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { ) -> SocketResult<()> { let table = self.table_mut(); let socket = table.get_resource(&this)?; + let network = table.get_resource(&network)?; + let local_address: SocketAddr = local_address.into(); match socket.tcp_state { TcpState::Default => {} - _ => return Err(ErrorCode::NotInProgress.into()), + TcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), + _ => return Err(ErrorCode::InvalidState.into()), } - let network = table.get_resource(&network)?; + validate_unicast(&local_address)?; + validate_address_family(&socket, &local_address)?; + let binder = network.pool.tcp_binder(local_address)?; // Perform the OS bind call. - binder.bind_existing_tcp_listener( - &*socket.tcp_socket().as_socketlike_view::<TcpListener>(), - )?; + binder + .bind_existing_tcp_listener(&*socket.tcp_socket().as_socketlike_view::<TcpListener>()) + .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. + _ => ErrorCode::from(error), + })?; let socket = table.get_resource_mut(&this)?; socket.tcp_state = TcpState::BindStarted; @@ -67,14 +81,25 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { let table = self.table_mut(); let r = { let socket = table.get_resource(&this)?; + let network = table.get_resource(&network)?; + let remote_address: SocketAddr = remote_address.into(); match socket.tcp_state { TcpState::Default => {} - TcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + TcpState::Bound + | TcpState::Connected + | TcpState::ConnectFailed + | TcpState::Listening => return Err(ErrorCode::InvalidState.into()), + TcpState::Connecting + | TcpState::ConnectReady + | TcpState::ListenStarted + | TcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), } - let network = table.get_resource(&network)?; + validate_unicast(&remote_address)?; + validate_remote_address(&remote_address)?; + validate_address_family(&socket, &remote_address)?; + let connecter = network.pool.tcp_connecter(remote_address)?; // Do an OS `connect`. Our socket is non-blocking, so it'll either... @@ -93,9 +118,14 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { return Ok(()); } // continue in progress, - Err(err) if err.raw_os_error() == Some(INPROGRESS.raw_os_error()) => {} + Err(err) if Errno::from_io_error(&err) == Some(INPROGRESS) => {} // or fail immediately. - Err(err) => return Err(err.into()), + Err(err) => { + 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(), + }); + } } let socket = table.get_resource_mut(&this)?; @@ -131,7 +161,10 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket 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 = TcpState::ConnectFailed; + return Err(err.into()); + } } } _ => return Err(ErrorCode::NotInProgress.into()), @@ -151,15 +184,25 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { match socket.tcp_state { TcpState::Bound => {} - TcpState::ListenStarted => return Err(ErrorCode::AlreadyListening.into()), - TcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + TcpState::Default + | TcpState::Connected + | TcpState::ConnectFailed + | TcpState::Listening => return Err(ErrorCode::InvalidState.into()), + TcpState::ListenStarted + | TcpState::Connecting + | TcpState::ConnectReady + | TcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), } socket .tcp_socket() .as_socketlike_view::<TcpListener>() - .listen(socket.listen_backlog_size)?; + .listen(socket.listen_backlog_size) + .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. + _ => ErrorCode::from(error), + })?; socket.tcp_state = TcpState::ListenStarted; @@ -193,18 +236,63 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { match socket.tcp_state { TcpState::Listening => {} - TcpState::Connected => return Err(ErrorCode::AlreadyConnected.into()), - _ => return Err(ErrorCode::NotInProgress.into()), + _ => return Err(ErrorCode::InvalidState.into()), } // 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::<TcpListener>() - .accept_with(Blocking::No) - })?; - let mut tcp_socket = TcpSocket::from_tcp_stream(connection)?; + let (connection, _addr) = tcp_socket + .try_io(Interest::READABLE, || { + tcp_socket + .as_socketlike_view::<TcpListener>() + .accept_with(Blocking::No) + }) + .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." + + // 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, + + _ => ErrorCode::from(error), + })?; + + #[cfg(target_os = "macos")] + { + // 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 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. + } + + 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)?; // Mark the socket as connected so that we can exit early from methods like `start-bind`. tcp_socket.tcp_state = TcpState::Connected; @@ -222,6 +310,13 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { fn local_address(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<IpSocketAddress> { let table = self.table(); let socket = table.get_resource(&this)?; + + match socket.tcp_state { + TcpState::Default => return Err(ErrorCode::InvalidState.into()), + TcpState::BindStarted => return Err(ErrorCode::ConcurrencyConflict.into()), + _ => {} + } + let addr = socket .tcp_socket() .as_socketlike_view::<std::net::TcpStream>() @@ -232,6 +327,12 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { fn remote_address(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<IpSocketAddress> { let table = self.table(); let socket = table.get_resource(&this)?; + + match socket.tcp_state { + TcpState::Connected => {} + _ => return Err(ErrorCode::InvalidState.into()), + } + let addr = socket .tcp_socket() .as_socketlike_view::<std::net::TcpStream>() @@ -246,57 +347,41 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { let table = self.table(); 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.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()) + match socket.family { + SocketAddressFamily::Ipv4 => Ok(IpAddressFamily::Ipv4), + SocketAddressFamily::Ipv6 { .. } => Ok(IpAddressFamily::Ipv6), } } fn ipv6_only(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<bool> { let table = self.table(); let socket = table.get_resource(&this)?; - Ok(sockopt::get_ipv6_v6only(socket.tcp_socket())?) + + // 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 { + SocketAddressFamily::Ipv4 => Err(ErrorCode::NotSupported.into()), + SocketAddressFamily::Ipv6 { v6only } => Ok(v6only), + } } fn set_ipv6_only(&mut self, this: Resource<tcp::TcpSocket>, value: bool) -> SocketResult<()> { - let table = self.table(); - let socket = table.get_resource(&this)?; - Ok(sockopt::set_ipv6_v6only(socket.tcp_socket(), value)?) + let table = self.table_mut(); + let socket = table.get_resource_mut(&this)?; + + match socket.family { + 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()), + }, + } } fn set_listen_backlog_size( @@ -328,13 +413,13 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { // Not all platforms support this. We'll only update our own value if the OS supports changing the backlog size after the fact. rustix::net::listen(socket.tcp_socket(), value) - .map_err(|_| ErrorCode::AlreadyListening)?; + .map_err(|_| ErrorCode::NotSupported)?; socket.listen_backlog_size = Some(value); Ok(()) } - TcpState::Connected => Err(ErrorCode::AlreadyConnected.into()), + TcpState::Connected | TcpState::ConnectFailed => Err(ErrorCode::InvalidState.into()), TcpState::Connecting | TcpState::ConnectReady | TcpState::ListenStarted => { Err(ErrorCode::ConcurrencyConflict.into()) } @@ -369,17 +454,16 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { 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.tcp_socket()) { - Ok(value) => Ok(value), - Err(Errno::NOPROTOOPT) => { - let value = sockopt::get_ip_ttl(socket.tcp_socket())?; - let value = value.try_into().unwrap(); - Ok(value) + let ttl = match socket.family { + SocketAddressFamily::Ipv4 => sockopt::get_ip_ttl(socket.tcp_socket())? + .try_into() + .unwrap(), + SocketAddressFamily::Ipv6 { .. } => { + sockopt::get_ipv6_unicast_hops(socket.tcp_socket())? } - Err(err) => Err(err.into()), - } + }; + + Ok(ttl) } fn set_unicast_hop_limit( @@ -387,22 +471,37 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource<tcp::TcpSocket>, 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. + // 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 { + SocketAddressFamily::Ipv4 => sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?, + SocketAddressFamily::Ipv6 { .. } => { + sockopt::set_ipv6_unicast_hops(socket.tcp_socket(), Some(value))? + } + } - // 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) => Ok(sockopt::set_ip_ttl(socket.tcp_socket(), value.into())?), - Err(err) => Err(err.into()), + #[cfg(target_os = "macos")] + { + socket.hop_limit = Some(value); } + + Ok(()) } fn receive_buffer_size(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u64> { let table = self.table(); let socket = table.get_resource(&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( @@ -410,19 +509,37 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource<tcp::TcpSocket>, 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.tcp_socket(), - value, - )?) + 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) { + // 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, + }?; + + #[cfg(target_os = "macos")] + { + socket.receive_buffer_size = Some(value); + } + + Ok(()) } fn send_buffer_size(&mut self, this: Resource<tcp::TcpSocket>) -> SocketResult<u64> { let table = self.table(); let socket = table.get_resource(&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( @@ -430,13 +547,21 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { this: Resource<tcp::TcpSocket>, 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.tcp_socket(), - value, - )?) + 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) { + Err(Errno::NOBUFS) => Ok(()), // See `set_receive_buffer_size` + r => r, + }?; + + #[cfg(target_os = "macos")] + { + socket.send_buffer_size = Some(value); + } + + Ok(()) } fn subscribe(&mut self, this: Resource<tcp::TcpSocket>) -> anyhow::Result<Resource<Pollable>> { @@ -451,6 +576,14 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { let table = self.table(); let socket = table.get_resource(&this)?; + match socket.tcp_state { + TcpState::Connected => {} + TcpState::Connecting | TcpState::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, @@ -479,6 +612,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T { | TcpState::BindStarted | TcpState::Bound | TcpState::ListenStarted + | TcpState::ConnectFailed | TcpState::ConnectReady => {} TcpState::Listening | TcpState::Connecting | TcpState::Connected => { @@ -505,3 +639,89 @@ const INPROGRESS: Errno = Errno::INPROGRESS; // <https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-connect> #[cfg(windows)] const INPROGRESS: Errno = Errno::WOULDBLOCK; + +fn validate_unicast(addr: &SocketAddr) -> SocketResult<()> { + match to_canonical(&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) -> SocketResult<()> { + if to_canonical(&addr.ip()).is_unspecified() { + return Err(ErrorCode::InvalidArgument.into()); + } + + if addr.port() == 0 { + return Err(ErrorCode::InvalidArgument.into()); + } + + Ok(()) +} + +fn validate_address_family(socket: &TcpSocket, addr: &SocketAddr) -> SocketResult<()> { + match (socket.family, addr.ip()) { + (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()), + (SocketAddressFamily::Ipv6 { v6only }, IpAddr::V6(ipv6)) => { + 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(()) + } + } + _ => Err(ErrorCode::InvalidArgument.into()), + } +} + +// 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 { + IpAddr::V6(*ipv6) + } + } + } +} + +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() +} + +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 + } +} diff --git a/crates/wasi/src/preview2/ip_name_lookup.rs b/crates/wasi/src/preview2/ip_name_lookup.rs index ce7cde93b9b9..e3f9e1a61b15 100644 --- a/crates/wasi/src/preview2/ip_name_lookup.rs +++ b/crates/wasi/src/preview2/ip_name_lookup.rs @@ -3,7 +3,6 @@ use crate::preview2::bindings::sockets::network::{ErrorCode, IpAddress, IpAddres use crate::preview2::poll::{subscribe, Pollable, Subscribe}; use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, SocketError, WasiView}; use anyhow::Result; -use std::io; use std::mem; use std::net::{SocketAddr, ToSocketAddrs}; use std::pin::Pin; @@ -11,8 +10,8 @@ use std::vec; use wasmtime::component::Resource; pub enum ResolveAddressStream { - Waiting(AbortOnDropJoinHandle<io::Result<Vec<IpAddress>>>), - Done(io::Result<vec::IntoIter<IpAddress>>), + Waiting(AbortOnDropJoinHandle<Result<Vec<IpAddress>, SocketError>>), + Done(Result<vec::IntoIter<IpAddress>, SocketError>), } #[async_trait::async_trait] @@ -29,10 +28,10 @@ impl<T: WasiView> Host for T { // `Host::parse` serves us two functions: // 1. validate the input is not an IP address, // 2. convert unicode domains to punycode. - let name = match url::Host::parse(&name).map_err(|_| ErrorCode::InvalidName)? { + let name = match url::Host::parse(&name).map_err(|_| ErrorCode::InvalidArgument)? { url::Host::Domain(name) => name, - url::Host::Ipv4(_) => return Err(ErrorCode::InvalidName.into()), - url::Host::Ipv6(_) => return Err(ErrorCode::InvalidName.into()), + url::Host::Ipv4(_) => return Err(ErrorCode::InvalidArgument.into()), + url::Host::Ipv6(_) => return Err(ErrorCode::InvalidArgument.into()), }; if !network.allow_ip_name_lookup { @@ -48,8 +47,10 @@ impl<T: WasiView> 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<Vec<_>> { - let result = (name.as_str(), 0).to_socket_addrs()?; + let task = spawn_blocking(move || -> Result<Vec<_>, SocketError> { + let result = (name.as_str(), 0) + .to_socket_addrs() + .map_err(|_| ErrorCode::NameUnresolvable)?; // If/when we use `getaddrinfo` directly, map the error properly. Ok(result .filter_map(|addr| { // In lieu of preventing these addresses from being resolved @@ -98,12 +99,6 @@ impl<T: WasiView> 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!(); } diff --git a/crates/wasi/src/preview2/tcp.rs b/crates/wasi/src/preview2/tcp.rs index 434eeb3aeead..dd782c4c66ce 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; @@ -38,6 +39,9 @@ pub(crate) enum TcpState { /// An outgoing connection is ready to be established. ConnectReady, + /// An outgoing connection was attempted but failed. + ConnectFailed, + /// An outgoing connection has been established. Connected, } @@ -56,6 +60,24 @@ pub struct TcpSocket { /// The desired listen queue size. Set to None to use the system's default. pub(crate) listen_backlog_size: Option<i32>, + + pub(crate) family: SocketAddressFamily, + + /// The manually configured buffer size. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) receive_buffer_size: Option<usize>, + /// The manually configured buffer size. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) send_buffer_size: Option<usize>, + /// The manually configured TTL. `None` means: no preference, use system default. + #[cfg(target_os = "macos")] + pub(crate) hop_limit: Option<u8>, +} + +#[derive(Copy, Clone)] +pub(crate) enum SocketAddressFamily { + Ipv4, + Ipv6 { v6only: bool }, } pub(crate) struct TcpReadStream { @@ -243,18 +265,32 @@ 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) + + 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(tcp_socket: cap_std::net::TcpStream) -> io::Result<Self> { + pub(crate) fn from_tcp_stream( + tcp_socket: cap_std::net::TcpStream, + family: SocketAddressFamily, + ) -> io::Result<Self> { 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<Self> { + pub(crate) fn from_tcp_listener( + tcp_listener: cap_std::net::TcpListener, + family: SocketAddressFamily, + ) -> io::Result<Self> { 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))?; @@ -263,6 +299,13 @@ impl TcpSocket { inner: Arc::new(stream), 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, + #[cfg(target_os = "macos")] + hop_limit: None, }) } diff --git a/crates/wasi/wit/deps/sockets/ip-name-lookup.wit b/crates/wasi/wit/deps/sockets/ip-name-lookup.wit index da9b435d9ef9..8fc3074af6d5 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: /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getaddrinfo.html> diff --git a/crates/wasi/wit/deps/sockets/network.wit b/crates/wasi/wit/deps/sockets/network.wit index 03755253b294..861ec673de68 100644 --- a/crates/wasi/wit/deps/sockets/network.wit +++ b/crates/wasi/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,72 +64,36 @@ 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 ### datagram-too-large, @@ -129,9 +101,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 b64cabba7993..a9a33738b20d 100644 --- a/crates/wasi/wit/deps/sockets/tcp-create-socket.wit +++ b/crates/wasi/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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html> diff --git a/crates/wasi/wit/deps/sockets/tcp.wit b/crates/wasi/wit/deps/sockets/tcp.wit index 0ae7c05e8fcb..62a9068716b2 100644 --- a/crates/wasi/wit/deps/sockets/tcp.wit +++ b/crates/wasi/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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html> @@ -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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html> @@ -140,10 +165,10 @@ interface tcp { /// - <https://man.freebsd.org/cgi/man.cgi?getsockname> local-address: func() -> result<ip-socket-address, error-code>; - /// 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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html> @@ -162,40 +187,35 @@ 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<bool, error-code>; 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) + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `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<bool, error-code>; 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<bool, error-code>; 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<u8, error-code>; set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; @@ -211,9 +231,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<u64, error-code>; set-receive-buffer-size: func(value: u64) -> result<_, error-code>; send-buffer-size: func() -> result<u64, error-code>; @@ -237,7 +256,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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/shutdown.html> diff --git a/crates/wasi/wit/deps/sockets/udp-create-socket.wit b/crates/wasi/wit/deps/sockets/udp-create-socket.wit index 64d899456ca8..e026359fd90f 100644 --- a/crates/wasi/wit/deps/sockets/udp-create-socket.wit +++ b/crates/wasi/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: /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/socket.html> diff --git a/crates/wasi/wit/deps/sockets/udp.wit b/crates/wasi/wit/deps/sockets/udp.wit index a29250caa9af..7a9d7f72c4fa 100644 --- a/crates/wasi/wit/deps/sockets/udp.wit +++ b/crates/wasi/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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getsockname.html> @@ -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 /// - <https://pubs.opengroup.org/onlinepubs/9699919799/functions/getpeername.html> @@ -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<bool, error-code>; 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<u8, error-code>; 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<u64, error-code>; set-receive-buffer-size: func(value: u64) -> result<_, error-code>; send-buffer-size: func() -> result<u64, error-code>;