Skip to content

Commit 9299efd

Browse files
committed
Implement the wasi:sockets/ip-name-lookup interface
This commit is an initial implementation of the new `ip-name-lookup` interface from the `wasi-sockets` proposal. The intention is to get a sketch of what an implementation would look like for the preview2 release later this year. Notable features of this implementation are: * Name lookups are disabled by default and must be explicitly enabled in `WasiCtx`. This seems like a reasonable default for now while the full set of configuration around this is settled. * I've added new "typed" methods to `preview2::Table` to avoid the need for extra helpers when using resources. * A new `-Sallow-ip-name-lookup` option is added to control this on the CLI. * Implementation-wise this uses the blocking resolution in the Rust standard library, built on `getaddrinfo`. This doesn't invoke `getaddrinfo` "raw", however, so information such as error details can be lost in translation. This will probably need to change in the future. Closes #7070
1 parent b76a61f commit 9299efd

File tree

19 files changed

+307
-39
lines changed

19 files changed

+307
-39
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli-flags/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,8 @@ wasmtime_option_group! {
242242
/// Flag for WASI preview2 to inherit the host's network within the
243243
/// guest so it has full access to all addresses/ports/etc.
244244
pub inherit_network: Option<bool>,
245+
/// Indicates whether `wasi:sockets/ip-name-lookup` is enabled or not.
246+
pub allow_ip_name_lookup: Option<bool>,
245247

246248
}
247249

crates/test-programs/tests/wasi-sockets.rs

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ async fn run(name: &str) -> anyhow::Result<()> {
5252
let wasi = WasiCtxBuilder::new()
5353
.inherit_stdio()
5454
.inherit_network(ambient_authority())
55+
.allow_ip_name_lookup(true)
5556
.arg(name)
5657
.build();
5758

@@ -74,3 +75,8 @@ async fn tcp_v4() {
7475
async fn tcp_v6() {
7576
run("tcp_v6").await.unwrap();
7677
}
78+
79+
#[test_log::test(tokio::test(flavor = "multi_thread"))]
80+
async fn ip_name_lookup() {
81+
run("ip_name_lookup").await.unwrap();
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use wasi_sockets_tests::wasi::clocks::*;
2+
use wasi_sockets_tests::wasi::io::*;
3+
use wasi_sockets_tests::wasi::sockets::*;
4+
5+
fn main() {
6+
let network = instance_network::instance_network();
7+
8+
let addresses =
9+
ip_name_lookup::resolve_addresses(&network, "example.com", None, false).unwrap();
10+
let pollable = addresses.subscribe();
11+
poll::poll_one(&pollable);
12+
assert!(addresses.resolve_next_address().is_ok());
13+
14+
let addresses = ip_name_lookup::resolve_addresses(&network, "a.b<&>", None, false).unwrap();
15+
let pollable = addresses.subscribe();
16+
poll::poll_one(&pollable);
17+
assert!(addresses.resolve_next_address().is_err());
18+
19+
// Try resolving a valid address and ensure that it eventually terminates.
20+
// To help prevent this test from being flaky this additionally times out
21+
// the resolution and allows errors.
22+
let addresses = ip_name_lookup::resolve_addresses(&network, "github.com", None, false).unwrap();
23+
let lookup = addresses.subscribe();
24+
let timeout = monotonic_clock::subscribe(1_000_000_000, false);
25+
let ready = poll::poll_list(&[&lookup, &timeout]);
26+
assert!(ready.len() > 0);
27+
match ready[0] {
28+
0 => loop {
29+
match addresses.resolve_next_address() {
30+
Ok(Some(_)) => {}
31+
Ok(None) => break,
32+
Err(_) => break,
33+
}
34+
},
35+
1 => {}
36+
_ => unreachable!(),
37+
}
38+
}

crates/wasi-http/wit/test.wit

+2
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ world test-command-with-sockets {
3939
import wasi:sockets/tcp-create-socket
4040
import wasi:sockets/network
4141
import wasi:sockets/instance-network
42+
import wasi:sockets/ip-name-lookup
43+
import wasi:clocks/monotonic-clock
4244
}

crates/wasi/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ wasi-tokio = { workspace = true, optional = true }
2121
wiggle = { workspace = true, optional = true }
2222
libc = { workspace = true }
2323
once_cell = { workspace = true }
24+
log = { workspace = true }
2425

2526
tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] }
2627
bytes = { workspace = true }

crates/wasi/src/preview2/command.rs

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub fn add_to_linker<T: WasiView + Sync>(
5656
crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?;
5757
crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?;
5858
crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?;
59+
crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?;
5960
Ok(())
6061
}
6162

@@ -118,6 +119,7 @@ pub mod sync {
118119
crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?;
119120
crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?;
120121
crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?;
122+
crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?;
121123
Ok(())
122124
}
123125
}

