Skip to content

Commit

Permalink
Implement the wasi:sockets/ip-name-lookup interface
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alexcrichton committed Sep 28, 2023
1 parent b76a61f commit 9299efd
Show file tree
Hide file tree
Showing 19 changed files with 307 additions and 39 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/cli-flags/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,8 @@ wasmtime_option_group! {
/// Flag for WASI preview2 to inherit the host's network within the
/// guest so it has full access to all addresses/ports/etc.
pub inherit_network: Option<bool>,
/// Indicates whether `wasi:sockets/ip-name-lookup` is enabled or not.
pub allow_ip_name_lookup: Option<bool>,

}

Expand Down
6 changes: 6 additions & 0 deletions crates/test-programs/tests/wasi-sockets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ async fn run(name: &str) -> anyhow::Result<()> {
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.inherit_network(ambient_authority())
.allow_ip_name_lookup(true)
.arg(name)
.build();

Expand All @@ -74,3 +75,8 @@ async fn tcp_v4() {
async fn tcp_v6() {
run("tcp_v6").await.unwrap();
}

#[test_log::test(tokio::test(flavor = "multi_thread"))]
async fn ip_name_lookup() {
run("ip_name_lookup").await.unwrap();
}
38 changes: 38 additions & 0 deletions crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use wasi_sockets_tests::wasi::clocks::*;
use wasi_sockets_tests::wasi::io::*;
use wasi_sockets_tests::wasi::sockets::*;

fn main() {
let network = instance_network::instance_network();

let addresses =
ip_name_lookup::resolve_addresses(&network, "example.com", None, false).unwrap();
let pollable = addresses.subscribe();
poll::poll_one(&pollable);
assert!(addresses.resolve_next_address().is_ok());

let addresses = ip_name_lookup::resolve_addresses(&network, "a.b<&>", None, false).unwrap();
let pollable = addresses.subscribe();
poll::poll_one(&pollable);
assert!(addresses.resolve_next_address().is_err());

// Try resolving a valid address and ensure that it eventually terminates.
// To help prevent this test from being flaky this additionally times out
// the resolution and allows errors.
let addresses = ip_name_lookup::resolve_addresses(&network, "github.com", None, false).unwrap();
let lookup = addresses.subscribe();
let timeout = monotonic_clock::subscribe(1_000_000_000, false);
let ready = poll::poll_list(&[&lookup, &timeout]);
assert!(ready.len() > 0);
match ready[0] {
0 => loop {
match addresses.resolve_next_address() {
Ok(Some(_)) => {}
Ok(None) => break,
Err(_) => break,
}
},
1 => {}
_ => unreachable!(),
}
}
2 changes: 2 additions & 0 deletions crates/wasi-http/wit/test.wit
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ world test-command-with-sockets {
import wasi:sockets/tcp-create-socket
import wasi:sockets/network
import wasi:sockets/instance-network
import wasi:sockets/ip-name-lookup
import wasi:clocks/monotonic-clock
}
1 change: 1 addition & 0 deletions crates/wasi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ wasi-tokio = { workspace = true, optional = true }
wiggle = { workspace = true, optional = true }
libc = { workspace = true }
once_cell = { workspace = true }
log = { workspace = true }

tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] }
bytes = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions crates/wasi/src/preview2/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub fn add_to_linker<T: WasiView + Sync>(
crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?;
Ok(())
}

Expand Down Expand Up @@ -118,6 +119,7 @@ pub mod sync {
crate::preview2::bindings::sockets::tcp_create_socket::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::instance_network::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::network::add_to_linker(l, |t| t)?;
crate::preview2::bindings::sockets::ip_name_lookup::add_to_linker(l, |t| t)?;
Ok(())
}
}
37 changes: 28 additions & 9 deletions crates/wasi/src/preview2/ctx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct WasiCtxBuilder {
insecure_random_seed: u128,
wall_clock: Box<dyn HostWallClock + Send + Sync>,
monotonic_clock: Box<dyn HostMonotonicClock + Send + Sync>,
allow_ip_name_lookup: bool,
built: bool,
}

Expand Down Expand Up @@ -76,6 +77,7 @@ impl WasiCtxBuilder {
insecure_random_seed,
wall_clock: wall_clock(),
monotonic_clock: monotonic_clock(),
allow_ip_name_lookup: false,
built: false,
}
}
Expand Down Expand Up @@ -201,21 +203,27 @@ impl WasiCtxBuilder {
}

