Skip to content

Commit 0121482

Browse files
authored
feat(client): add set_interface() connector option (#26)
1 parent 3c37c5b commit 0121482

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

src/client/connect/http.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ struct Config {
7575
reuse_address: bool,
7676
send_buffer_size: Option<usize>,
7777
recv_buffer_size: Option<usize>,
78+
interface: Option<String>,
7879
}
7980

8081
#[derive(Default, Debug, Clone, Copy)]
@@ -165,6 +166,7 @@ impl<R> HttpConnector<R> {
165166
reuse_address: false,
166167
send_buffer_size: None,
167168
recv_buffer_size: None,
169+
interface: None,
168170
}),
169171
resolver,
170172
}
@@ -288,6 +290,25 @@ impl<R> HttpConnector<R> {
288290
self
289291
}
290292

293+
/// Sets the value for the `SO_BINDTODEVICE` option on this socket.
294+
///
295+
/// If a socket is bound to an interface, only packets received from that particular
296+
/// interface are processed by the socket. Note that this only works for some socket
297+
/// types, particularly AF_INET sockets.
298+
///
299+
/// On Linux it can be used to specify a [VRF], but the binary needs
300+
/// to either have `CAP_NET_RAW` or to be run as root.
301+
///
302+
/// This function is only available on Android、Fuchsia and Linux.
303+
///
304+
/// [VRF]: https://www.kernel.org/doc/Documentation/networking/vrf.txt
305+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
306+
#[inline]
307+
pub fn set_interface<S: Into<String>>(&mut self, interface: S) -> &mut Self {
308+
self.config_mut().interface = Some(interface.into());
309+
self
310+
}
311+
291312
// private
292313

293314
fn config_mut(&mut self) -> &mut Config {
@@ -673,6 +694,14 @@ fn connect(
673694
}
674695
}
675696

697+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
698+
// That this only works for some socket types, particularly AF_INET sockets.
699+
if let Some(interface) = &config.interface {
700+
socket
701+
.bind_device(Some(interface.as_bytes()))
702+
.map_err(ConnectError::m("tcp bind interface error"))?;
703+
}
704+
676705
bind_local_address(
677706
&socket,
678707
addr,
@@ -828,6 +857,14 @@ mod tests {
828857
(ip_v4, ip_v6)
829858
}
830859

860+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
861+
fn default_interface() -> Option<String> {
862+
pnet_datalink::interfaces()
863+
.iter()
864+
.find(|e| e.is_up() && !e.is_loopback() && !e.ips.is_empty())
865+
.map(|e| e.name.clone())
866+
}
867+
831868
#[tokio::test]
832869
#[cfg_attr(miri, ignore)]
833870
async fn test_errors_missing_scheme() {
@@ -877,6 +914,57 @@ mod tests {
877914
}
878915
}
879916

917+
// NOTE: pnet crate that we use in this test doesn't compile on Windows
918+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
919+
#[tokio::test]
920+
#[ignore = "setting `SO_BINDTODEVICE` requires the `CAP_NET_RAW` capability (works when running as root)"]
921+
async fn interface() {
922+
use socket2::{Domain, Protocol, Socket, Type};
923+
use std::net::TcpListener;
924+
925+
let interface: Option<String> = default_interface();
926+
927+
let server4 = TcpListener::bind("127.0.0.1:0").unwrap();
928+
let port = server4.local_addr().unwrap().port();
929+
930+
let server6 = TcpListener::bind(&format!("[::1]:{}", port)).unwrap();
931+
932+
let assert_interface_name =
933+
|dst: String,
934+
server: TcpListener,
935+
bind_iface: Option<String>,
936+
expected_interface: Option<String>| async move {
937+
let mut connector = HttpConnector::new();
938+
if let Some(iface) = bind_iface {
939+
connector.set_interface(iface);
940+
}
941+
942+
connect(connector, dst.parse().unwrap()).await.unwrap();
943+
let domain = Domain::for_address(server.local_addr().unwrap());
944+
let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP)).unwrap();
945+
946+
assert_eq!(
947+
socket.device().unwrap().as_deref(),
948+
expected_interface.as_deref().map(|val| val.as_bytes())
949+
);
950+
};
951+
952+
assert_interface_name(
953+
format!("http://127.0.0.1:{}", port),
954+
server4,
955+
interface.clone(),
956+
interface.clone(),
957+
)
958+
.await;
959+
assert_interface_name(
960+
format!("http://[::1]:{}", port),
961+
server6,
962+
interface.clone(),
963+
interface.clone(),
964+
)
965+
.await;
966+
}
967+
880968
#[test]
881969
#[cfg_attr(not(feature = "__internal_happy_eyeballs_tests"), ignore)]
882970
fn client_happy_eyeballs() {
@@ -1005,6 +1093,7 @@ mod tests {
10051093
enforce_http: false,
10061094
send_buffer_size: None,
10071095
recv_buffer_size: None,
1096+
interface: None,
10081097
};
10091098
let connecting_tcp = ConnectingTcp::new(dns::SocketAddrs::new(addrs), &cfg);
10101099
let start = Instant::now();

0 commit comments

Comments
 (0)