wasmtime_wasi/p2/host/
network.rs

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