wasmtime_wasi/host/
network.rs

1use crate::bindings::sockets::network::{
2    self, ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress,
3    Ipv6SocketAddress,
4};
5use crate::network::{from_ipv4_addr, from_ipv6_addr, to_ipv4_addr, to_ipv6_addr};
6use crate::{IoView, SocketError, WasiImpl, WasiView};
7use anyhow::Error;
8use rustix::io::Errno;
9use std::io;
10use wasmtime::component::Resource;
11
12impl<T> network::Host for WasiImpl<T>
13where
14    T: WasiView,
15{
16    fn convert_error_code(&mut self, error: SocketError) -> anyhow::Result<ErrorCode> {
17        error.downcast()
18    }
19
20    fn network_error_code(&mut self, err: Resource<Error>) -> anyhow::Result<Option<ErrorCode>> {
21        let err = self.table().get(&err)?;
22
23        if let Some(err) = err.downcast_ref::<std::io::Error>() {
24            return Ok(Some(ErrorCode::from(err)));
25        }
26
27        Ok(None)
28    }
29}
30
31impl<T> crate::bindings::sockets::network::HostNetwork for WasiImpl<T>
32where
33    T: WasiView,
34{
35    fn drop(&mut self, this: Resource<network::Network>) -> Result<(), anyhow::Error> {
36        let table = self.table();
37
38        table.delete(this)?;
39
40        Ok(())
41    }
42}
43
44impl From<io::Error> for ErrorCode {
45    fn from(value: io::Error) -> Self {
46        (&value).into()
47    }
48}
49
50impl From<&io::Error> for ErrorCode {
51    fn from(value: &io::Error) -> Self {
52        // Attempt the more detailed native error code first:
53        if let Some(errno) = Errno::from_io_error(value) {
54            return errno.into();
55        }
56
57        match value.kind() {
58            std::io::ErrorKind::AddrInUse => ErrorCode::AddressInUse,
59            std::io::ErrorKind::AddrNotAvailable => ErrorCode::AddressNotBindable,
60            std::io::ErrorKind::ConnectionAborted => ErrorCode::ConnectionAborted,
61            std::io::ErrorKind::ConnectionRefused => ErrorCode::ConnectionRefused,
62            std::io::ErrorKind::ConnectionReset => ErrorCode::ConnectionReset,
63            std::io::ErrorKind::Interrupted => ErrorCode::WouldBlock,
64            std::io::ErrorKind::InvalidInput => ErrorCode::InvalidArgument,
65            std::io::ErrorKind::NotConnected => ErrorCode::InvalidState,
66            std::io::ErrorKind::OutOfMemory => ErrorCode::OutOfMemory,
67            std::io::ErrorKind::PermissionDenied => ErrorCode::AccessDenied,
68            std::io::ErrorKind::TimedOut => ErrorCode::Timeout,
69            std::io::ErrorKind::Unsupported => ErrorCode::NotSupported,
70            std::io::ErrorKind::WouldBlock => ErrorCode::WouldBlock,
71
72            _ => {
73                tracing::debug!("unknown I/O error: {value}");
74                ErrorCode::Unknown
75            }
76        }
77    }
78}
79
80impl From<Errno> for ErrorCode {
81    fn from(value: Errno) -> Self {
82        (&value).into()
83    }
84}
85
86impl From<&Errno> for ErrorCode {
87    fn from(value: &Errno) -> Self {
88        match *value {
89            Errno::WOULDBLOCK => ErrorCode::WouldBlock,
90            #[allow(unreachable_patterns)] // EWOULDBLOCK and EAGAIN can have the same value.
91            Errno::AGAIN => ErrorCode::WouldBlock,
92            Errno::INTR => ErrorCode::WouldBlock,
93            #[cfg(not(windows))]
94            Errno::PERM => ErrorCode::AccessDenied,
95            Errno::ACCESS => ErrorCode::AccessDenied,
96            Errno::ADDRINUSE => ErrorCode::AddressInUse,
97            Errno::ADDRNOTAVAIL => ErrorCode::AddressNotBindable,
98            Errno::ALREADY => ErrorCode::ConcurrencyConflict,
99            Errno::TIMEDOUT => ErrorCode::Timeout,
100            Errno::CONNREFUSED => ErrorCode::ConnectionRefused,
101            Errno::CONNRESET => ErrorCode::ConnectionReset,
102            Errno::CONNABORTED => ErrorCode::ConnectionAborted,
103            Errno::INVAL => ErrorCode::InvalidArgument,
104            Errno::HOSTUNREACH => ErrorCode::RemoteUnreachable,
105            Errno::HOSTDOWN => ErrorCode::RemoteUnreachable,
106            Errno::NETDOWN => ErrorCode::RemoteUnreachable,
107            Errno::NETUNREACH => ErrorCode::RemoteUnreachable,
108            #[cfg(target_os = "linux")]
109            Errno::NONET => ErrorCode::RemoteUnreachable,
110            Errno::ISCONN => ErrorCode::InvalidState,
111            Errno::NOTCONN => ErrorCode::InvalidState,
112            Errno::DESTADDRREQ => ErrorCode::InvalidState,
113            #[cfg(not(windows))]
114            Errno::NFILE => ErrorCode::NewSocketLimit,
115            Errno::MFILE => ErrorCode::NewSocketLimit,
116            Errno::MSGSIZE => ErrorCode::DatagramTooLarge,
117            #[cfg(not(windows))]
118            Errno::NOMEM => ErrorCode::OutOfMemory,
119            Errno::NOBUFS => ErrorCode::OutOfMemory,
120            Errno::OPNOTSUPP => ErrorCode::NotSupported,
121            Errno::NOPROTOOPT => ErrorCode::NotSupported,
122            Errno::PFNOSUPPORT => ErrorCode::NotSupported,
123            Errno::PROTONOSUPPORT => ErrorCode::NotSupported,
124            Errno::PROTOTYPE => ErrorCode::NotSupported,
125            Errno::SOCKTNOSUPPORT => ErrorCode::NotSupported,
126            Errno::AFNOSUPPORT => ErrorCode::NotSupported,
127
128            // FYI, EINPROGRESS should have already been handled by connect.
129            _ => {
130                tracing::debug!("unknown I/O error: {value}");
131                ErrorCode::Unknown
132            }
133        }
134    }
135}
136
137impl From<std::net::IpAddr> for IpAddress {
138    fn from(addr: std::net::IpAddr) -> Self {
139        match addr {
140            std::net::IpAddr::V4(v4) => Self::Ipv4(from_ipv4_addr(v4)),
141            std::net::IpAddr::V6(v6) => Self::Ipv6(from_ipv6_addr(v6)),
142        }
143    }
144}
145
146impl From<IpSocketAddress> for std::net::SocketAddr {
147    fn from(addr: IpSocketAddress) -> Self {
148        match addr {
149            IpSocketAddress::Ipv4(ipv4) => Self::V4(ipv4.into()),
150            IpSocketAddress::Ipv6(ipv6) => Self::V6(ipv6.into()),
151        }
152    }
153}
154
155impl From<std::net::SocketAddr> for IpSocketAddress {
156    fn from(addr: std::net::SocketAddr) -> Self {
157        match addr {
158            std::net::SocketAddr::V4(v4) => Self::Ipv4(v4.into()),
159            std::net::SocketAddr::V6(v6) => Self::Ipv6(v6.into()),
160        }
161    }
162}
163
164impl From<Ipv4SocketAddress> for std::net::SocketAddrV4 {
165    fn from(addr: Ipv4SocketAddress) -> Self {
166        Self::new(to_ipv4_addr(addr.address), addr.port)
167    }
168}
169
170impl From<std::net::SocketAddrV4> for Ipv4SocketAddress {
171    fn from(addr: std::net::SocketAddrV4) -> Self {
172        Self {
173            address: from_ipv4_addr(*addr.ip()),
174            port: addr.port(),
175        }
176    }
177}
178
179impl From<Ipv6SocketAddress> for std::net::SocketAddrV6 {
180    fn from(addr: Ipv6SocketAddress) -> Self {
181        Self::new(
182            to_ipv6_addr(addr.address),
183            addr.port,
184            addr.flow_info,
185            addr.scope_id,
186        )
187    }
188}
189
190impl From<std::net::SocketAddrV6> for Ipv6SocketAddress {
191    fn from(addr: std::net::SocketAddrV6) -> Self {
192        Self {
193            address: from_ipv6_addr(*addr.ip()),
194            port: addr.port(),
195            flow_info: addr.flowinfo(),
196            scope_id: addr.scope_id(),
197        }
198    }
199}
200
201impl std::net::ToSocketAddrs for IpSocketAddress {
202    type Iter = <std::net::SocketAddr as std::net::ToSocketAddrs>::Iter;
203
204    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
205        std::net::SocketAddr::from(*self).to_socket_addrs()
206    }
207}
208
209impl std::net::ToSocketAddrs for Ipv4SocketAddress {
210    type Iter = <std::net::SocketAddrV4 as std::net::ToSocketAddrs>::Iter;
211
212    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
213        std::net::SocketAddrV4::from(*self).to_socket_addrs()
214    }
215}
216
217impl std::net::ToSocketAddrs for Ipv6SocketAddress {
218    type Iter = <std::net::SocketAddrV6 as std::net::ToSocketAddrs>::Iter;
219
220    fn to_socket_addrs(&self) -> io::Result<Self::Iter> {
221        std::net::SocketAddrV6::from(*self).to_socket_addrs()
222    }
223}
224
225impl From<IpAddressFamily> for cap_net_ext::AddressFamily {
226    fn from(family: IpAddressFamily) -> Self {
227        match family {
228            IpAddressFamily::Ipv4 => cap_net_ext::AddressFamily::Ipv4,
229            IpAddressFamily::Ipv6 => cap_net_ext::AddressFamily::Ipv6,
230        }
231    }
232}
233
234impl From<cap_net_ext::AddressFamily> for IpAddressFamily {
235    fn from(family: cap_net_ext::AddressFamily) -> Self {
236        match family {
237            cap_net_ext::AddressFamily::Ipv4 => IpAddressFamily::Ipv4,
238            cap_net_ext::AddressFamily::Ipv6 => IpAddressFamily::Ipv6,
239        }
240    }
241}
242
243pub(crate) mod util {
244    use std::io;
245    use std::net::{IpAddr, Ipv6Addr, SocketAddr};
246    use std::time::Duration;
247
248    use crate::network::SocketAddressFamily;
249    use cap_net_ext::{AddressFamily, Blocking, UdpSocketExt};
250    use rustix::fd::{AsFd, OwnedFd};
251    use rustix::io::Errno;
252    use rustix::net::sockopt;
253
254    pub fn validate_unicast(addr: &SocketAddr) -> io::Result<()> {
255        match to_canonical(&addr.ip()) {
256            IpAddr::V4(ipv4) => {
257                if ipv4.is_multicast() || ipv4.is_broadcast() {
258                    Err(io::Error::new(
259                        io::ErrorKind::InvalidInput,
260                        "Both IPv4 broadcast and multicast addresses are not supported",
261                    ))
262                } else {
263                    Ok(())
264                }
265            }
266            IpAddr::V6(ipv6) => {
267                if ipv6.is_multicast() {
268                    Err(io::Error::new(
269                        io::ErrorKind::InvalidInput,
270                        "IPv6 multicast addresses are not supported",
271                    ))
272                } else {
273                    Ok(())
274                }
275            }
276        }
277    }
278
279    pub fn validate_remote_address(addr: &SocketAddr) -> io::Result<()> {
280        if to_canonical(&addr.ip()).is_unspecified() {
281            return Err(io::Error::new(
282                io::ErrorKind::InvalidInput,
283                "Remote address may not be `0.0.0.0` or `::`",
284            ));
285        }
286
287        if addr.port() == 0 {
288            return Err(io::Error::new(
289                io::ErrorKind::InvalidInput,
290                "Remote port may not be 0",
291            ));
292        }
293
294        Ok(())
295    }
296
297    pub fn validate_address_family(
298        addr: &SocketAddr,
299        socket_family: &SocketAddressFamily,
300    ) -> io::Result<()> {
301        match (socket_family, addr.ip()) {
302            (SocketAddressFamily::Ipv4, IpAddr::V4(_)) => Ok(()),
303            (SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => {
304                if is_deprecated_ipv4_compatible(&ipv6) {
305                    // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated
306                    // since 2006, OS handling of them is inconsistent and our own
307                    // validations don't take them into account either.
308                    // Note that these are not the same as IPv4-*mapped* IPv6 addresses.
309                    Err(io::Error::new(
310                        io::ErrorKind::InvalidInput,
311                        "IPv4-compatible IPv6 addresses are not supported",
312                    ))
313                } else if ipv6.to_ipv4_mapped().is_some() {
314                    Err(io::Error::new(
315                        io::ErrorKind::InvalidInput,
316                        "IPv4-mapped IPv6 address passed to an IPv6-only socket",
317                    ))
318                } else {
319                    Ok(())
320                }
321            }
322            _ => Err(io::Error::new(
323                io::ErrorKind::InvalidInput,
324                "Address family mismatch",
325            )),
326        }
327    }
328
329    // Can be removed once `IpAddr::to_canonical` becomes stable.
330    pub fn to_canonical(addr: &IpAddr) -> IpAddr {
331        match addr {
332            IpAddr::V4(ipv4) => IpAddr::V4(*ipv4),
333            IpAddr::V6(ipv6) => {
334                if let Some(ipv4) = ipv6.to_ipv4_mapped() {
335                    IpAddr::V4(ipv4)
336                } else {
337                    IpAddr::V6(*ipv6)
338                }
339            }
340        }
341    }
342
343    fn is_deprecated_ipv4_compatible(addr: &Ipv6Addr) -> bool {
344        matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])
345            && *addr != Ipv6Addr::UNSPECIFIED
346            && *addr != Ipv6Addr::LOCALHOST
347    }
348
349    /*
350     * Syscalls wrappers with (opinionated) portability fixes.
351     */
352
353    pub fn udp_socket(family: AddressFamily, blocking: Blocking) -> io::Result<OwnedFd> {
354        // Delegate socket creation to cap_net_ext. They handle a couple of things for us:
355        // - On Windows: call WSAStartup if not done before.
356        // - Set the NONBLOCK and CLOEXEC flags. Either immediately during socket creation,
357        //   or afterwards using ioctl or fcntl. Exact method depends on the platform.
358
359        let socket = cap_std::net::UdpSocket::new(family, blocking)?;
360        Ok(OwnedFd::from(socket))
361    }
362
363    pub fn udp_bind<Fd: AsFd>(sockfd: Fd, addr: &SocketAddr) -> rustix::io::Result<()> {
364        rustix::net::bind(sockfd, addr).map_err(|error| match error {
365            // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS
366            // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
367            #[cfg(windows)]
368            Errno::NOBUFS => Errno::ADDRINUSE,
369            _ => error,
370        })
371    }
372
373    pub fn udp_disconnect<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<()> {
374        match rustix::net::connect_unspec(sockfd) {
375            // BSD platforms return an error even if the UDP socket was disconnected successfully.
376            //
377            // MacOS was kind enough to document this: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/connect.2.html
378            // > Datagram sockets may dissolve the association by connecting to an
379            // > invalid address, such as a null address or an address with the address
380            // > family set to AF_UNSPEC (the error EAFNOSUPPORT will be harmlessly
381            // > returned).
382            //
383            // ... except that this appears to be incomplete, because experiments
384            // have shown that MacOS actually returns EINVAL, depending on the
385            // address family of the socket.
386            #[cfg(target_os = "macos")]
387            Err(Errno::INVAL | Errno::AFNOSUPPORT) => Ok(()),
388            r => r,
389        }
390    }
391
392    // Even though SO_REUSEADDR is a SOL_* level option, this function contain a
393    // compatibility fix specific to TCP. That's why it contains the `_tcp_` infix instead of `_socket_`.
394    #[allow(unused_variables)] // Parameters are not used on Windows
395    pub fn set_tcp_reuseaddr<Fd: AsFd>(sockfd: Fd, value: bool) -> rustix::io::Result<()> {
396        // When a TCP socket is closed, the system may
397        // temporarily reserve that specific address+port pair in a so called
398        // TIME_WAIT state. During that period, any attempt to rebind to that pair
399        // will fail. Setting SO_REUSEADDR to true bypasses that behaviour. Unlike
400        // the name "SO_REUSEADDR" might suggest, it does not allow multiple
401        // active sockets to share the same local address.
402
403        // On Windows that behavior is the default, so there is no need to manually
404        // configure such an option. But (!), Windows _does_ have an identically
405        // named socket option which allows users to "hijack" active sockets.
406        // This is definitely not what we want to do here.
407
408        // Microsoft's own documentation[1] states that we should set SO_EXCLUSIVEADDRUSE
409        // instead (to the inverse value), however the github issue below[2] seems
410        // to indicate that that may no longer be correct.
411        // [1]: https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse
412        // [2]: https://github.com/python-trio/trio/issues/928
413
414        #[cfg(not(windows))]
415        sockopt::set_socket_reuseaddr(sockfd, value)?;
416
417        Ok(())
418    }
419
420    pub fn set_tcp_keepidle<Fd: AsFd>(sockfd: Fd, value: Duration) -> rustix::io::Result<()> {
421        if value <= Duration::ZERO {
422            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
423            return Err(Errno::INVAL);
424        }
425
426        // Ensure that the value passed to the actual syscall never gets rounded down to 0.
427        const MIN_SECS: u64 = 1;
428
429        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
430        const MAX_SECS: u64 = i16::MAX as u64;
431
432        sockopt::set_tcp_keepidle(
433            sockfd,
434            value.clamp(Duration::from_secs(MIN_SECS), Duration::from_secs(MAX_SECS)),
435        )
436    }
437
438    pub fn set_tcp_keepintvl<Fd: AsFd>(sockfd: Fd, value: Duration) -> rustix::io::Result<()> {
439        if value <= Duration::ZERO {
440            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
441            return Err(Errno::INVAL);
442        }
443
444        // Ensure that any fractional value passed to the actual syscall never gets rounded down to 0.
445        const MIN_SECS: u64 = 1;
446
447        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
448        const MAX_SECS: u64 = i16::MAX as u64;
449
450        sockopt::set_tcp_keepintvl(
451            sockfd,
452            value.clamp(Duration::from_secs(MIN_SECS), Duration::from_secs(MAX_SECS)),
453        )
454    }
455
456    pub fn set_tcp_keepcnt<Fd: AsFd>(sockfd: Fd, value: u32) -> rustix::io::Result<()> {
457        if value == 0 {
458            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
459            return Err(Errno::INVAL);
460        }
461
462        const MIN_CNT: u32 = 1;
463        // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
464        const MAX_CNT: u32 = i8::MAX as u32;
465
466        sockopt::set_tcp_keepcnt(sockfd, value.clamp(MIN_CNT, MAX_CNT))
467    }
468
469    pub fn get_ip_ttl<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<u8> {
470        sockopt::get_ip_ttl(sockfd)?
471            .try_into()
472            .map_err(|_| Errno::OPNOTSUPP)
473    }
474
475    pub fn get_ipv6_unicast_hops<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<u8> {
476        sockopt::get_ipv6_unicast_hops(sockfd)
477    }
478
479    pub fn set_ip_ttl<Fd: AsFd>(sockfd: Fd, value: u8) -> rustix::io::Result<()> {
480        match value {
481            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
482            //
483            // A well-behaved IP application should never send out new packets with TTL 0.
484            // We validate the value ourselves because OS'es are not consistent in this.
485            // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
486            0 => Err(Errno::INVAL),
487            _ => sockopt::set_ip_ttl(sockfd, value.into()),
488        }
489    }
490
491    pub fn set_ipv6_unicast_hops<Fd: AsFd>(sockfd: Fd, value: u8) -> rustix::io::Result<()> {
492        match value {
493            0 => Err(Errno::INVAL), // See `set_ip_ttl`
494            _ => sockopt::set_ipv6_unicast_hops(sockfd, Some(value)),
495        }
496    }
497
498    fn normalize_get_buffer_size(value: usize) -> usize {
499        if cfg!(target_os = "linux") {
500            // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.
501            // getsockopt returns this internally doubled value.
502            // We'll half the value to at least get it back into the same ballpark that the application requested it in.
503            //
504            // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
505            value / 2
506        } else {
507            value
508        }
509    }
510
511    fn normalize_set_buffer_size(value: usize) -> usize {
512        value.clamp(1, i32::MAX as usize)
513    }
514
515    pub fn get_socket_recv_buffer_size<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<usize> {
516        let value = sockopt::get_socket_recv_buffer_size(sockfd)?;
517        Ok(normalize_get_buffer_size(value))
518    }
519
520    pub fn get_socket_send_buffer_size<Fd: AsFd>(sockfd: Fd) -> rustix::io::Result<usize> {
521        let value = sockopt::get_socket_send_buffer_size(sockfd)?;
522        Ok(normalize_get_buffer_size(value))
523    }
524
525    pub fn set_socket_recv_buffer_size<Fd: AsFd>(
526        sockfd: Fd,
527        value: usize,
528    ) -> rustix::io::Result<()> {
529        if value == 0 {
530            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
531            return Err(Errno::INVAL);
532        }
533
534        let value = normalize_set_buffer_size(value);
535
536        match sockopt::set_socket_recv_buffer_size(sockfd, value) {
537            // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value
538            // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds
539            // their capability.
540            // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the
541            // value is out of range. We normalize this behavior in favor of the more commonly understood
542            // "performance hint" semantics. In other words; even ENOBUFS is "Ok".
543            // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input
544            // `size` ourselves, to completely close the gap with other platforms.
545            //
546            // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
547            Err(Errno::NOBUFS) => Ok(()),
548            r => r,
549        }
550    }
551
552    pub fn set_socket_send_buffer_size<Fd: AsFd>(
553        sockfd: Fd,
554        value: usize,
555    ) -> rustix::io::Result<()> {
556        if value == 0 {
557            // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
558            return Err(Errno::INVAL);
559        }
560
561        let value = normalize_set_buffer_size(value);
562
563        match sockopt::set_socket_send_buffer_size(sockfd, value) {
564            Err(Errno::NOBUFS) => Ok(()), // See set_socket_recv_buffer_size
565            r => r,
566        }
567    }
568}