Skip to content

Commit 7074afb

Browse files
feat(wasip3): implement wasi:sockets (#11291)
* chore(wasip3): update `wasi:sockets` WIT pull in `wasi:sockets` from WebAssembly/wasi-sockets#126 Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: rearrange Ctx/View Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: name modules after packages Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: move `Network` to `p2` Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * feat(wasip3): implement `wasi:sockets` Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: reuse socket utilities Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * test(wasip3): allow name resolution to fail Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * fix: expect macos-only var to be unused on other OSes Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: whole buffer is written for UDP Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * undo non-blocking write Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * extract buf capacity const, refactor tcp receive Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * introduce container struct for TCP non-inherited opts Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * handle `listen` receiver close Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: clean up Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * adapt to API changes prtest:full Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * refactor: check for listen receiver close each iteration Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> * Try to make test less flaky * Cut down on `#[cfg]` required for wasi:sockets * Fix test for real this time --------- Signed-off-by: Roman Volosatovs <rvolosatovs@riseup.net> Co-authored-by: Alex Crichton <alex@alexcrichton.com>
1 parent 98d06b6 commit 7074afb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+4581
-700
lines changed

ci/vendor-wit.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ make_vendor "wasi/src/p3" "
7373
clocks@13d1c82@wit-0.3.0-draft
7474
filesystem@e2a2ddc@wit-0.3.0-draft
7575
random@4e94663@wit-0.3.0-draft
76-
sockets@bb247e2@wit-0.3.0-draft
76+
sockets@e863ee2@wit-0.3.0-draft
7777
"
7878

7979
rm -rf $cache_dir
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use futures::join;
2+
use test_programs::p3::wasi::sockets::ip_name_lookup::{ErrorCode, resolve_addresses};
3+
use test_programs::p3::wasi::sockets::types::IpAddress;
4+
5+
struct Component;
6+
7+
test_programs::p3::export!(Component);
8+
9+
async fn resolve_one(name: &str) -> Result<IpAddress, ErrorCode> {
10+
Ok(resolve_addresses(name.into())
11+
.await?
12+
.first()
13+
.unwrap()
14+
.to_owned())
15+
}
16+
17+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
18+
async fn run() -> Result<(), ()> {
19+
// Valid domains
20+
let (res0, res1) = join!(
21+
resolve_addresses("localhost".into()),
22+
resolve_addresses("example.com".into())
23+
);
24+
if res0.is_err() && res1.is_err() {
25+
panic!("should have been able to resolve at least one domain");
26+
}
27+
28+
// NB: this is an actual real resolution, so it might time out, might cause
29+
// issues, etc. This result is ignored to prevent flaky failures in CI.
30+
let _ = resolve_addresses("münchen.de".into()).await;
31+
32+
// Valid IP addresses
33+
assert_eq!(
34+
resolve_one("0.0.0.0").await.unwrap(),
35+
IpAddress::IPV4_UNSPECIFIED
36+
);
37+
assert_eq!(
38+
resolve_one("127.0.0.1").await.unwrap(),
39+
IpAddress::IPV4_LOOPBACK
40+
);
41+
assert_eq!(
42+
resolve_one("192.0.2.0").await.unwrap(),
43+
IpAddress::Ipv4((192, 0, 2, 0))
44+
);
45+
assert_eq!(
46+
resolve_one("::").await.unwrap(),
47+
IpAddress::IPV6_UNSPECIFIED
48+
);
49+
assert_eq!(resolve_one("::1").await.unwrap(), IpAddress::IPV6_LOOPBACK);
50+
assert_eq!(
51+
resolve_one("[::]").await.unwrap(),
52+
IpAddress::IPV6_UNSPECIFIED
53+
);
54+
assert_eq!(
55+
resolve_one("2001:0db8:0:0:0:0:0:0").await.unwrap(),
56+
IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0))
57+
);
58+
assert_eq!(
59+
resolve_one("dead:beef::").await.unwrap(),
60+
IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
61+
);
62+
assert_eq!(
63+
resolve_one("dead:beef::0").await.unwrap(),
64+
IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
65+
);
66+
assert_eq!(
67+
resolve_one("DEAD:BEEF::0").await.unwrap(),
68+
IpAddress::Ipv6((0xdead, 0xbeef, 0, 0, 0, 0, 0, 0))
69+
);
70+
71+
// Invalid inputs
72+
assert_eq!(
73+
resolve_addresses("".into()).await.unwrap_err(),
74+
ErrorCode::InvalidArgument
75+
);
76+
assert_eq!(
77+
resolve_addresses(" ".into()).await.unwrap_err(),
78+
ErrorCode::InvalidArgument
79+
);
80+
assert_eq!(
81+
resolve_addresses("a.b<&>".into()).await.unwrap_err(),
82+
ErrorCode::InvalidArgument
83+
);
84+
assert_eq!(
85+
resolve_addresses("127.0.0.1:80".into()).await.unwrap_err(),
86+
ErrorCode::InvalidArgument
87+
);
88+
assert_eq!(
89+
resolve_addresses("[::]:80".into()).await.unwrap_err(),
90+
ErrorCode::InvalidArgument
91+
);
92+
assert_eq!(
93+
resolve_addresses("http://example.com/".into())
94+
.await
95+
.unwrap_err(),
96+
ErrorCode::InvalidArgument
97+
);
98+
Ok(())
99+
}
100+
}
101+
102+
fn main() {}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
use futures::join;
2+
use test_programs::p3::sockets::attempt_random_port;
3+
use test_programs::p3::wasi::sockets::types::{
4+
ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, TcpSocket,
5+
};
6+
use test_programs::p3::wit_stream;
7+
use wit_bindgen::yield_blocking;
8+
9+
struct Component;
10+
11+
test_programs::p3::export!(Component);
12+
13+
/// Bind a socket and let the system determine a port.
14+
fn test_tcp_bind_ephemeral_port(ip: IpAddress) {
15+
let bind_addr = IpSocketAddress::new(ip, 0);
16+
17+
let sock = TcpSocket::new(ip.family());
18+
sock.bind(bind_addr).unwrap();
19+
20+
let bound_addr = sock.local_address().unwrap();
21+
22+
assert_eq!(bind_addr.ip(), bound_addr.ip());
23+
assert_ne!(bind_addr.port(), bound_addr.port());
24+
}
25+
26+
/// Bind a socket on a specified port.
27+
fn test_tcp_bind_specific_port(ip: IpAddress) {
28+
let sock = TcpSocket::new(ip.family());
29+
30+
let bind_addr = attempt_random_port(ip, |bind_addr| sock.bind(bind_addr)).unwrap();
31+
32+
let bound_addr = sock.local_address().unwrap();
33+
34+
assert_eq!(bind_addr.ip(), bound_addr.ip());
35+
assert_eq!(bind_addr.port(), bound_addr.port());
36+
}
37+
38+
/// Two sockets may not be actively bound to the same address at the same time.
39+
fn test_tcp_bind_addrinuse(ip: IpAddress) {
40+
let bind_addr = IpSocketAddress::new(ip, 0);
41+
42+
let sock1 = TcpSocket::new(ip.family());
43+
sock1.bind(bind_addr).unwrap();
44+
sock1.listen().unwrap();
45+
46+
let bound_addr = sock1.local_address().unwrap();
47+
48+
let sock2 = TcpSocket::new(ip.family());
49+
assert_eq!(sock2.bind(bound_addr), Err(ErrorCode::AddressInUse));
50+
}
51+
52+
// The WASI runtime should set SO_REUSEADDR for us
53+
async fn test_tcp_bind_reuseaddr(ip: IpAddress) {
54+
let client = TcpSocket::new(ip.family());
55+
56+
let bind_addr = {
57+
let listener1 = TcpSocket::new(ip.family());
58+
59+
let bind_addr = attempt_random_port(ip, |bind_addr| listener1.bind(bind_addr)).unwrap();
60+
61+
let mut accept = listener1.listen().unwrap();
62+
63+
let connect_addr =
64+
IpSocketAddress::new(IpAddress::new_loopback(ip.family()), bind_addr.port());
65+
join!(
66+
async {
67+
client.connect(connect_addr).await.unwrap();
68+
},
69+
async {
70+
let sock = accept.next().await.unwrap();
71+
let (mut data_tx, data_rx) = wit_stream::new();
72+
join!(
73+
async {
74+
sock.send(data_rx).await.unwrap();
75+
},
76+
async {
77+
let remaining = data_tx.write_all(vec![0; 10]).await;
78+
assert!(remaining.is_empty());
79+
drop(data_tx);
80+
}
81+
);
82+
},
83+
);
84+
85+
bind_addr
86+
};
87+
88+
// If SO_REUSEADDR was configured correctly, the following lines
89+
// shouldn't be affected by the TIME_WAIT state of the just closed
90+
// `listener1` socket.
91+
//
92+
// Note though that the way things are modeled in Wasmtime right now is that
93+
// the TCP socket is kept alive by a spawned task created in `listen`
94+
// meaning that to fully close the socket it requires the spawned task to
95+
// shut down. That may require yielding to the host or similar so try a few
96+
// times to let the host get around to closing the task while testing each
97+
// time to see if we can reuse the address. This loop is bounded because it
98+
// should complete "quickly".
99+
for _ in 0..10 {
100+
let listener2 = TcpSocket::new(ip.family());
101+
if listener2.bind(bind_addr).is_ok() {
102+
listener2.listen().unwrap();
103+
return;
104+
}
105+
yield_blocking();
106+
}
107+
108+
panic!("looks like REUSEADDR isn't in use?");
109+
}
110+
111+
// Try binding to an address that is not configured on the system.
112+
fn test_tcp_bind_addrnotavail(ip: IpAddress) {
113+
let bind_addr = IpSocketAddress::new(ip, 0);
114+
115+
let sock = TcpSocket::new(ip.family());
116+
117+
assert_eq!(sock.bind(bind_addr), Err(ErrorCode::AddressNotBindable));
118+
}
119+
120+
/// Bind should validate the address family.
121+
fn test_tcp_bind_wrong_family(family: IpAddressFamily) {
122+
let wrong_ip = match family {
123+
IpAddressFamily::Ipv4 => IpAddress::IPV6_LOOPBACK,
124+
IpAddressFamily::Ipv6 => IpAddress::IPV4_LOOPBACK,
125+
};
126+
127+
let sock = TcpSocket::new(family);
128+
let result = sock.bind(IpSocketAddress::new(wrong_ip, 0));
129+
130+
assert!(matches!(result, Err(ErrorCode::InvalidArgument)));
131+
}
132+
133+
/// Bind only works on unicast addresses.
134+
fn test_tcp_bind_non_unicast() {
135+
let ipv4_broadcast = IpSocketAddress::new(IpAddress::IPV4_BROADCAST, 0);
136+
let ipv4_multicast = IpSocketAddress::new(IpAddress::Ipv4((224, 254, 0, 0)), 0);
137+
let ipv6_multicast = IpSocketAddress::new(IpAddress::Ipv6((0xff00, 0, 0, 0, 0, 0, 0, 0)), 0);
138+
139+
let sock_v4 = TcpSocket::new(IpAddressFamily::Ipv4);
140+
let sock_v6 = TcpSocket::new(IpAddressFamily::Ipv6);
141+
142+
assert!(matches!(
143+
sock_v4.bind(ipv4_broadcast),
144+
Err(ErrorCode::InvalidArgument)
145+
));
146+
assert!(matches!(
147+
sock_v4.bind(ipv4_multicast),
148+
Err(ErrorCode::InvalidArgument)
149+
));
150+
assert!(matches!(
151+
sock_v6.bind(ipv6_multicast),
152+
Err(ErrorCode::InvalidArgument)
153+
));
154+
}
155+
156+
fn test_tcp_bind_dual_stack() {
157+
let sock = TcpSocket::new(IpAddressFamily::Ipv6);
158+
let addr = IpSocketAddress::new(IpAddress::IPV4_MAPPED_LOOPBACK, 0);
159+
160+
// Binding an IPv4-mapped-IPv6 address on a ipv6-only socket should fail:
161+
assert!(matches!(sock.bind(addr), Err(ErrorCode::InvalidArgument)));
162+
}
163+
164+
impl test_programs::p3::exports::wasi::cli::run::Guest for Component {
165+
async fn run() -> Result<(), ()> {
166+
const RESERVED_IPV4_ADDRESS: IpAddress = IpAddress::Ipv4((192, 0, 2, 0)); // Reserved for documentation and examples.
167+
const RESERVED_IPV6_ADDRESS: IpAddress =
168+
IpAddress::Ipv6((0x2001, 0x0db8, 0, 0, 0, 0, 0, 0)); // Reserved for documentation and examples.
169+
170+
test_tcp_bind_ephemeral_port(IpAddress::IPV4_LOOPBACK);
171+
test_tcp_bind_ephemeral_port(IpAddress::IPV6_LOOPBACK);
172+
test_tcp_bind_ephemeral_port(IpAddress::IPV4_UNSPECIFIED);
173+
test_tcp_bind_ephemeral_port(IpAddress::IPV6_UNSPECIFIED);
174+
175+
test_tcp_bind_specific_port(IpAddress::IPV4_LOOPBACK);
176+
test_tcp_bind_specific_port(IpAddress::IPV6_LOOPBACK);
177+
test_tcp_bind_specific_port(IpAddress::IPV4_UNSPECIFIED);
178+
test_tcp_bind_specific_port(IpAddress::IPV6_UNSPECIFIED);
179+
180+
test_tcp_bind_reuseaddr(IpAddress::IPV4_LOOPBACK).await;
181+
test_tcp_bind_reuseaddr(IpAddress::IPV6_LOOPBACK).await;
182+
183+
test_tcp_bind_addrinuse(IpAddress::IPV4_LOOPBACK);
184+
test_tcp_bind_addrinuse(IpAddress::IPV6_LOOPBACK);
185+
test_tcp_bind_addrinuse(IpAddress::IPV4_UNSPECIFIED);
186+
test_tcp_bind_addrinuse(IpAddress::IPV6_UNSPECIFIED);
187+
188+
test_tcp_bind_addrnotavail(RESERVED_IPV4_ADDRESS);
189+
test_tcp_bind_addrnotavail(RESERVED_IPV6_ADDRESS);
190+
191+
test_tcp_bind_wrong_family(IpAddressFamily::Ipv4);
192+
test_tcp_bind_wrong_family(IpAddressFamily::Ipv6);
193+
194+
test_tcp_bind_non_unicast();
195+
196+
test_tcp_bind_dual_stack();
197+
198+
Ok(())
199+
}
200+
}
201+
202+
fn main() {}

0 commit comments

Comments
 (0)