From 000d5f63da125b206276622493c23eecc9249030 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 26 Feb 2025 14:13:18 -0800 Subject: [PATCH 1/5] add `HttpConnector::interface` on solarish systems --- src/client/legacy/connect/http.rs | 113 ++++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/src/client/legacy/connect/http.rs b/src/client/legacy/connect/http.rs index 4a6bd354..4ebe2ada 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::from(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(idx), + SocketAddr::V6(_) => socket.bind_device_by_index_v6(idx), + } + .map_err("tcp bind interface error")?; + } } #[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))] From 2461067c7717b04e9f47f0575d381804257bb12b Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Thu, 20 Mar 2025 10:53:03 -0700 Subject: [PATCH 2/5] patch `socket2` dep for unreleased illumos support This points the `socket2` dependency at rust-lang/socket2@d95e91462935a5252708a9df0babf29ad31c66f3 temporarily so that we can use these APIs on Solarish operating systems. --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 5d831f2d..d678e324 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,3 +88,8 @@ required-features = ["server", "http1", "tokio"] [[example]] name = "server_graceful" required-features = ["tokio", "server-graceful", "server-auto"] + +[patch.crates-io] +# TODO: remove this patch once https://github.com/rust-lang/socket2/pull/566 is +# published +socket2 = { git = "https://github.com/rust-lang/socket2", rev = "d95e91462935a5252708a9df0babf29ad31c66f3" } From acc8ef0031ff4be546e4a826a20262923ebaf2d3 Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 26 Mar 2025 11:33:37 -0700 Subject: [PATCH 3/5] fixup --- Cargo.toml | 3 ++- src/client/legacy/connect/http.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d678e324..7dfe59ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ socket2 = { version = "0.5", 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 4ebe2ada..356cefbe 100644 --- a/src/client/legacy/connect/http.rs +++ b/src/client/legacy/connect/http.rs @@ -865,7 +865,7 @@ fn connect( ))] { let idx = unsafe { libc::if_nametoindex(interface.as_ptr()) }; - let idx = std::num::NonZeroU32::from(idx).ok_or_else(|| { + 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", @@ -878,7 +878,7 @@ fn connect( SocketAddr::V4(_) => socket.bind_device_by_index_v4(idx), SocketAddr::V6(_) => socket.bind_device_by_index_v6(idx), } - .map_err("tcp bind interface error")?; + .map_err(ConnectError::m("tcp bind interface error"))?; } } From 3a90fa3a95da368f529c6337d227f5e7485315fb Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Wed, 26 Mar 2025 12:58:32 -0700 Subject: [PATCH 4/5] fixup --- src/client/legacy/connect/http.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/client/legacy/connect/http.rs b/src/client/legacy/connect/http.rs index 356cefbe..1e4df192 100644 --- a/src/client/legacy/connect/http.rs +++ b/src/client/legacy/connect/http.rs @@ -875,8 +875,8 @@ fn connect( // Different setsockopt calls are necessary depending on whether the // address is IPv4 or IPv6. match addr { - SocketAddr::V4(_) => socket.bind_device_by_index_v4(idx), - SocketAddr::V6(_) => socket.bind_device_by_index_v6(idx), + 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"))?; } @@ -1299,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", From a716f860b51a0c4cf8328c7cb12779f8b899da8c Mon Sep 17 00:00:00 2001 From: Eliza Weisman Date: Mon, 31 Mar 2025 09:39:31 -0700 Subject: [PATCH 5/5] Use released `socket2` v0.5.6 --- Cargo.toml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7dfe59ee..05eb5fb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ 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 } @@ -89,8 +89,3 @@ required-features = ["server", "http1", "tokio"] [[example]] name = "server_graceful" required-features = ["tokio", "server-graceful", "server-auto"] - -[patch.crates-io] -# TODO: remove this patch once https://github.com/rust-lang/socket2/pull/566 is -# published -socket2 = { git = "https://github.com/rust-lang/socket2", rev = "d95e91462935a5252708a9df0babf29ad31c66f3" }