1 use core::fmt;
2 use core::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
3 use core::str::FromStr as _;
4 use core::time::Duration;
5
6 use cap_net_ext::{AddressFamily, Blocking, UdpSocketExt};
7 use rustix::fd::AsFd;
8 use rustix::io::Errno;
9 use rustix::net::{bind, connect, connect_unspec, sockopt};
10 use tracing::debug;
11
12 use crate::sockets::SocketAddressFamily;
13
14 #[derive(Debug)]
15 pub enum ErrorCode {
16 Unknown,
17 AccessDenied,
18 NotSupported,
19 InvalidArgument,
20 OutOfMemory,
21 Timeout,
22 InvalidState,
23 AddressNotBindable,
24 AddressInUse,
25 RemoteUnreachable,
26 ConnectionRefused,
27 ConnectionReset,
28 ConnectionAborted,
29 DatagramTooLarge,
30 NotInProgress,
31 ConcurrencyConflict,
32 }
33
34 impl fmt::Display for ErrorCode {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 fmt::Debug::fmt(self, f)
37 }
38 }
39
40 impl std::error::Error for ErrorCode {}
41
is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool42 fn is_deprecated_ipv4_compatible(addr: Ipv6Addr) -> bool {
43 matches!(addr.segments(), [0, 0, 0, 0, 0, 0, _, _])
44 && addr != Ipv6Addr::UNSPECIFIED
45 && addr != Ipv6Addr::LOCALHOST
46 }
47
is_valid_address_family(addr: IpAddr, socket_family: SocketAddressFamily) -> bool48 pub fn is_valid_address_family(addr: IpAddr, socket_family: SocketAddressFamily) -> bool {
49 match (socket_family, addr) {
50 (SocketAddressFamily::Ipv4, IpAddr::V4(..)) => true,
51 (SocketAddressFamily::Ipv6, IpAddr::V6(ipv6)) => {
52 // Reject IPv4-*compatible* IPv6 addresses. They have been deprecated
53 // since 2006, OS handling of them is inconsistent and our own
54 // validations don't take them into account either.
55 // Note that these are not the same as IPv4-*mapped* IPv6 addresses.
56 !is_deprecated_ipv4_compatible(ipv6) && ipv6.to_ipv4_mapped().is_none()
57 }
58 _ => false,
59 }
60 }
61
is_valid_remote_address(addr: SocketAddr) -> bool62 pub fn is_valid_remote_address(addr: SocketAddr) -> bool {
63 !addr.ip().to_canonical().is_unspecified() && addr.port() != 0
64 }
65
is_valid_unicast_address(addr: IpAddr) -> bool66 pub fn is_valid_unicast_address(addr: IpAddr) -> bool {
67 match addr.to_canonical() {
68 IpAddr::V4(ipv4) => !ipv4.is_multicast() && !ipv4.is_broadcast(),
69 IpAddr::V6(ipv6) => !ipv6.is_multicast(),
70 }
71 }
72
to_ipv4_addr(addr: (u8, u8, u8, u8)) -> Ipv4Addr73 pub fn to_ipv4_addr(addr: (u8, u8, u8, u8)) -> Ipv4Addr {
74 let (x0, x1, x2, x3) = addr;
75 Ipv4Addr::new(x0, x1, x2, x3)
76 }
77
from_ipv4_addr(addr: Ipv4Addr) -> (u8, u8, u8, u8)78 pub fn from_ipv4_addr(addr: Ipv4Addr) -> (u8, u8, u8, u8) {
79 let [x0, x1, x2, x3] = addr.octets();
80 (x0, x1, x2, x3)
81 }
82
to_ipv6_addr(addr: (u16, u16, u16, u16, u16, u16, u16, u16)) -> Ipv6Addr83 pub fn to_ipv6_addr(addr: (u16, u16, u16, u16, u16, u16, u16, u16)) -> Ipv6Addr {
84 let (x0, x1, x2, x3, x4, x5, x6, x7) = addr;
85 Ipv6Addr::new(x0, x1, x2, x3, x4, x5, x6, x7)
86 }
87
from_ipv6_addr(addr: Ipv6Addr) -> (u16, u16, u16, u16, u16, u16, u16, u16)88 pub fn from_ipv6_addr(addr: Ipv6Addr) -> (u16, u16, u16, u16, u16, u16, u16, u16) {
89 let [x0, x1, x2, x3, x4, x5, x6, x7] = addr.segments();
90 (x0, x1, x2, x3, x4, x5, x6, x7)
91 }
92
93 /*
94 * Syscalls wrappers with (opinionated) portability fixes.
95 */
96
normalize_get_buffer_size(value: usize) -> usize97 pub fn normalize_get_buffer_size(value: usize) -> usize {
98 if cfg!(target_os = "linux") {
99 // Linux doubles the value passed to setsockopt to allow space for bookkeeping overhead.
100 // getsockopt returns this internally doubled value.
101 // We'll half the value to at least get it back into the same ballpark that the application requested it in.
102 //
103 // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
104 value / 2
105 } else {
106 value
107 }
108 }
109
normalize_set_buffer_size(value: usize) -> usize110 pub fn normalize_set_buffer_size(value: usize) -> usize {
111 value.clamp(1, i32::MAX as usize)
112 }
113
114 impl From<std::io::Error> for ErrorCode {
from(value: std::io::Error) -> Self115 fn from(value: std::io::Error) -> Self {
116 (&value).into()
117 }
118 }
119
120 impl From<&std::io::Error> for ErrorCode {
from(value: &std::io::Error) -> Self121 fn from(value: &std::io::Error) -> Self {
122 // Attempt the more detailed native error code first:
123 if let Some(errno) = Errno::from_io_error(value) {
124 return errno.into();
125 }
126
127 match value.kind() {
128 std::io::ErrorKind::AddrInUse => Self::AddressInUse,
129 std::io::ErrorKind::AddrNotAvailable => Self::AddressNotBindable,
130 std::io::ErrorKind::ConnectionAborted => Self::ConnectionAborted,
131 std::io::ErrorKind::ConnectionRefused => Self::ConnectionRefused,
132 std::io::ErrorKind::ConnectionReset => Self::ConnectionReset,
133 std::io::ErrorKind::InvalidInput => Self::InvalidArgument,
134 std::io::ErrorKind::NotConnected => Self::InvalidState,
135 std::io::ErrorKind::OutOfMemory => Self::OutOfMemory,
136 std::io::ErrorKind::PermissionDenied => Self::AccessDenied,
137 std::io::ErrorKind::TimedOut => Self::Timeout,
138 std::io::ErrorKind::Unsupported => Self::NotSupported,
139 _ => {
140 debug!("unknown I/O error: {value}");
141 Self::Unknown
142 }
143 }
144 }
145 }
146
147 impl From<Errno> for ErrorCode {
from(value: Errno) -> Self148 fn from(value: Errno) -> Self {
149 (&value).into()
150 }
151 }
152
153 impl From<&Errno> for ErrorCode {
from(value: &Errno) -> Self154 fn from(value: &Errno) -> Self {
155 match *value {
156 #[cfg(not(windows))]
157 Errno::PERM => Self::AccessDenied,
158 Errno::ACCESS => Self::AccessDenied,
159 Errno::ADDRINUSE => Self::AddressInUse,
160 Errno::ADDRNOTAVAIL => Self::AddressNotBindable,
161 Errno::TIMEDOUT => Self::Timeout,
162 Errno::CONNREFUSED => Self::ConnectionRefused,
163 Errno::CONNRESET => Self::ConnectionReset,
164 Errno::CONNABORTED => Self::ConnectionAborted,
165 Errno::INVAL => Self::InvalidArgument,
166 Errno::HOSTUNREACH => Self::RemoteUnreachable,
167 Errno::HOSTDOWN => Self::RemoteUnreachable,
168 Errno::NETDOWN => Self::RemoteUnreachable,
169 Errno::NETUNREACH => Self::RemoteUnreachable,
170 #[cfg(target_os = "linux")]
171 Errno::NONET => Self::RemoteUnreachable,
172 Errno::ISCONN => Self::InvalidState,
173 Errno::NOTCONN => Self::InvalidState,
174 Errno::DESTADDRREQ => Self::InvalidState,
175 Errno::MSGSIZE => Self::DatagramTooLarge,
176 #[cfg(not(windows))]
177 Errno::NOMEM => Self::OutOfMemory,
178 Errno::NOBUFS => Self::OutOfMemory,
179 Errno::OPNOTSUPP => Self::NotSupported,
180 Errno::NOPROTOOPT => Self::NotSupported,
181 Errno::PFNOSUPPORT => Self::NotSupported,
182 Errno::PROTONOSUPPORT => Self::NotSupported,
183 Errno::PROTOTYPE => Self::NotSupported,
184 Errno::SOCKTNOSUPPORT => Self::NotSupported,
185 Errno::AFNOSUPPORT => Self::NotSupported,
186
187 // FYI, EINPROGRESS should have already been handled by connect.
188 _ => {
189 debug!("unknown I/O error: {value}");
190 Self::Unknown
191 }
192 }
193 }
194 }
195
get_ip_ttl(fd: impl AsFd) -> Result<u8, ErrorCode>196 pub fn get_ip_ttl(fd: impl AsFd) -> Result<u8, ErrorCode> {
197 let v = sockopt::ip_ttl(fd)?;
198 let Ok(v) = v.try_into() else {
199 return Err(ErrorCode::NotSupported);
200 };
201 Ok(v)
202 }
203
get_ipv6_unicast_hops(fd: impl AsFd) -> Result<u8, ErrorCode>204 pub fn get_ipv6_unicast_hops(fd: impl AsFd) -> Result<u8, ErrorCode> {
205 let v = sockopt::ipv6_unicast_hops(fd)?;
206 Ok(v)
207 }
208
get_unicast_hop_limit(fd: impl AsFd, family: SocketAddressFamily) -> Result<u8, ErrorCode>209 pub fn get_unicast_hop_limit(fd: impl AsFd, family: SocketAddressFamily) -> Result<u8, ErrorCode> {
210 match family {
211 SocketAddressFamily::Ipv4 => get_ip_ttl(fd),
212 SocketAddressFamily::Ipv6 => get_ipv6_unicast_hops(fd),
213 }
214 }
215
set_unicast_hop_limit( fd: impl AsFd, family: SocketAddressFamily, value: u8, ) -> Result<(), ErrorCode>216 pub fn set_unicast_hop_limit(
217 fd: impl AsFd,
218 family: SocketAddressFamily,
219 value: u8,
220 ) -> Result<(), ErrorCode> {
221 if value == 0 {
222 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
223 //
224 // A well-behaved IP application should never send out new packets with TTL 0.
225 // We validate the value ourselves because OS'es are not consistent in this.
226 // On Linux the validation is even inconsistent between their IPv4 and IPv6 implementation.
227 return Err(ErrorCode::InvalidArgument);
228 }
229 match family {
230 SocketAddressFamily::Ipv4 => {
231 sockopt::set_ip_ttl(fd, value.into())?;
232 }
233 SocketAddressFamily::Ipv6 => {
234 sockopt::set_ipv6_unicast_hops(fd, Some(value))?;
235 }
236 }
237 Ok(())
238 }
239
receive_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode>240 pub fn receive_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {
241 let v = sockopt::socket_recv_buffer_size(fd)?;
242 Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))
243 }
244
set_receive_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode>245 pub fn set_receive_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {
246 if value == 0 {
247 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
248 return Err(ErrorCode::InvalidArgument);
249 }
250 let value = value.try_into().unwrap_or(usize::MAX);
251 let value = normalize_set_buffer_size(value);
252 match sockopt::set_socket_recv_buffer_size(fd, value) {
253 // Most platforms (Linux, Windows, Fuchsia, Solaris, Illumos, Haiku, ESP-IDF, ..and more?) treat the value
254 // passed to SO_SNDBUF/SO_RCVBUF as a performance tuning hint and silently clamp the input if it exceeds
255 // their capability.
256 // As far as I can see, only the *BSD family views this option as a hard requirement and fails when the
257 // value is out of range. We normalize this behavior in favor of the more commonly understood
258 // "performance hint" semantics. In other words; even ENOBUFS is "Ok".
259 // A future improvement could be to query the corresponding sysctl on *BSD platforms and clamp the input
260 // `size` ourselves, to completely close the gap with other platforms.
261 //
262 // This normalized behavior is tested for in: test-programs/src/bin/preview2_tcp_sockopts.rs
263 Err(Errno::NOBUFS) => {}
264 Err(err) => return Err(err.into()),
265 _ => {}
266 };
267 Ok(value)
268 }
269
send_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode>270 pub fn send_buffer_size(fd: impl AsFd) -> Result<u64, ErrorCode> {
271 let v = sockopt::socket_send_buffer_size(fd)?;
272 Ok(normalize_get_buffer_size(v).try_into().unwrap_or(u64::MAX))
273 }
274
set_send_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode>275 pub fn set_send_buffer_size(fd: impl AsFd, value: u64) -> Result<usize, ErrorCode> {
276 if value == 0 {
277 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
278 return Err(ErrorCode::InvalidArgument);
279 }
280 let value = value.try_into().unwrap_or(usize::MAX);
281 let value = normalize_set_buffer_size(value);
282 match sockopt::set_socket_send_buffer_size(fd, value) {
283 Err(Errno::NOBUFS) => {}
284 Err(err) => return Err(err.into()),
285 _ => {}
286 };
287 Ok(value)
288 }
289
set_keep_alive_idle_time(fd: impl AsFd, value: u64) -> Result<u64, ErrorCode>290 pub fn set_keep_alive_idle_time(fd: impl AsFd, value: u64) -> Result<u64, ErrorCode> {
291 const NANOS_PER_SEC: u64 = 1_000_000_000;
292
293 // Ensure that the value passed to the actual syscall never gets rounded down to 0.
294 const MIN: u64 = NANOS_PER_SEC;
295
296 // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
297 const MAX: u64 = (i16::MAX as u64) * NANOS_PER_SEC;
298
299 if value <= 0 {
300 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
301 return Err(ErrorCode::InvalidArgument);
302 }
303 let value = value.clamp(MIN, MAX);
304 sockopt::set_tcp_keepidle(fd, Duration::from_nanos(value))?;
305 Ok(value)
306 }
307
set_keep_alive_interval(fd: impl AsFd, value: Duration) -> Result<(), ErrorCode>308 pub fn set_keep_alive_interval(fd: impl AsFd, value: Duration) -> Result<(), ErrorCode> {
309 // Ensure that any fractional value passed to the actual syscall never gets rounded down to 0.
310 const MIN: Duration = Duration::from_secs(1);
311
312 // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
313 const MAX: Duration = Duration::from_secs(i16::MAX as u64);
314
315 if value <= Duration::ZERO {
316 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
317 return Err(ErrorCode::InvalidArgument);
318 }
319 sockopt::set_tcp_keepintvl(fd, value.clamp(MIN, MAX))?;
320 Ok(())
321 }
322
set_keep_alive_count(fd: impl AsFd, value: u32) -> Result<(), ErrorCode>323 pub fn set_keep_alive_count(fd: impl AsFd, value: u32) -> Result<(), ErrorCode> {
324 const MIN_CNT: u32 = 1;
325 // Cap it at Linux' maximum, which appears to have the lowest limit across our supported platforms.
326 const MAX_CNT: u32 = i8::MAX as u32;
327
328 if value == 0 {
329 // WIT: "If the provided value is 0, an `invalid-argument` error is returned."
330 return Err(ErrorCode::InvalidArgument);
331 }
332 sockopt::set_tcp_keepcnt(fd, value.clamp(MIN_CNT, MAX_CNT))?;
333 Ok(())
334 }
335
tcp_bind( socket: &tokio::net::TcpSocket, local_address: SocketAddr, ) -> Result<(), ErrorCode>336 pub fn tcp_bind(
337 socket: &tokio::net::TcpSocket,
338 local_address: SocketAddr,
339 ) -> Result<(), ErrorCode> {
340 // From the WASI spec:
341 // > The bind operation shouldn't be affected by the TIME_WAIT state of a
342 // > recently closed socket on the same local address. In practice this
343 // > means that the SO_REUSEADDR socket option should be set implicitly on
344 // > all platforms, except on Windows where this is the default behavior
345 // > and SO_REUSEADDR performs something different.
346 #[cfg(not(windows))]
347 {
348 _ = sockopt::set_socket_reuseaddr(&socket, true);
349 }
350
351 // Perform the OS bind call.
352 socket
353 .bind(local_address)
354 .map_err(|err| match Errno::from_io_error(&err) {
355 // From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html:
356 // > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket
357 //
358 // The most common reasons for this error should have already
359 // been handled by our own validation slightly higher up in this
360 // function. This error mapping is here just in case there is
361 // an edge case we didn't catch.
362 Some(Errno::AFNOSUPPORT) => ErrorCode::InvalidArgument,
363 // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS
364 // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
365 #[cfg(windows)]
366 Some(Errno::NOBUFS) => ErrorCode::AddressInUse,
367 _ => err.into(),
368 })
369 }
370
udp_socket(family: AddressFamily) -> std::io::Result<cap_std::net::UdpSocket>371 pub fn udp_socket(family: AddressFamily) -> std::io::Result<cap_std::net::UdpSocket> {
372 // Delegate socket creation to cap_net_ext. They handle a couple of things for us:
373 // - On Windows: call WSAStartup if not done before.
374 // - Set the NONBLOCK and CLOEXEC flags. Either immediately during socket creation,
375 // or afterwards using ioctl or fcntl. Exact method depends on the platform.
376
377 let socket = cap_std::net::UdpSocket::new(family, Blocking::No)?;
378 Ok(socket)
379 }
380
udp_bind(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), ErrorCode>381 pub fn udp_bind(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), ErrorCode> {
382 bind(sockfd, &addr).map_err(|err| match err {
383 // See: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-bind#:~:text=WSAENOBUFS
384 // Windows returns WSAENOBUFS when the ephemeral ports have been exhausted.
385 #[cfg(windows)]
386 Errno::NOBUFS => ErrorCode::AddressInUse,
387 // From https://pubs.opengroup.org/onlinepubs/9699919799/functions/bind.html:
388 // > [EAFNOSUPPORT] The specified address is not a valid address for the address family of the specified socket
389 //
390 // The most common reasons for this error should have already
391 // been handled by our own validation slightly higher up in this
392 // function. This error mapping is here just in case there is
393 // an edge case we didn't catch.
394 Errno::AFNOSUPPORT => ErrorCode::InvalidArgument,
395 _ => err.into(),
396 })
397 }
398
udp_connect(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), Errno>399 pub fn udp_connect(sockfd: impl AsFd, addr: SocketAddr) -> Result<(), Errno> {
400 match connect(sockfd.as_fd(), &addr) {
401 // When connecting a UDP socket, the OS looks up the best route to the
402 // remote address and selects an appropriate outgoing interface.
403 // If the new destination routes through an interface different than the
404 // previously selected interface, most operating systems will
405 // automatically update the socket's local address to match that route.
406 //
407 // Linux however doesn't do that automatically and we manually
408 // dissolve the existing association and then connect again to the
409 // new destination.
410 #[cfg(target_os = "linux")]
411 Err(Errno::INVAL) => {
412 _ = udp_disconnect(sockfd.as_fd());
413 return connect(sockfd.as_fd(), &addr);
414 }
415 r => r,
416 }
417 }
418
udp_disconnect(sockfd: impl AsFd) -> Result<(), Errno>419 pub fn udp_disconnect(sockfd: impl AsFd) -> Result<(), Errno> {
420 match connect_unspec(sockfd) {
421 // BSD platforms return an error even if the UDP socket was disconnected successfully.
422 //
423 // MacOS was kind enough to document this: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/connect.2.html
424 // > Datagram sockets may dissolve the association by connecting to an
425 // > invalid address, such as a null address or an address with the address
426 // > family set to AF_UNSPEC (the error EAFNOSUPPORT will be harmlessly
427 // > returned).
428 //
429 // ... except that this appears to be incomplete, because experiments
430 // have shown that MacOS actually returns EINVAL, depending on the
431 // address family of the socket.
432 #[cfg(target_os = "macos")]
433 Err(Errno::INVAL | Errno::AFNOSUPPORT) => Ok(()),
434 r => r,
435 }
436 }
437
parse_host(name: &str) -> Result<url::Host, ErrorCode>438 pub fn parse_host(name: &str) -> Result<url::Host, ErrorCode> {
439 // `url::Host::parse` serves us two functions:
440 // 1. validate the input is a valid domain name or IP,
441 // 2. convert unicode domains to punycode.
442 match url::Host::parse(&name) {
443 Ok(host) => Ok(host),
444
445 // `url::Host::parse` doesn't understand bare IPv6 addresses without [brackets]
446 Err(_) => {
447 if let Ok(addr) = Ipv6Addr::from_str(name) {
448 Ok(url::Host::Ipv6(addr))
449 } else {
450 Err(ErrorCode::InvalidArgument)
451 }
452 }
453 }
454 }
455
456 #[cfg(feature = "p3")]
implicit_bind_addr(family: SocketAddressFamily) -> SocketAddr457 pub fn implicit_bind_addr(family: SocketAddressFamily) -> SocketAddr {
458 let ip = match family {
459 SocketAddressFamily::Ipv4 => IpAddr::V4(Ipv4Addr::UNSPECIFIED),
460 SocketAddressFamily::Ipv6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED),
461 };
462 SocketAddr::new(ip, 0)
463 }
464