Skip to content

enable HttpConnector::interface on macOS and Solarish systems #176

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down Expand Up @@ -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"]
Expand Down
123 changes: 116 additions & 7 deletions src/client/legacy/connect/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ struct Config {
recv_buffer_size: Option<usize>,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
interface: Option<String>,
#[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<std::ffi::CString>,
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
tcp_user_timeout: Option<Duration>,
}
Expand Down Expand Up @@ -226,7 +236,18 @@ impl<R> HttpConnector<R> {
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,
Expand Down Expand Up @@ -353,22 +374,55 @@ impl<R> HttpConnector<R> {
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<S: Into<String>>(&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
}

Expand Down Expand Up @@ -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"))]
Expand Down Expand Up @@ -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",
Expand Down