Skip to content

Commit 5831ace

Browse files
authored
feat(client): enable HttpConnector::interface on macOS and Solarish systems (#176)
1 parent dc03ad4 commit 5831ace

File tree

2 files changed

+119
-9
lines changed

2 files changed

+119
-9
lines changed

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ http-body = "1.0.0"
2525
bytes = "1.7.1"
2626
pin-project-lite = "0.2.4"
2727
futures-channel = { version = "0.3", optional = true }
28-
socket2 = { version = "0.5", optional = true, features = ["all"] }
28+
socket2 = { version = "0.5.9", optional = true, features = ["all"] }
2929
tracing = { version = "0.1", default-features = false, features = ["std"], optional = true }
3030
tokio = { version = "1", optional = true, default-features = false }
3131
tower-service = { version = "0.3", optional = true }
32+
libc = { version = "0.2", optional = true }
3233

3334
[dev-dependencies]
3435
hyper = { version = "1.4.0", features = ["full"] }
@@ -59,7 +60,7 @@ full = [
5960
]
6061

6162
client = ["hyper/client", "dep:tracing", "dep:futures-channel", "dep:tower-service"]
62-
client-legacy = ["client", "dep:socket2", "tokio/sync"]
63+
client-legacy = ["client", "dep:socket2", "tokio/sync", "dep:libc"]
6364

6465
server = ["hyper/server"]
6566
server-auto = ["server", "http1", "http2"]

src/client/legacy/connect/http.rs

Lines changed: 116 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ struct Config {
7878
recv_buffer_size: Option<usize>,
7979
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
8080
interface: Option<String>,
81+
#[cfg(any(
82+
target_os = "illumos",
83+
target_os = "ios",
84+
target_os = "macos",
85+
target_os = "solaris",
86+
target_os = "tvos",
87+
target_os = "visionos",
88+
target_os = "watchos",
89+
))]
90+
interface: Option<std::ffi::CString>,
8191
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
8292
tcp_user_timeout: Option<Duration>,
8393
}
@@ -226,7 +236,18 @@ impl<R> HttpConnector<R> {
226236
reuse_address: false,
227237
send_buffer_size: None,
228238
recv_buffer_size: None,
229-
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
239+
#[cfg(any(
240+
target_os = "android",
241+
target_os = "fuchsia",
242+
target_os = "illumos",
243+
target_os = "ios",
244+
target_os = "linux",
245+
target_os = "macos",
246+
target_os = "solaris",
247+
target_os = "tvos",
248+
target_os = "visionos",
249+
target_os = "watchos",
250+
))]
230251
interface: None,
231252
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
232253
tcp_user_timeout: None,
@@ -353,22 +374,55 @@ impl<R> HttpConnector<R> {
353374
self
354375
}
355376

356-
/// Sets the value for the `SO_BINDTODEVICE` option on this socket.
377+
/// Sets the name of the interface to bind sockets produced by this
378+
/// connector.
379+
///
380+
/// On Linux, this sets the `SO_BINDTODEVICE` option on this socket (see
381+
/// [`man 7 socket`] for details). On macOS (and macOS-derived systems like
382+
/// iOS), illumos, and Solaris, this will instead use the `IP_BOUND_IF`
383+
/// socket option (see [`man 7p ip`]).
357384
///
358385
/// If a socket is bound to an interface, only packets received from that particular
359386
/// interface are processed by the socket. Note that this only works for some socket
360-
/// types, particularly AF_INET sockets.
387+
/// types, particularly `AF_INET`` sockets.
361388
///
362389
/// On Linux it can be used to specify a [VRF], but the binary needs
363390
/// to either have `CAP_NET_RAW` or to be run as root.
364391
///
365-
/// This function is only available on Android、Fuchsia and Linux.
392+
/// This function is only available on the following operating systems:
393+
/// - Linux, including Android
394+
/// - Fuchsia
395+
/// - illumos and Solaris
396+
/// - macOS, iOS, visionOS, watchOS, and tvOS
366397
///
367398
/// [VRF]: https://www.kernel.org/doc/Documentation/networking/vrf.txt
368-
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
399+
/// [`man 7 socket`] https://man7.org/linux/man-pages/man7/socket.7.html
400+
/// [`man 7p ip`]: https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html
401+
#[cfg(any(
402+
target_os = "android",
403+
target_os = "fuchsia",
404+
target_os = "illumos",
405+
target_os = "ios",
406+
target_os = "linux",
407+
target_os = "macos",
408+
target_os = "solaris",
409+
target_os = "tvos",
410+
target_os = "visionos",
411+
target_os = "watchos",
412+
))]
369413
#[inline]
370414
pub fn set_interface<S: Into<String>>(&mut self, interface: S) -> &mut Self {
371-
self.config_mut().interface = Some(interface.into());
415+
let interface = interface.into();
416+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
417+
{
418+
self.config_mut().interface = Some(interface);
419+
}
420+
#[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))]
421+
{
422+
let interface = std::ffi::CString::new(interface)
423+
.expect("interface name should not have nulls in it");
424+
self.config_mut().interface = Some(interface);
425+
}
372426
self
373427
}
374428

