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>;