Skip to main content

wasmtime_wasi/sockets/
mod.rs

1use crate::runtime::{AbortOnDropJoinHandle, poll_noop};
2use core::fmt;
3use core::future::Future;
4use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
5use core::ops::Deref;
6use rustix::fd::AsFd;
7use rustix::io::Errno;
8use rustix::net::sockopt;
9use std::pin::Pin;
10use std::sync::Arc;
11use std::task::Poll;
12use tracing::debug;
13use wasmtime::component::{HasData, ResourceTable};
14
15pub(crate) mod ip_name_lookup;
16mod tcp;
17mod udp;
18pub use tcp::TcpSocket;
19pub(crate) use tcp::{TcpListenStream, TcpReceiveStream, TcpSendStream};
20pub use udp::UdpSocket;
21
22/// A helper struct which implements [`HasData`] for the `wasi:sockets` APIs.
23///
24/// This can be useful when directly calling `add_to_linker` functions directly,
25/// such as [`wasmtime_wasi::p2::bindings::sockets::tcp::add_to_linker`] as the
26/// `D` type parameter. See [`HasData`] for more information about the type
27/// parameter's purpose.
28///
29/// When using this type you can skip the [`WasiSocketsView`] trait, for
30/// example.
31///
32/// [`wasmtime_wasi::p2::bindings::sockets::tcp::add_to_linker`]: crate::p2::bindings::sockets::tcp::add_to_linker
33///
34/// # Examples
35///
36/// ```
37/// use wasmtime::component::{Linker, ResourceTable};
38/// use wasmtime::{Engine, Result};
39/// use wasmtime_wasi::sockets::*;
40///
41/// struct MyStoreState {
42///     table: ResourceTable,
43///     sockets: WasiSocketsCtx,
44/// }
45///
46/// fn main() -> Result<()> {
47///     let engine = Engine::default();
48///     let mut linker = Linker::new(&engine);
49///
50///     wasmtime_wasi::p2::bindings::sockets::tcp::add_to_linker::<MyStoreState, WasiSockets>(
51///         &mut linker,
52///         |state| WasiSocketsCtxView {
53///             ctx: &mut state.sockets,
54///             table: &mut state.table,
55///         },
56///     )?;
57///     Ok(())
58/// }
59/// ```
60pub struct WasiSockets;
61
62impl HasData for WasiSockets {
63    type Data<'a> = WasiSocketsCtxView<'a>;
64}
65
66#[derive(Clone, Default)]
67pub struct WasiSocketsCtx {
68    pub(crate) socket_addr_check: SocketAddrCheck,
69    pub(crate) allowed_network_uses: AllowedNetworkUses,
70}
71
72pub struct WasiSocketsCtxView<'a> {
73    pub ctx: &'a mut WasiSocketsCtx,
74    pub table: &'a mut ResourceTable,
75}
76
77pub trait WasiSocketsView: Send {
78    fn sockets(&mut self) -> WasiSocketsCtxView<'_>;
79}
80
81#[derive(Copy, Clone, Default)]
82pub(crate) struct AllowedNetworkUses {
83    pub(crate) ip_name_lookup: bool,
84    pub(crate) udp: bool,
85    pub(crate) tcp: bool,
86}
87
88impl AllowedNetworkUses {
89    pub(crate) fn check_allowed_udp(&self) -> std::io::Result<()> {
90        if !self.udp {
91            return Err(std::io::Error::new(
92                std::io::ErrorKind::PermissionDenied,
93                "UDP is not allowed",
94            ));
95        }
96
97        Ok(())
98    }
99
100    pub(crate) fn check_allowed_tcp(&self) -> std::io::Result<()> {
101        if !self.tcp {
102            return Err(std::io::Error::new(
103                std::io::ErrorKind::PermissionDenied,
104                "TCP is not allowed",
105            ));
106        }
107
108        Ok(())
109    }
110}
111
112/// A check that will be called for each socket address that is used of whether the address is permitted.
113#[derive(Clone)]
114pub(crate) struct SocketAddrCheck(
115    Arc<
116        dyn Fn(SocketAddr, SocketAddrUse) -> Pin<Box<dyn Future<Output = bool> + Send + Sync>>
117            + Send
118            + Sync,
119    >,
120);
121
122impl SocketAddrCheck {
123    /// A check that will be called for each socket address that is used.
124    ///
125    /// Returning `true` will permit socket connections to the `SocketAddr`,
126    /// while returning `false` will reject the connection.
127    pub(crate) fn new(
128        f: impl Fn(SocketAddr, SocketAddrUse) -> Pin<Box<dyn Future<Output = bool> + Send + Sync>>
129        + Send
130        + Sync
131        + 'static,
132    ) -> Self {
133        Self(Arc::new(f))
134    }
135
136    pub(crate) async fn check(
137        &self,
138        addr: SocketAddr,
139        reason: SocketAddrUse,
140    ) -> std::io::Result<()> {
141        if (self.0)(addr, reason).await {
142            Ok(())
143        } else {
144            Err(std::io::Error::new(
145                std::io::ErrorKind::PermissionDenied,
146                "An address was not permitted by the socket address check.",
147            ))
148        }
149    }
150}
151
152impl Deref for SocketAddrCheck {
153    type Target = dyn Fn(SocketAddr, SocketAddrUse) -> Pin<Box<dyn Future<Output = bool> + Send + Sync>>
154        + Send
155        + Sync;
156
157    fn deref(&self) -> &Self::Target {
158        self.0.as_ref()
159    }
160}
161
162impl Default for SocketAddrCheck {
163    fn default() -> Self {
164        Self(Arc::new(|_, _| Box::pin(async { false })))
165    }
166}
167
168/// The reason what a socket address is being used for.
169#[derive(Clone, Copy, Debug)]
170pub enum SocketAddrUse {
171    /// Binding TCP socket.
172    ///
173    /// This is invoked for both explicit calls to `bind` as well as implicit
174    /// binds that are about to be performed by the OS as part of
175    /// e.g. `connect` & `listen`.
176    ///
177    /// The address that is passed to the check is the address provided to
178    /// `bind` for explicit binds, or the wildcard address for implicit binds.
179    TcpBind,
180
181    /// Put a TCP socket in listener mode.
182    ///
183    /// If the socket was already bound at the time of the call, the actual
184    /// local address of the socket is passed to the check. If the socket is
185    /// about to be implicitly bound by `listen`, the wildcard address is passed.
186    TcpListen,
187
188    /// Accepting a new client TCP socket.
189    ///
190    /// The address passed to the check is the remote address of the client that
191    /// is being accepted. If the check fails, the client socket will be
192    /// silently dropped before reaching the guest.
193    TcpAccept,
194
195    /// Connecting a TCP socket.
196    ///
197    /// The address passed to the check is the remote address that the socket is
198    /// attempting to connect to.
199    TcpConnect,
200
201    /// Binding UDP socket.
202    ///
203    /// This is invoked for both explicit calls to `bind` as well as implicit
204    /// binds that are about to be performed by the OS as part of
205    /// e.g. `connect` & `send`.
206    ///
207    /// The address that is passed to the check is the address provided to
208    /// `bind` for explicit binds, or the wildcard address for implicit binds.
209    UdpBind,
210
211    /// Sending a datagram on a UDP socket.
212    ///
213    /// The address passed to the check is the remote address that the socket is
214    /// attempting to send to.
215    UdpSend,
216
217    /// Receiving a datagram on a UDP socket.
218    ///
219    /// The address passed to the check is the remote address of the datagram
220    /// that is being received. If the check fails, the datagram will be
221    /// silently dropped before reaching the guest.
222    UdpReceive,
223}
224
225#[derive(Copy, Clone, Eq, PartialEq)]
226pub(crate) enum SocketAddressFamily {
227    Ipv4,
228    Ipv6,
229}
230
231/// A utility type that separates
232/// (1) polling a future for completion and
233/// (2) obtaining the output of a future
234/// into separate operations. This is a common pattern in WASI 0.2.
235pub(crate) enum MaybeReady<T, F = Pin<Box<dyn Future<Output = T> + Send>>> {
236    Pending(F),
237    Ready(T),
238}
239
240pub(crate) type MaybeSpawned<T> = MaybeReady<T, AbortOnDropJoinHandle<T>>;
241
242impl<T> MaybeSpawned<T> {
243    /// Poll the future and attempt to resolve it immediately. If the future is
244    /// not ready yet, it will be moved to a background task.
245    pub(crate) fn poll_or_spawn(fut: impl Future<Output = T> + Send + 'static) -> Self
246    where
247        T: Send + 'static,
248    {
249        let mut fut = Box::pin(fut);
250        match crate::runtime::with_ambient_tokio_runtime(|| poll_noop(fut.as_mut())) {
251            Some(val) => Self::Ready(val),
252            None => Self::new(crate::runtime::spawn(fut)),
253        }
254    }
255}
256
257impl<T, F> MaybeReady<T, F>
258where
259    F: Future<Output = T> + Unpin,
260{
261    pub(crate) fn new(fut: F) -> Self {
262        Self::Pending(fut)
263    }
264
265    pub(crate) fn unwrap_ready(self) -> T {
266        match self {
267            Self::Ready(val) => val,
268            Self::Pending(_) => panic!("future not ready"),
269        }
270    }
271    pub(crate) fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll<&mut T> {
272        match self {
273            Self::Pending(fut) => match Pin::new(fut).as_mut().poll(cx) {
274                Poll::Ready(val) => {
275                    *self = Self::Ready(val);
276                    Poll::Ready(match self {
277                        Self::Ready(val) => val,
278                        _ => unreachable!(),
279                    })
280                }
281                Poll::Pending => Poll::Pending,
282            },
283            Self::Ready(val) => Poll::Ready(val),
284        }
285    }
286    pub(crate) async fn into_future(self) -> T {
287        match self {
288            Self::Ready(val) => val,
289            Self::Pending(fut) => fut.await,
290        }
291    }
292}
293
294#[derive(Clone, Copy, Debug)]
295pub enum ErrorCode {
296    AccessDenied,
297    NotSupported,
298    InvalidArgument,
299    OutOfMemory,
300    Timeout,
301    InvalidState,
302    AddressNotBindable,
303    AddressInUse,
304    RemoteUnreachable,
305    ConnectionRefused,
306    ConnectionBroken,
307    ConnectionReset,
308    ConnectionAborted,
309    DatagramTooLarge,
310    Other,
311}
312
313impl fmt::Display for ErrorCode {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        fmt::Debug::fmt(self, f)
316    }
317}
318
319impl std::error::Error for ErrorCode {}
320
321impl From<std::io::Error> for ErrorCode {
322    fn from(value: std::io::Error) -> Self {
323        (&value).into()
324    }
325}
326
327impl From<&std::io::Error> for ErrorCode {
328    fn from(value: &std::io::Error) -> Self {
329        // Attempt the more detailed native error code first:
330        if let Some(errno) = Errno::from_io_error(value) {
331            return errno.into();
332        }
333
334        match value.kind() {
335            std::io::ErrorKind::AddrInUse => Self::AddressInUse,
336            std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable,
337            std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted,
338            std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused,
339            std::io::ErrorKind::ConnectionReset => Self::ConnectionReset,
340            std::io::ErrorKind::InvalidInput => Self::InvalidArgument,
341            std::io::ErrorKind::NotConnected => Self::InvalidState,
342            std::io::ErrorKind::OutOfMemory => Self::OutOfMemory,
343            std::io::ErrorKind::PermissionDenied => Self::AccessDenied,
344            std::io::ErrorKind::TimedOut => Self::Timeout,
345            std::io::ErrorKind::Unsupported => Self::NotSupported,
346            std::io::ErrorKind::HostUnreachable => Self::RemoteUnreachable,
347            std::io::ErrorKind::NetworkUnreachable => Self::RemoteUnreachable,
348            std::io::ErrorKind::NetworkDown => Self::RemoteUnreachable,
349            std::io::ErrorKind::BrokenPipe => Self::ConnectionBroken,
350            _ => {
351                debug!("unknown I/O error: {value}");
352                Self::Other
353            }
354        }
355    }
356}
357
358impl From<Errno> for ErrorCode {
359    fn from(value: Errno) -> Self {
360        (&value).into()
361    }
362}
363
364impl From<&Errno> for ErrorCode {
365    fn from(value: &Errno) -> Self {
366        match *value {
367            #[cfg(not(windows))]
368            Errno::PERM => Self::AccessDenied,
369            Errno::ACCESS => Self::AccessDenied,
370            Errno::ADDRINUSE => Self::AddressInUse,
371            Errno::ADDRNOTAVAIL => Self::AddressNotBindable,
372            Errno::TIMEDOUT => Self::Timeout,
373            #[cfg(not(windows))]
374            Errno::PIPE => Self::ConnectionBroken,
375            Errno::CONNREFUSED => Self::ConnectionRefused,
376            Errno::CONNRESET => Self::ConnectionReset,
377            Errno::CONNABORTED => Self::ConnectionAborted,
378            Errno::INVAL => Self::InvalidArgument,
379            Errno::HOSTUNREACH => Self::RemoteUnreachable,
380            Errno::HOSTDOWN => Self::RemoteUnreachable,
381            Errno::NETDOWN => Self::RemoteUnreachable,
382            Errno::NETUNREACH => Self::RemoteUnreachable,
383            #[cfg(target_os = "linux")]
384            Errno::NONET => Self::RemoteUnreachable,
385            Errno::ISCONN => Self::InvalidState,
386            Errno::NOTCONN => Self::InvalidState,
387            Errno::DESTADDRREQ => Self::InvalidState,
388            Errno::MSGSIZE => Self::DatagramTooLarge,
389            #[cfg(not(windows))]
390            Errno::NOMEM => Self::OutOfMemory,
391            Errno::NOBUFS => Self::OutOfMemory,
392            Errno::OPNOTSUPP => Self::NotSupported,
393            Errno::NOPROTOOPT => Self::NotSupported,
394            Errno::PFNOSUPPORT => Self::NotSupported,
395            Errno::PROTONOSUPPORT => Self::NotSupported,
396            Errno::PROTOTYPE => Self::NotSupported,
397            Errno::SOCKTNOSUPPORT => Self::NotSupported,
398            Errno::AFNOSUPPORT => Self::NotSupported,
399
400            // FYI, EINPROGRESS should have already been handled by connect.
401            _ => {
402                debug!("unknown I/O error: {value}");
403                Self::Other
404            }
405        }
406    }
407}
408
409fn is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool {
410    matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])
411        && addr != Ipv6Addr::UNSPECIFIED
412        && addr != Ipv6Addr::LOCALHOST
413}
414
415pub(crate) fn is_valid_address_family(addr: IpAddr, socket_family: SocketAddressFamily) -> bool {
416    match (socket_family, addr) {
417        (SocketAddressFamily::Ipv4, IpAddr::V4(..)) => true,
418        (SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => {
419            // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated
420            // since 2006, OS handling of them is inconsistent and our own
421            // validations don't take them into account either.
422            // Note that these are not the same as IPv4-*mapped* IPv6 addresses.
423            !is_deprecated_ipv4_compatible(ipv6) && ipv6.to_ipv4_mapped().is_none()
424        }
425        _ => false,
426    }
427}
428
429pub(crate) fn is_valid_remote_address(addr: SocketAddr) -> bool {
430    !addr.ip().to_canonical().is_unspecified() && addr.port() != 0
431}
432
433pub(crate) fn is_valid_unicast_address(addr: IpAddr) -> bool {
434    match addr.to_canonical() {
435        IpAddr::V4(ipv4) => !ipv4.is_multicast() && !ipv4.is_broadcast(),
436        IpAddr::V6(ipv6) => !ipv6.is_multicast(),
437    }
438}
439
440pub(crate) fn to_ipv4_addr(addr: (u8, u8, u8, u8)) -> Ipv4Addr {
441    let (x0, x1, x2, x3) = addr;
442    Ipv4Addr::new(x0, x1, x2, x3)
443}
444
445pub(crate) fn from_ipv4_addr(addr: Ipv4Addr) -> (u8, u8, u8, u8) {
446    let [x0, x1, x2, x3] = addr.octets();
447    (x0, x1, x2, x3)
448}
449
450pub(crate) fn to_ipv6_addr(addr: (u16, u16, u16, u16, u16, u16, u16, u16)) -> Ipv6Addr {
451    let (x0, x1, x2, x3, x4, x5, x6, x7) = addr;
452    Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7)
453}
454
455pub(crate) fn from_ipv6_addr(addr: Ipv6Addr) -> (u16, u16, u16, u16, u16, u16, u16, u16) {
456    let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments();
457    (x0, x1, x2, x3, x4, x5, x6, x7)
458}
459
460/*
461 * Syscalls wrappers with (opinionated) portability fixes.
462 */
463
464fn normalize_get_buffer_size(value: usize) -> usize {
465    if cfg!(target_os = "linux") {
466        // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.
467        // getsockopt returns this internally doubled value.
468        // We'll half the value to at least get it back into the same ballpark that the application requested it in.
469        //
470        // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
471        value / 2
472    } else {
473        value
474    }
475}
476
477fn normalize_set_buffer_size(value: usize) -> usize {
478    value.clamp(1, i32::MAX as usize)
479}
480
481fn get_ip_ttl(fd: impl AsFd) -> Result<u8, ErrorCode> {
482    let v = sockopt::ip_ttl(fd)?;
483    let Ok(v) = v.try_into() else {
484        return Err(ErrorCode::NotSupported);
485    };
486    Ok(v)
487}
488
489fn get_ipv6_unicast_hops(fd: impl AsFd) -> Result<u8, ErrorCode> {
490    let v = sockopt::ipv6_unicast_hops(fd)?;
491    Ok(v)
492}
493
494pub(crate) fn get_unicast_hop_limit(
495    fd: impl AsFd,
496    family: SocketAddressFamily,
497) -> Result<u8, ErrorCode> {
498    match family {
499        SocketAddressFamily::Ipv4 => get_ip_ttl(fd),
500        SocketAddressFamily::Ipv6 => get_ipv6_unicast_hops(fd),
501    }
502}
503
504pub(crate) fn set_unicast_hop_limit(
505    fd: impl AsFd,
506    family: SocketAddressFamily,
507    value: u8,
508) -> Result<(), ErrorCode> {
509    if value == 0 {
510        // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
511        //
512        // A well-behaved IP application should never send out new packets with TTL 0.
513        // We validate the value ourselves because OS'es are not consistent in this.
514        // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
515        return Err(ErrorCode::InvalidArgument);
516    }
517    match family {
518        SocketAddressFamily::Ipv4 => {
519            sockopt::set_ip_ttl(fd, value.into())?;
520        }
521        SocketAddressFamily::Ipv6 => {
522            sockopt::set_ipv6_unicast_hops(fd, Some(value))?;
523        }
524    }
525    Ok(())
526}
527
528pub(crate) fn get_receive_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {
529    let v = sockopt::socket_recv_buffer_size(fd)?;
530    Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))
531}
532
533pub(crate) fn set_receive_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {
534    if value == 0 {
535        // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
536        return Err(ErrorCode::InvalidArgument);
537    }
538    let value = value.try_into().unwrap_or(usize::MAX);
539    let value = normalize_set_buffer_size(value);
540    match sockopt::set_socket_recv_buffer_size(fd, value) {
541        // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value
542        // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds
543        // their capability.
544        // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the
545        // value is out of range. We normalize this behavior in favor of the more commonly understood
546        // "performance hint" semantics. In other words; even ENOBUFS is "Ok".
547        // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input
548        // `size` ourselves, to completely close the gap with other platforms.
549        //
550        // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
551        Err(Errno::NOBUFS) => {}
552        Err(err) => return Err(err.into()),
553        _ => {}
554    };
555    Ok(value)
556}
557
558pub(crate) fn get_send_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {
559    let v = sockopt::socket_send_buffer_size(fd)?;
560    Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))
561}
562
563pub(crate) fn set_send_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {
564    if value == 0 {
565        // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
566        return Err(ErrorCode::InvalidArgument);
567    }
568    let value = value.try_into().unwrap_or(usize::MAX);
569    let value = normalize_set_buffer_size(value);
570    match sockopt::set_socket_send_buffer_size(fd, value) {
571        // See comment in `set_receive_buffer_size` for why we ignore NOBUFS.
572        Err(Errno::NOBUFS) => {}
573        Err(err) => return Err(err.into()),
574        _ => {}
575    };
576    Ok(value)
577}
578
579pub(crate) fn unspecified_addr(family: SocketAddressFamily) -> SocketAddr {
580    let ip = match family {
581        SocketAddressFamily::Ipv4 => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
582        SocketAddressFamily::Ipv6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
583    };
584    SocketAddr::new(ip, 0)
585}