/// Add network addresses to the pool.
pub fn insert_addr<A: cap_std::net::ToSocketAddrs>(&mut self, addrs: A) -> std::io::Result<()> {
self.pool.insert(addrs, ambient_authority())
pub fn insert_addr<A: cap_std::net::ToSocketAddrs>(
&mut self,
addrs: A,
) -> std::io::Result<&mut Self> {
self.pool.insert(addrs, ambient_authority())?;
Ok(self)
}

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

/// Add a range of network addresses, accepting any port, to the pool.
///
/// Unlike `insert_ip_net`, this function grants access to any requested port.
pub fn insert_ip_net_port_any(&mut self, ip_net: ipnet::IpNet) {
pub fn insert_ip_net_port_any(&mut self, ip_net: ipnet::IpNet) -> &mut Self {
self.pool
.insert_ip_net_port_any(ip_net, ambient_authority())
.insert_ip_net_port_any(ip_net, ambient_authority());
self
}

/// Add a range of network addresses, accepting a range of ports, to
Expand All @@ -228,14 +236,22 @@ impl WasiCtxBuilder {
ip_net: ipnet::IpNet,
ports_start: u16,
ports_end: Option<u16>,
) {
) -> &mut Self {
self.pool
.insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority())
.insert_ip_net_port_range(ip_net, ports_start, ports_end, ambient_authority());
self
}

/// Add a range of network addresses with a specific port to the pool.
pub fn insert_ip_net(&mut self, ip_net: ipnet::IpNet, port: u16) {
self.pool.insert_ip_net(ip_net, port, ambient_authority())
pub fn insert_ip_net(&mut self, ip_net: ipnet::IpNet, port: u16) -> &mut Self {
self.pool.insert_ip_net(ip_net, port, ambient_authority());
self
}

/// Allow usage of `wasi:sockets/ip-name-lookup`
pub fn allow_ip_name_lookup(&mut self, enable: bool) -> &mut Self {
self.allow_ip_name_lookup = enable;
self
}

/// Uses the configured context so far to construct the final `WasiCtx`.
Expand Down Expand Up @@ -264,6 +280,7 @@ impl WasiCtxBuilder {
insecure_random_seed,
wall_clock,
monotonic_clock,
allow_ip_name_lookup,
built: _,
} = mem::replace(self, Self::new());
self.built = true;
Expand All @@ -281,6 +298,7 @@ impl WasiCtxBuilder {
insecure_random_seed,
wall_clock,
monotonic_clock,
allow_ip_name_lookup,
}
}
}
Expand All @@ -305,4 +323,5 @@ pub struct WasiCtx {
pub(crate) stdout: Box<dyn StdoutStream>,
pub(crate) stderr: Box<dyn StdoutStream>,
pub(crate) pool: Pool,
pub(crate) allow_ip_name_lookup: bool,
}
5 changes: 4 additions & 1 deletion crates/wasi/src/preview2/host/instance_network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use wasmtime::component::Resource;

impl<T: WasiView> instance_network::Host for T {
fn instance_network(&mut self) -> Result<Resource<Network>, anyhow::Error> {
let network = HostNetworkState::new(self.ctx().pool.clone());
let network = HostNetworkState {
pool: self.ctx().pool.clone(),
allow_ip_name_lookup: self.ctx().allow_ip_name_lookup,
};
let network = self.table_mut().push_network(network)?;
Ok(network)
}
Expand Down
12 changes: 4 additions & 8 deletions crates/wasi/src/preview2/host/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ use crate::preview2::{
HostPollable, TableError, TablePollableExt, WasiView,
};
use std::any::Any;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use wasmtime::component::Resource;

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

Expand Down
6 changes: 5 additions & 1 deletion crates/wasi/src/preview2/host/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ impl From<io::Error> for network::Error {
Some(libc::EADDRINUSE) => ErrorCode::AddressInUse,
Some(_) => return Self::trap(error.into()),
},
_ => return Self::trap(error.into()),

_ => {
log::debug!("unknown I/O error: {error}");
ErrorCode::Unknown
}
}
.into()
}
Expand Down
4 changes: 2 additions & 2 deletions crates/wasi/src/preview2/host/tcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl<T: WasiView> crate::preview2::host::tcp::tcp::HostTcpSocket for T {
}

let network = table.get_network(&network)?;
let binder = network.0.tcp_binder(local_address)?;
let binder = network.pool.tcp_binder(local_address)?;

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

