diff --git a/Cargo.lock b/Cargo.lock index 34ffadaa31a2..ebde4e29ce0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3751,6 +3751,7 @@ dependencies = [ "io-extras", "io-lifetimes 2.0.2", "libc", + "log", "once_cell", "rustix 0.38.8", "system-interface", @@ -3759,6 +3760,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "url", "wasi-cap-std-sync", "wasi-common", "wasi-tokio", diff --git a/Cargo.toml b/Cargo.toml index ddaffc7d5889..d4cc8fc06a95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -270,6 +270,7 @@ pretty_env_logger = "0.5.0" syn = "2.0.25" test-log = { version = "0.2", default-features = false, features = ["trace"] } tracing-subscriber = { version = "0.3.1", default-features = false, features = ['fmt', 'env-filter'] } +url = "2.3.1" [features] default = [ diff --git a/crates/cli-flags/src/lib.rs b/crates/cli-flags/src/lib.rs index 6e53576d3ee4..db5ba3abc300 100644 --- a/crates/cli-flags/src/lib.rs +++ b/crates/cli-flags/src/lib.rs @@ -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, + /// Indicates whether `wasi:sockets/ip-name-lookup` is enabled or not. + pub allow_ip_name_lookup: Option, } diff --git a/crates/test-programs/tests/wasi-sockets.rs b/crates/test-programs/tests/wasi-sockets.rs index 71669817bce6..8484a4f98f77 100644 --- a/crates/test-programs/tests/wasi-sockets.rs +++ b/crates/test-programs/tests/wasi-sockets.rs @@ -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(); @@ -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(); +} 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 new file mode 100644 index 000000000000..50932e057c65 --- /dev/null +++ b/crates/test-programs/wasi-sockets-tests/src/bin/ip_name_lookup.rs @@ -0,0 +1,36 @@ +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 result = ip_name_lookup::resolve_addresses(&network, "a.b<&>", None, false); + assert!(matches!(result, Err(network::ErrorCode::InvalidName))); + + // 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!(), + } +} diff --git a/crates/wasi-http/wit/test.wit b/crates/wasi-http/wit/test.wit index 0b6bd28e997d..03073513f8e6 100644 --- a/crates/wasi-http/wit/test.wit +++ b/crates/wasi-http/wit/test.wit @@ -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 } diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml index 2ef0c8d67c30..1582ddfc18b1 100644 --- a/crates/wasi/Cargo.toml +++ b/crates/wasi/Cargo.toml @@ -21,6 +21,8 @@ wasi-tokio = { workspace = true, optional = true } wiggle = { workspace = true, optional = true } libc = { workspace = true } once_cell = { workspace = true } +log = { workspace = true } +url = { workspace = true } tokio = { workspace = true, optional = true, features = ["time", "sync", "io-std", "io-util", "rt", "rt-multi-thread", "net"] } bytes = { workspace = true } diff --git a/crates/wasi/src/preview2/command.rs b/crates/wasi/src/preview2/command.rs index f83dce6b20cf..7702e2a706da 100644 --- a/crates/wasi/src/preview2/command.rs +++ b/crates/wasi/src/preview2/command.rs @@ -54,6 +54,7 @@ pub fn add_to_linker(l: &mut wasmtime::component::Linker) -> any 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(()) } @@ -116,6 +117,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(()) } } diff --git a/crates/wasi/src/preview2/ctx.rs b/crates/wasi/src/preview2/ctx.rs index 95c0f2eae41d..344a4a3c12da 100644 --- a/crates/wasi/src/preview2/ctx.rs +++ b/crates/wasi/src/preview2/ctx.rs @@ -27,6 +27,7 @@ pub struct WasiCtxBuilder { insecure_random_seed: u128, wall_clock: Box, monotonic_clock: Box, + allow_ip_name_lookup: bool, built: bool, } @@ -76,6 +77,7 @@ impl WasiCtxBuilder { insecure_random_seed, wall_clock: wall_clock(), monotonic_clock: monotonic_clock(), + allow_ip_name_lookup: false, built: false, } } @@ -201,21 +203,27 @@ impl WasiCtxBuilder { } /// Add network addresses to the pool. - pub fn insert_addr(&mut self, addrs: A) -> std::io::Result<()> { - self.pool.insert(addrs, ambient_authority()) + pub fn insert_addr( + &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 @@ -228,14 +236,22 @@ impl WasiCtxBuilder { ip_net: ipnet::IpNet, ports_start: u16, ports_end: Option, - ) { + ) -> &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`. @@ -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; @@ -281,6 +298,7 @@ impl WasiCtxBuilder { insecure_random_seed, wall_clock, monotonic_clock, + allow_ip_name_lookup, } } } @@ -305,4 +323,5 @@ pub struct WasiCtx { pub(crate) stdout: Box, pub(crate) stderr: Box, pub(crate) pool: Pool, + pub(crate) allow_ip_name_lookup: bool, } diff --git a/crates/wasi/src/preview2/filesystem.rs b/crates/wasi/src/preview2/filesystem.rs index 488ec93b1f89..9b8467a86ed3 100644 --- a/crates/wasi/src/preview2/filesystem.rs +++ b/crates/wasi/src/preview2/filesystem.rs @@ -1,5 +1,7 @@ use crate::preview2::bindings::filesystem::types; -use crate::preview2::{AbortOnDropJoinHandle, HostOutputStream, StreamError, Subscribe}; +use crate::preview2::{ + spawn_blocking, AbortOnDropJoinHandle, HostOutputStream, StreamError, Subscribe, +}; use anyhow::anyhow; use bytes::{Bytes, BytesMut}; use std::io; @@ -74,7 +76,7 @@ impl File { R: Send + 'static, { let f = self.file.clone(); - tokio::task::spawn_blocking(move || body(&f)).await.unwrap() + spawn_blocking(move || body(&f)).await } } @@ -110,7 +112,7 @@ impl Dir { R: Send + 'static, { let d = self.dir.clone(); - tokio::task::spawn_blocking(move || body(&d)).await.unwrap() + spawn_blocking(move || body(&d)).await } } @@ -127,13 +129,12 @@ impl FileInputStream { use system_interface::fs::FileIoExt; let f = Arc::clone(&self.file); let p = self.position; - let (r, mut buf) = tokio::task::spawn_blocking(move || { + let (r, mut buf) = spawn_blocking(move || { let mut buf = BytesMut::zeroed(size); let r = f.read_at(&mut buf, p); (r, buf) }) - .await - .unwrap(); + .await; let n = read_result(r)?; buf.truncate(n); self.position += n as u64; @@ -213,7 +214,7 @@ impl HostOutputStream for FileOutputStream { let f = Arc::clone(&self.file); let m = self.mode; - let task = AbortOnDropJoinHandle::from(tokio::task::spawn_blocking(move || match m { + let task = spawn_blocking(move || match m { FileOutputMode::Position(mut p) => { let mut buf = buf; while !buf.is_empty() { @@ -232,7 +233,7 @@ impl HostOutputStream for FileOutputStream { } Ok(()) } - })); + }); self.state = OutputState::Waiting(task); Ok(()) } diff --git a/crates/wasi/src/preview2/host/instance_network.rs b/crates/wasi/src/preview2/host/instance_network.rs index 02bf5e8a4014..627cc359f6d2 100644 --- a/crates/wasi/src/preview2/host/instance_network.rs +++ b/crates/wasi/src/preview2/host/instance_network.rs @@ -5,7 +5,10 @@ use wasmtime::component::Resource; impl instance_network::Host for T { fn instance_network(&mut self) -> Result, anyhow::Error> { - let network = Network::new(self.ctx().pool.clone()); + let network = Network { + pool: self.ctx().pool.clone(), + allow_ip_name_lookup: self.ctx().allow_ip_name_lookup, + }; let network = self.table_mut().push_resource(network)?; Ok(network) } diff --git a/crates/wasi/src/preview2/host/network.rs b/crates/wasi/src/preview2/host/network.rs index d1aa52d7fd05..811213050e8e 100644 --- a/crates/wasi/src/preview2/host/network.rs +++ b/crates/wasi/src/preview2/host/network.rs @@ -67,7 +67,11 @@ impl From 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() } diff --git a/crates/wasi/src/preview2/host/tcp.rs b/crates/wasi/src/preview2/host/tcp.rs index 479ae44dc63c..6565a589413a 100644 --- a/crates/wasi/src/preview2/host/tcp.rs +++ b/crates/wasi/src/preview2/host/tcp.rs @@ -31,7 +31,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } let network = table.get_resource(&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( @@ -75,7 +75,7 @@ impl crate::preview2::host::tcp::tcp::HostTcpSocket for T { } let network = table.get_resource(&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... { diff --git a/crates/wasi/src/preview2/ip_name_lookup.rs b/crates/wasi/src/preview2/ip_name_lookup.rs new file mode 100644 index 000000000000..b4a02427b475 --- /dev/null +++ b/crates/wasi/src/preview2/ip_name_lookup.rs @@ -0,0 +1,137 @@ +use crate::preview2::bindings::sockets::ip_name_lookup::{Host, HostResolveAddressStream}; +use crate::preview2::bindings::sockets::network::{ + Error, ErrorCode, IpAddress, IpAddressFamily, Network, +}; +use crate::preview2::poll::{subscribe, Pollable, Subscribe}; +use crate::preview2::{spawn_blocking, AbortOnDropJoinHandle, WasiView}; +use anyhow::Result; +use std::io; +use std::mem; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::pin::Pin; +use std::vec; +use wasmtime::component::Resource; + +pub enum ResolveAddressStream { + Waiting(AbortOnDropJoinHandle>>), + Done(io::Result>), +} + +#[async_trait::async_trait] +impl Host for T { + fn resolve_addresses( + &mut self, + network: Resource, + name: String, + family: Option, + include_unavailable: bool, + ) -> Result, Error> { + let network = self.table().get_resource(&network)?; + + // `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)? { + url::Host::Domain(name) => name, + url::Host::Ipv4(_) => return Err(ErrorCode::InvalidName.into()), + url::Host::Ipv6(_) => return Err(ErrorCode::InvalidName.into()), + }; + + if !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 = spawn_blocking(move || -> io::Result> { + 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 resource = self + .table_mut() + .push_resource(ResolveAddressStream::Waiting(task))?; + Ok(resource) + } +} + +#[async_trait::async_trait] +impl HostResolveAddressStream for T { + fn resolve_next_address( + &mut self, + resource: Resource, + ) -> Result, Error> { + let stream = self.table_mut().get_resource_mut(&resource)?; + loop { + match stream { + ResolveAddressStream::Waiting(future) => { + match crate::preview2::poll_noop(Pin::new(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, + ) -> Result> { + subscribe(self.table_mut(), resource) + } + + fn drop(&mut self, resource: Resource) -> Result<()> { + self.table_mut().delete_resource(resource)?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl Subscribe for ResolveAddressStream { + async fn ready(&mut self) { + if let ResolveAddressStream::Waiting(future) = self { + *self = ResolveAddressStream::Done(future.await.map(|v| v.into_iter())); + } + } +} diff --git a/crates/wasi/src/preview2/mod.rs b/crates/wasi/src/preview2/mod.rs index 78b436afe38a..851c0d1a15c4 100644 --- a/crates/wasi/src/preview2/mod.rs +++ b/crates/wasi/src/preview2/mod.rs @@ -25,6 +25,7 @@ mod ctx; mod error; mod filesystem; mod host; +mod ip_name_lookup; mod network; pub mod pipe; mod poll; @@ -142,6 +143,9 @@ pub mod bindings { "poll-one", ], }, + with: { + "wasi:sockets/ip-name-lookup/resolve-address-stream": super::ip_name_lookup::ResolveAddressStream, + }, trappable_error_type: { "wasi:io/streams"::"stream-error": Error, "wasi:filesystem/types"::"error-code": Error, @@ -204,18 +208,21 @@ impl Future for AbortOnDropJoinHandle { } } -pub fn spawn(f: F) -> AbortOnDropJoinHandle +pub fn spawn(f: F) -> AbortOnDropJoinHandle where - F: Future + Send + 'static, - G: Send + 'static, + F: Future + Send + 'static, + F::Output: Send + 'static, { - let j = match tokio::runtime::Handle::try_current() { - Ok(_) => tokio::task::spawn(f), - Err(_) => { - let _enter = RUNTIME.enter(); - tokio::task::spawn(f) - } - }; + let j = with_ambient_tokio_runtime(|| tokio::task::spawn(f)); + AbortOnDropJoinHandle(j) +} + +pub fn spawn_blocking(f: F) -> AbortOnDropJoinHandle +where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, +{ + let j = with_ambient_tokio_runtime(|| tokio::task::spawn_blocking(f)); AbortOnDropJoinHandle(j) } diff --git a/crates/wasi/src/preview2/network.rs b/crates/wasi/src/preview2/network.rs index f2f7bfd7de6e..614130392d29 100644 --- a/crates/wasi/src/preview2/network.rs +++ b/crates/wasi/src/preview2/network.rs @@ -1,9 +1,6 @@ use cap_std::net::Pool; -pub struct Network(pub(crate) Pool); - -impl Network { - pub fn new(pool: Pool) -> Self { - Self(pool) - } +pub struct Network { + pub pool: Pool, + pub allow_ip_name_lookup: bool, } diff --git a/crates/wasi/wit/test.wit b/crates/wasi/wit/test.wit index 0b6bd28e997d..03073513f8e6 100644 --- a/crates/wasi/wit/test.wit +++ b/crates/wasi/wit/test.wit @@ -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 } diff --git a/src/commands/run.rs b/src/commands/run.rs index a75dfdb6c2a1..2af92f6ddf24 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -936,6 +936,9 @@ impl RunCommand { if self.common.wasi.inherit_network == Some(true) { builder.inherit_network(ambient_authority()); } + if let Some(enable) = self.common.wasi.allow_ip_name_lookup { + builder.allow_ip_name_lookup(enable); + } store.data_mut().preview2_ctx = Some(Arc::new(builder.build())); Ok(())