diff --git a/Cargo.toml b/Cargo.toml index 5d831f2d..05eb5fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,11 @@ http-body = "1.0.0" bytes = "1.7.1" pin-project-lite = "0.2.4" futures-channel = { version = "0.3", optional = true } -socket2 = { version = "0.5", optional = true, features = ["all"] } +socket2 = { version = "0.5.9", optional = true, features = ["all"] } tracing = { version = "0.1", default-features = false, features = ["std"], optional = true } tokio = { version = "1", optional = true, default-features = false } tower-service = { version = "0.3", optional = true } +libc = { version = "0.2", optional = true } [dev-dependencies] hyper = { version = "1.4.0", features = ["full"] } @@ -59,7 +60,7 @@ full = [ ] client = ["hyper/client", "dep:tracing", "dep:futures-channel", "dep:tower-service"] -client-legacy = ["client", "dep:socket2", "tokio/sync"] +client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc"] server = ["hyper/server"] server-auto = ["server", "http1", "http2"] diff --git a/src/client/legacy/connect/http.rs b/src/client/legacy/connect/http.rs index 4a6bd354..1e4df192 100644 --- a/src/client/legacy/connect/http.rs +++ b/src/client/legacy/connect/http.rs @@ -78,6 +78,16 @@ struct Config { recv_buffer_size: Option, #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] interface: Option, + #[cfg(any( + target_os = "illumos", + target_os = "ios", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] + interface: Option, #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] tcp_user_timeout: Option, } @@ -226,7 +236,18 @@ impl HttpConnector { reuse_address: false, send_buffer_size: None, recv_buffer_size: None, - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + #[cfg(any( + target_os = "android", + target_os = "fuchsia", + target_os = "illumos", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] interface: None, #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] tcp_user_timeout: None, @@ -353,22 +374,55 @@ impl HttpConnector { self } - /// Sets the value for the `SO_BINDTODEVICE` option on this socket. + /// Sets the name of the interface to bind sockets produced by this + /// connector. + /// + /// On Linux, this sets the `SO_BINDTODEVICE` option on this socket (see + /// [`man 7 socket`] for details). On macOS (and macOS-derived systems like + /// iOS), illumos, and Solaris, this will instead use the `IP_BOUND_IF` + /// socket option (see [`man 7p ip`]). /// /// If a socket is bound to an interface, only packets received from that particular /// interface are processed by the socket. Note that this only works for some socket - /// types, particularly AF_INET sockets. + /// types, particularly `AF_INET`` sockets. /// /// On Linux it can be used to specify a [VRF], but the binary needs /// to either have `CAP_NET_RAW` or to be run as root. /// - /// This function is only available on Android、Fuchsia and Linux. + /// This function is only available on the following operating systems: + /// - Linux, including Android + /// - Fuchsia + /// - illumos and Solaris + /// - macOS, iOS, visionOS, watchOS, and tvOS /// /// [VRF]: https://www.kernel.org/doc/Documentation/networking/vrf.txt - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + /// [`man 7 socket`] https://man7.org/linux/man-pages/man7/socket.7.html + /// [`man 7p ip`]: https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html + #[cfg(any( + target_os = "android", + target_os = "fuchsia", + target_os = "illumos", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] #[inline] pub fn set_interface>(&mut self, interface: S) -> &mut Self { - self.config_mut().interface = Some(interface.into()); + let interface = interface.into(); + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] + { + self.config_mut().interface = Some(interface); + } + #[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))] + { + let interface = std::ffi::CString::new(interface) + .expect("interface name should not have nulls in it"); + self.config_mut().interface = Some(interface); + } self } @@ -775,12 +829,57 @@ fn connect( } } - #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] // That this only works for some socket types, particularly AF_INET sockets. + #[cfg(any( + target_os = "android", + target_os = "fuchsia", + target_os = "illumos", + target_os = "ios", + target_os = "linux", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] if let Some(interface) = &config.interface { + // On Linux-like systems, set the interface to bind using + // `SO_BINDTODEVICE`. + #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] socket .bind_device(Some(interface.as_bytes())) .map_err(ConnectError::m("tcp bind interface error"))?; + + // On macOS-like and Solaris-like systems, we instead use `IP_BOUND_IF`. + // This socket option desires an integer index for the interface, so we + // must first determine the index of the requested interface name using + // `if_nametoindex`. + #[cfg(any( + target_os = "illumos", + target_os = "ios", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] + { + let idx = unsafe { libc::if_nametoindex(interface.as_ptr()) }; + let idx = std::num::NonZeroU32::new(idx).ok_or_else(|| { + // If the index is 0, check errno and return an I/O error. + ConnectError::new( + "error converting interface name to index", + io::Error::last_os_error(), + ) + })?; + // Different setsockopt calls are necessary depending on whether the + // address is IPv4 or IPv6. + match addr { + SocketAddr::V4(_) => socket.bind_device_by_index_v4(Some(idx)), + SocketAddr::V6(_) => socket.bind_device_by_index_v6(Some(idx)), + } + .map_err(ConnectError::m("tcp bind interface error"))?; + } } #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] @@ -1200,6 +1299,16 @@ mod tests { target_os = "linux" ))] interface: None, + #[cfg(any( + target_os = "illumos", + target_os = "ios", + target_os = "macos", + target_os = "solaris", + target_os = "tvos", + target_os = "visionos", + target_os = "watchos", + ))] + interface: None, #[cfg(any( target_os = "android", target_os = "fuchsia",