crates/wasi/src/preview2/ctx.rs

+28-9
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct WasiCtxBuilder {
2727
insecure_random_seed: u128,
2828
wall_clock: Box<dyn HostWallClock + Send + Sync>,
2929
monotonic_clock: Box<dyn HostMonotonicClock + Send + Sync>,
30+
allow_ip_name_lookup: bool,
3031
built: bool,
3132
}
3233

@@ -76,6 +77,7 @@ impl WasiCtxBuilder {
7677
insecure_random_seed,
7778
wall_clock: wall_clock(),
7879
monotonic_clock: monotonic_clock(),
80+
allow_ip_name_lookup: false,
7981
built: false,
8082
}
8183
}
@@ -201,21 +203,27 @@ impl WasiCtxBuilder {
201203
}
202204

203205
/// Add network addresses to the pool.
204-
pub fn insert_addr<A: cap_std::net::ToSocketAddrs>(&mut self, addrs: A) -> std::io::Result<()> {
205-
self.pool.insert(addrs, ambient_authority())
206+
pub fn insert_addr<A: cap_std::net::ToSocketAddrs>(
207+
&mut self,
208+
addrs: A,
209+
) -> std::io::Result<&mut Self> {
210+
self.pool.insert(addrs, ambient_authority())?;
211+
Ok(self)
206212
}
207213

208214
/// Add a specific [`cap_std::net::SocketAddr`] to the pool.
209-
pub fn insert_socket_addr(&mut self, addr: cap_std::net::SocketAddr) {
215+
pub fn insert_socket_addr(&mut self, addr: cap_std::net::SocketAddr) -> &mut Self {
210216
self.pool.insert_socket_addr(addr, ambient_authority());
217+
self
211218
}
212219

213220
/// Add a range of network addresses, accepting any port, to the pool.
214221
///
215222
/// Unlike `insert_ip_net`, this function grants access to any requested port.
216-
pub fn insert_ip_net_port_any(&mut self, ip_net: ipnet::IpNet) {
223+
pub fn insert_ip_net_port_any(&mut self, ip_net: ipnet::IpNet) -> &mut Self {
217224
self.pool
218-
.insert_ip_net_port_any(ip_net, ambient_authority())
225+
.insert_ip_net_port_any(ip_net, ambient_authority());
226+
self
219227
}
220228

221229
/// Add a range of network addresses, accepting a range of ports, to
@@ -228,14 +236,22 @@ impl WasiCtxBuilder {
228236
ip_net: ipnet::IpNet,
229237
ports_start: u16,
230238
ports_end: Option<u16>,
231-
) {
239+
) -> &mut Self {
232240
self.pool
233-
.insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority())
241+
.insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority());
242+
self
234243
}
235244

236245
/// Add a range of network addresses with a specific port to the pool.
237-
pub fn insert_ip_net(&mut self, ip_net: ipnet::IpNet, port: u16) {
238-
self.pool.insert_ip_net(ip_net, port, ambient_authority())
246+
pub fn insert_ip_net(&mut self, ip_net: ipnet::IpNet, port: u16) -> &mut Self {
247+
self.pool.insert_ip_net(ip_net, port, ambient_authority());
248+
self
249+
}
250+
251+
/// Allow usage of `wasi:sockets/ip-name-lookup`
252+
pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self {
253+
self.allow_ip_name_lookup = enable;
254+
self
239255
}
240256

241257
/// Uses the configured context so far to construct the final `WasiCtx`.
@@ -264,6 +280,7 @@ impl WasiCtxBuilder {
264280
insecure_random_seed,
265281
wall_clock,
266282
monotonic_clock,
283+
allow_ip_name_lookup,
267284
built: _,
268285
} = mem::replace(self, Self::new());
269286
self.built = true;
@@ -281,6 +298,7 @@ impl WasiCtxBuilder {
281298
insecure_random_seed,
282299
wall_clock,
283300
monotonic_clock,
301+
allow_ip_name_lookup,
284302
}
285303
}
286304
}
@@ -305,4 +323,5 @@ pub struct WasiCtx {
305323
pub(crate) stdout: Box<dyn StdoutStream>,
306324
pub(crate) stderr: Box<dyn StdoutStream>,
307325
pub(crate) pool: Pool,
326+
pub(crate) allow_ip_name_lookup: bool,
308327
}