let network = table.get_network(&network)?;
let connecter = network.0.tcp_connecter(remote_address)?;
let connecter = network.pool.tcp_connecter(remote_address)?;

// Do an OS `connect`. Our socket is non-blocking, so it'll either...
{
Expand Down
127 changes: 127 additions & 0 deletions crates/wasi/src/preview2/ip_name_lookup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use crate::preview2::bindings::io::poll::Pollable;
use crate::preview2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream};
use crate::preview2::bindings::sockets::network::{
Error, ErrorCode, IpAddress, IpAddressFamily, Network,
};
use crate::preview2::network::TableNetworkExt;
use crate::preview2::poll::{Subscribe, TablePollableExt};
use crate::preview2::{AbortOnDropJoinHandle, WasiView};
use anyhow::Result;
use std::io;
use std::mem;
use std::net::{SocketAddr, ToSocketAddrs};
use std::vec;
use wasmtime::component::Resource;

pub enum ResolveAddressStream {
Waiting(AbortOnDropJoinHandle<io::Result<Vec<IpAddress>>>),
Done(io::Result<vec::IntoIter<IpAddress>>),
}

#[async_trait::async_trait]
impl<T: WasiView> Host for T {
fn resolve_addresses(
&mut self,
network: Resource<Network>,
name: String,
family: Option<IpAddressFamily>,
include_unavailable: bool,
) -> Result<Resource<ResolveAddressStream>, Error> {
if !self.table().get_network(&network)?.allow_ip_name_lookup {
return Err(ErrorCode::PermanentResolverFailure.into());
}

// ignored for now, should probably have a future PR to actually take
// this into account. This would require invoking `getaddrinfo` directly
// rather than using the standard library to do it for us.
let _ = include_unavailable;

// For now use the standard library to perform actual resolution through
// 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 = tokio::task::spawn_blocking(move || -> io::Result<Vec<_>> {
let result = (name.as_str(), 0).to_socket_addrs()?;
Ok(result
.filter_map(|addr| {
// In lieu of preventing these addresses from being resolved
// in the first place, filter them out here.
match addr {
SocketAddr::V4(addr) => match family {
None | Some(IpAddressFamily::Ipv4) => {
let [a, b, c, d] = addr.ip().octets();
Some(IpAddress::Ipv4((a, b, c, d)))
}
Some(IpAddressFamily::Ipv6) => None,
},
SocketAddr::V6(addr) => match family {
None | Some(IpAddressFamily::Ipv6) => {
let [a, b, c, d, e, f, g, h] = addr.ip().segments();
Some(IpAddress::Ipv6((a, b, c, d, e, f, g, h)))
}
Some(IpAddressFamily::Ipv4) => None,
},
}
})
.collect())
});
let task = AbortOnDropJoinHandle(task);
let resource = self
.table_mut()
.push_resource(ResolveAddressStream::Waiting(task))?;
Ok(resource)
}
}

#[async_trait::async_trait]
impl<T: WasiView> HostResolveAddressStream for T {
fn resolve_next_address(
&mut self,
resource: Resource<ResolveAddressStream>,
) -> Result<Option<IpAddress>, Error> {
let stream = self.table_mut().get_resource_mut(&resource)?;
loop {
match stream {
ResolveAddressStream::Waiting(future) => match crate::preview2::poll_noop(future) {
Some(result) => {
*stream = ResolveAddressStream::Done(result.map(|v| v.into_iter()));
}
None => return Err(ErrorCode::WouldBlock.into()),
},
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!();
}
ResolveAddressStream::Done(Ok(iter)) => return Ok(iter.next()),
}
}
}

fn subscribe(
&mut self,
resource: Resource<ResolveAddressStream>,
) -> Result<Resource<Pollable>> {
Ok(self.table_mut().push_host_pollable_resource(&resource)?)
}

fn drop(&mut self, resource: Resource<ResolveAddressStream>) -> Result<()> {
self.table_mut().delete_resource(resource)?;
Ok(())
}
}

#[async_trait::async_trait]
impl Subscribe for ResolveAddressStream {
async fn ready(&mut self) -> Result<()> {
if let ResolveAddressStream::Waiting(future) = self {
*self = ResolveAddressStream::Done(future.await.map(|v| v.into_iter()));
}
Ok(())
}
}
Loading

0 comments on commit 9299efd

Please sign in to comment.