Skip to content

Commit bcbfbf8

Browse files
authored
feat(client): expose TCP socket Happy Eyeballs timeout API (#844)
1 parent e7bab63 commit bcbfbf8

4 files changed

Lines changed: 164 additions & 146 deletions

File tree

src/client/http/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ struct Config {
134134
tcp_keepalive_retries: Option<u32>,
135135
tcp_send_buffer_size: Option<usize>,
136136
tcp_recv_buffer_size: Option<usize>,
137+
tcp_happy_eyeballs_timeout: Option<Duration>,
137138
tcp_connect_options: Option<TcpConnectOptions>,
138139
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
139140
tcp_user_timeout: Option<Duration>,
@@ -203,6 +204,7 @@ impl ClientBuilder {
203204
tcp_reuse_address: false,
204205
tcp_send_buffer_size: None,
205206
tcp_recv_buffer_size: None,
207+
tcp_happy_eyeballs_timeout: Some(Duration::from_millis(300)),
206208
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
207209
tcp_user_timeout: None,
208210
proxies: Vec::new(),
@@ -290,6 +292,7 @@ impl ClientBuilder {
290292
http.set_nodelay(config.tcp_nodelay);
291293
http.set_send_buffer_size(config.tcp_send_buffer_size);
292294
http.set_recv_buffer_size(config.tcp_recv_buffer_size);
295+
http.set_happy_eyeballs_timeout(config.tcp_happy_eyeballs_timeout);
293296
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
294297
http.set_tcp_user_timeout(config.tcp_user_timeout);
295298
};
@@ -977,6 +980,27 @@ impl ClientBuilder {
977980
self
978981
}
979982

983+
/// Set timeout for [RFC 6555 (Happy Eyeballs)][RFC 6555] algorithm.
984+
///
985+
/// If hostname resolves to both IPv4 and IPv6 addresses and connection
986+
/// cannot be established using preferred address family before timeout
987+
/// elapses, then connector will in parallel attempt connection using other
988+
/// address family.
989+
///
990+
/// If `None`, parallel connection attempts are disabled.
991+
///
992+
/// Default is 300 milliseconds.
993+
///
994+
/// [RFC 6555]: https://tools.ietf.org/html/rfc6555
995+
#[inline]
996+
pub fn tcp_happy_eyeballs_timeout<D>(mut self, val: D) -> ClientBuilder
997+
where
998+
D: Into<Option<Duration>>,
999+
{
1000+
self.config.tcp_happy_eyeballs_timeout = val.into();
1001+
self
1002+
}
1003+
9801004
/// Bind to a local IP Address.
9811005
///
9821006
/// # Example

src/core/client/connect/http.rs

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
future::Future,
55
io,
66
marker::PhantomData,
7-
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
7+
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
88
pin::Pin,
99
sync::Arc,
1010
task::{self, Poll, ready},
@@ -24,7 +24,7 @@ use super::{
2424
Connected, Connection,
2525
dns::{self, GaiResolver, Resolve, resolve},
2626
};
27-
use crate::core::{client::connect::options::TcpConnectOptions, error::BoxError, rt::TokioIo};
27+
use crate::core::{error::BoxError, rt::TokioIo};
2828

2929
/// A connector for the `http` scheme.
3030
///
@@ -66,6 +66,143 @@ pub struct HttpInfo {
6666
local_addr: SocketAddr,
6767
}
6868

69+
/// Options for configuring a TCP network connection.
70+
///
71+
/// `TcpConnectOptions` allows fine-grained control over how TCP sockets
72+
/// are created and connected. It can be used to:
73+
///
74+
/// - Bind a socket to a specific **network interface**
75+
/// - Bind to a **local IPv4 or IPv6 address**
76+
///
77+
/// This is especially useful for scenarios involving:
78+
/// - Virtual routing tables (e.g. Linux VRFs)
79+
/// - Multiple NICs (network interface cards)
80+
/// - Explicit source IP routing or firewall rules
81+
///
82+
/// Platform-specific behavior is handled internally, with the interface binding
83+
/// mechanism differing across Unix-like systems.
84+
///
85+
/// # Platform Notes
86+
///
87+
/// ## Interface binding (`set_interface`)
88+
///
89+
/// - **Linux / Android / Fuchsia**: uses the `SO_BINDTODEVICE` socket option See [`man 7 socket`](https://man7.org/linux/man-pages/man7/socket.7.html)
90+
///
91+
/// - **macOS / iOS / tvOS / watchOS / visionOS / illumos / Solaris**: uses the `IP_BOUND_IF` socket
92+
/// option See [`man 7p ip`](https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html)
93+
///
94+
/// Binding to an interface ensures that:
95+
/// - **Outgoing packets** are sent through the specified interface
96+
/// - **Incoming packets** are only accepted if received via that interface
97+
///
98+
/// ❗ This only applies to certain socket types (e.g. `AF_INET`), and may require
99+
/// elevated permissions (e.g. `CAP_NET_RAW` on Linux).
100+
#[derive(Debug, Clone, Hash, PartialEq, Eq, Default)]
101+
pub struct TcpConnectOptions {
102+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
103+
pub(super) interface: Option<std::borrow::Cow<'static, str>>,
104+
#[cfg(any(
105+
target_os = "illumos",
106+
target_os = "ios",
107+
target_os = "macos",
108+
target_os = "solaris",
109+
target_os = "tvos",
110+
target_os = "visionos",
111+
target_os = "watchos",
112+
))]
113+
pub(super) interface: Option<std::ffi::CString>,
114+
pub(super) local_ipv4: Option<Ipv4Addr>,
115+
pub(super) local_ipv6: Option<Ipv6Addr>,
116+
}
117+
118+
impl TcpConnectOptions {
119+
/// Sets the name of the network interface to bind the socket to.
120+
///
121+
/// ## Platform behavior
122+
/// - On Linux/Fuchsia/Android: sets `SO_BINDTODEVICE`
123+
/// - On macOS/illumos/Solaris/iOS/etc.: sets `IP_BOUND_IF`
124+
///
125+
/// If `interface` is `None`, the socket will not be explicitly bound to any device.
126+
///
127+
/// # Errors
128+
///
129+
/// On platforms that require a `CString` (e.g. macOS), this will return an error if the
130+
/// interface name contains an internal null byte (`\0`), which is invalid in C strings.
131+
///
132+
/// # See Also
133+
/// - [VRF documentation](https://www.kernel.org/doc/Documentation/networking/vrf.txt)
134+
/// - [`man 7 socket`](https://man7.org/linux/man-pages/man7/socket.7.html)
135+
/// - [`man 7p ip`](https://docs.oracle.com/cd/E86824_01/html/E54777/ip-7p.html)
136+
#[cfg(any(
137+
target_os = "android",
138+
target_os = "fuchsia",
139+
target_os = "illumos",
140+
target_os = "ios",
141+
target_os = "linux",
142+
target_os = "macos",
143+
target_os = "solaris",
144+
target_os = "tvos",
145+
target_os = "visionos",
146+
target_os = "watchos",
147+
))]
148+
#[inline]
149+
pub fn set_interface<S>(&mut self, interface: S) -> &mut Self
150+
where
151+
S: Into<Option<std::borrow::Cow<'static, str>>>,
152+
{
153+
#[cfg(any(target_os = "android", target_os = "fuchsia", target_os = "linux"))]
154+
{
155+
self.interface = interface.into();
156+
}
157+
158+
#[cfg(not(any(target_os = "android", target_os = "fuchsia", target_os = "linux")))]
159+
{
160+
self.interface = interface
161+
.into()
162+
.and_then(|iface| std::ffi::CString::new(iface.into_owned()).ok());
163+
}
164+
165+
self
166+
}
167+
168+
/// Sets the local address the socket will bind to before connecting.
169+
///
170+
/// If an address is provided, the socket will explicitly bind to it,
171+
/// ensuring that the outgoing connection uses this address as the source.
172+
///
173+
/// - If an `Ipv4Addr` is given, it will set `local_ipv4` and clear `local_ipv6`.
174+
/// - If an `Ipv6Addr` is given, it will set `local_ipv6` and clear `local_ipv4`.
175+
///
176+
/// If `None` is passed, both addresses are cleared and the OS will choose automatically.
177+
#[inline]
178+
pub fn set_local_address(&mut self, local_addr: Option<IpAddr>) {
179+
match local_addr {
180+
Some(IpAddr::V4(a)) => {
181+
self.local_ipv4 = Some(a);
182+
}
183+
Some(IpAddr::V6(a)) => {
184+
self.local_ipv6 = Some(a);
185+
}
186+
_ => {}
187+
};
188+
}
189+
190+
/// Sets both local IPv4 and IPv6 addresses explicitly.
191+
///
192+
/// Use this method to assign both address families independently.
193+
///
194+
/// If either argument is `None`, the socket will not be bound for that protocol.
195+
#[inline]
196+
pub fn set_local_addresses(
197+
&mut self,
198+
local_ipv4: Option<Ipv4Addr>,
199+
local_ipv6: Option<Ipv6Addr>,
200+
) {
201+
self.local_ipv4 = local_ipv4;
202+
self.local_ipv6 = local_ipv6;
203+
}
204+
}
205+
69206
#[derive(Clone)]
70207
struct Config {
71208
connect_timeout: Option<Duration>,
@@ -310,7 +447,6 @@ impl<R> HttpConnector<R> {
310447
/// Default is 300 milliseconds.
311448
///
312449
/// [RFC 6555]: https://tools.ietf.org/html/rfc6555
313-
#[allow(unused)]
314450
#[inline]
315451
pub fn set_happy_eyeballs_timeout(&mut self, dur: Option<Duration>) {
316452
self.config_mut().happy_eyeballs_timeout = dur;

src/core/client/connect/mod.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
pub mod dns;
44
mod http;
5-
mod options;
65
pub mod proxy;
76

87
use std::{
@@ -15,10 +14,7 @@ use std::{
1514

1615
use ::http::Extensions;
1716

18-
pub use self::{
19-
http::{HttpConnector, HttpInfo},
20-
options::TcpConnectOptions,
21-
};
17+
pub use self::http::{HttpConnector, HttpInfo, TcpConnectOptions};
2218

2319
/// Describes a type returned by a connector.
2420
pub trait Connection {

src/core/client/connect/options.rs

Lines changed: 0 additions & 138 deletions
This file was deleted.

0 commit comments

Comments
 (0)