crates/wasi/src/preview2/host/instance_network.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use wasmtime::component::Resource;
55

66
impl<T: WasiView> instance_network::Host for T {
77
fn instance_network(&mut self) -> Result<Resource<Network>, anyhow::Error> {
8-
let network = HostNetworkState::new(self.ctx().pool.clone());
8+
let network = HostNetworkState {
9+
pool: self.ctx().pool.clone(),
10+
allow_ip_name_lookup: self.ctx().allow_ip_name_lookup,
11+
};
912
let network = self.table_mut().push_network(network)?;
1013
Ok(network)
1114
}

crates/wasi/src/preview2/host/io.rs

+4-8
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ use crate::preview2::{
1010
HostPollable, TableError, TablePollableExt, WasiView,
1111
};
1212
use std::any::Any;
13-
use std::future::Future;
14-
use std::pin::Pin;
15-
use std::task::{Context, Poll};
1613
use wasmtime::component::Resource;
1714

1815
impl From<StreamState> for streams::StreamStatus {
@@ -55,11 +52,10 @@ impl<T: WasiView + Sync> streams::HostOutputStream for T {
5552
fn check_write(&mut self, stream: Resource<OutputStream>) -> Result<u64, streams::Error> {
5653
let s = self.table_mut().get_output_stream_mut(&stream)?;
5754
let mut ready = s.write_ready();
58-
let mut task = Context::from_waker(futures::task::noop_waker_ref());
59-
match Pin::new(&mut ready).poll(&mut task) {
60-
Poll::Ready(Ok(permit)) => Ok(permit as u64),
61-
Poll::Ready(Err(e)) => Err(e.into()),
62-
Poll::Pending => Ok(0),
55+
match crate::preview2::poll_noop(&mut ready) {
56+
Some(Ok(permit)) => Ok(permit as u64),
57+
Some(Err(e)) => Err(e.into()),
58+
None => Ok(0),
6359
}
6460
}
6561

crates/wasi/src/preview2/host/network.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ impl From<io::Error> for network::Error {
6868
Some(libc::EADDRINUSE) => ErrorCode::AddressInUse,
6969
Some(_) => return Self::trap(error.into()),
7070
},
71-
_ => return Self::trap(error.into()),
71+
72+
_ => {
73+
log::debug!("unknown I/O error: {error}");
74+
ErrorCode::Unknown
75+
}
7276
}
7377
.into()
7478
}

crates/wasi/src/preview2/host/tcp.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
3636
}
3737

3838
let network = table.get_network(&network)?;
39-
let binder = network.0.tcp_binder(local_address)?;
39+
let binder = network.pool.tcp_binder(local_address)?;
4040

4141
// Perform the OS bind call.
4242
binder.bind_existing_tcp_listener(
@@ -80,7 +80,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
8080
}
8181

8282
let network = table.get_network(&network)?;
83-
let connecter = network.0.tcp_connecter(remote_address)?;
83+
let connecter = network.pool.tcp_connecter(remote_address)?;
8484

8585
// Do an OS `connect`. Our socket is non-blocking, so it'll either...
8686
{
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use crate::preview2::bindings::io::poll::Pollable;
2+
use crate::preview2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
3+
use crate::preview2::bindings::sockets::network::{
4+
Error, ErrorCode, IpAddress, IpAddressFamily, Network,
5+
};
6+
use crate::preview2::network::TableNetworkExt;
7+
use crate::preview2::poll::{Subscribe, TablePollableExt};
8+
use crate::preview2::{AbortOnDropJoinHandle, WasiView};
9+
use anyhow::Result;
10+
use std::io;
11+
use std::mem;
12+
use std::net::{SocketAddr, ToSocketAddrs};
13+
use std::vec;
14+
use wasmtime::component::Resource;
15+
16+
pub enum ResolveAddressStream {
17+
Waiting(AbortOnDropJoinHandle<io::Result<Vec<IpAddress>>>),
18+
Done(io::Result<vec::IntoIter<IpAddress>>),
19+
}
20+
21+
#[async_trait::async_trait]
22+
impl<T: WasiView> Host for T {
23+
fn resolve_addresses(
24+
&mut self,
25+
network: Resource<Network>,
26+
name: String,
27+
family: Option<IpAddressFamily>,
28+
include_unavailable: bool,
29+
) -> Result<Resource<ResolveAddressStream>, Error> {
30+
if !self.table().get_network(&network)?.allow_ip_name_lookup {
31+
return Err(ErrorCode::PermanentResolverFailure.into());
32+
}
33+
34+
// ignored for now, should probably have a future PR to actually take
35+
// this into account. This would require invoking `getaddrinfo` directly
36+
// rather than using the standard library to do it for us.
37+
let _ = include_unavailable;
38+
39+
// For now use the standard library to perform actual resolution through
40+
// the usage of the `ToSocketAddrs` trait. This blocks the current
41+
// thread, so use `spawn_blocking`. Finally note that this is only
42+
// resolving names, not ports, so force the port to be 0.
43+
let task = tokio::task::spawn_blocking(move || -> io::Result<Vec<_>> {
44+
let result = (name.as_str(), 0).to_socket_addrs()?;
45+
Ok(result
46+
.filter_map(|addr| {
47+
// In lieu of preventing these addresses from being resolved
48+
// in the first place, filter them out here.
49+
match addr {
50+
SocketAddr::V4(addr) => match family {
51+
None | Some(IpAddressFamily::Ipv4) => {
52+
let [a, b, c, d] = addr.ip().octets();
53+
Some(IpAddress::Ipv4((a, b, c, d)))
54+
}
55+
Some(IpAddressFamily::Ipv6) => None,
56+
},
57+
SocketAddr::V6(addr) => match family {
58+
None | Some(IpAddressFamily::Ipv6) => {
59+
let [a, b, c, d, e, f, g, h] = addr.ip().segments();
60+
Some(IpAddress::Ipv6((a, b, c, d, e, f, g, h)))
61+
}
62+
Some(IpAddressFamily::Ipv4) => None,
63+
},
64+
}
65+
})
66+
.collect())
67+
});
68+
let task = AbortOnDropJoinHandle(task);
69+
let resource = self
70+
.table_mut()
71+
.push_resource(ResolveAddressStream::Waiting(task))?;
72+
Ok(resource)
73+
}
74+
}
75+
76+
#[async_trait::async_trait]
77+
impl<T: WasiView> HostResolveAddressStream for T {
78+
fn resolve_next_address(
79+
&mut self,
80+
resource: Resource<ResolveAddressStream>,
81+
) -> Result<Option<IpAddress>, Error> {
82+
let stream = self.table_mut().get_resource_mut(&resource)?;
83+
loop {
84+
match stream {
85+
ResolveAddressStream::Waiting(future) => match crate::preview2::poll_noop(future) {
86+
Some(result) => {
87+
*stream = ResolveAddressStream::Done(result.map(|v| v.into_iter()));
88+
}
89+
None => return Err(ErrorCode::WouldBlock.into()),
90+
},
91+
ResolveAddressStream::Done(slot @ Err(_)) => {
92+
// TODO: this `?` is what converts `io::Error` into `Error`
93+
// and the conversion is not great right now. The standard
94+
// library doesn't expose a ton of information through the
95+
// return value of `getaddrinfo` right now so supporting a
96+
// richer conversion here will probably require calling
97+
// `getaddrinfo` directly.
98+
mem::replace(slot, Ok(Vec::new().into_iter()))?;
99+
unreachable!();
100+
}
101+
ResolveAddressStream::Done(Ok(iter)) => return Ok(iter.next()),
102+
}
103+
}
104+
}
105+
106+
fn subscribe(
107+
&mut self,
108+
resource: Resource<ResolveAddressStream>,
109+
) -> Result<Resource<Pollable>> {
110+
Ok(self.table_mut().push_host_pollable_resource(&resource)?)
111+
}
112+
113+
fn drop(&mut self, resource: Resource<ResolveAddressStream>) -> Result<()> {
114+
self.table_mut().delete_resource(resource)?;
115+
Ok(())
116+
}
117+
}
118+
119+
#[async_trait::async_trait]
120+
impl Subscribe for ResolveAddressStream {
121+
async fn ready(&mut self) -> Result<()> {
122+
if let ResolveAddressStream::Waiting(future) = self {
123+
*self = ResolveAddressStream::Done(future.await.map(|v| v.into_iter()));
124+
}
125+
Ok(())
126+
}
127+
}

0 commit comments

Comments
 (0)