@@ -789,12 +843,57 @@ fn connect(
789843
}
790844
}
791845

792-
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
793846
// That this only works for some socket types, particularly AF_INET sockets.
847+
#[cfg(any(
848+
target_os = "android",
849+
target_os = "fuchsia",
850+
target_os = "illumos",
851+
target_os = "ios",
852+
target_os = "linux",
853+
target_os = "macos",
854+
target_os = "solaris",
855+
target_os = "tvos",
856+
target_os = "visionos",
857+
target_os = "watchos",
858+
))]
794859
if let Some(interface) = &config.interface {
860+
// On Linux-like systems, set the interface to bind using
861+
// `SO_BINDTODEVICE`.
862+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
795863
socket
796864
.bind_device(Some(interface.as_bytes()))
797865
.map_err(ConnectError::m("tcp bind interface error"))?;
866+
867+
// On macOS-like and Solaris-like systems, we instead use `IP_BOUND_IF`.
868+
// This socket option desires an integer index for the interface, so we
869+
// must first determine the index of the requested interface name using
870+
// `if_nametoindex`.
871+
#[cfg(any(
872+
target_os = "illumos",
873+
target_os = "ios",
874+
target_os = "macos",
875+
target_os = "solaris",
876+
target_os = "tvos",
877+
target_os = "visionos",
878+
target_os = "watchos",
879+
))]
880+
{
881+
let idx = unsafe { libc::if_nametoindex(interface.as_ptr()) };
882+
let idx = std::num::NonZeroU32::new(idx).ok_or_else(|| {
883+
// If the index is 0, check errno and return an I/O error.
884+
ConnectError::new(
885+
"error converting interface name to index",
886+
io::Error::last_os_error(),
887+
)
888+
})?;
889+
// Different setsockopt calls are necessary depending on whether the
890+
// address is IPv4 or IPv6.
891+
match addr {
892+
SocketAddr::V4(_) => socket.bind_device_by_index_v4(Some(idx)),
893+
SocketAddr::V6(_) => socket.bind_device_by_index_v6(Some(idx)),
894+
}
895+
.map_err(ConnectError::m("tcp bind interface error"))?;
896+
}
798897
}
799898

800899
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
@@ -1214,6 +1313,16 @@ mod tests {
12141313
target_os = "linux"
12151314
))]
12161315
interface: None,
1316+
#[cfg(any(
1317+
target_os = "illumos",
1318+
target_os = "ios",
1319+
target_os = "macos",
1320+
target_os = "solaris",
1321+
target_os = "tvos",
1322+
target_os = "visionos",
1323+
target_os = "watchos",
1324+
))]
1325+
interface: None,
12171326
#[cfg(any(
12181327
target_os = "android",
12191328
target_os = "fuchsia",

0 commit comments

Comments
 (0)