feat(client): implement rfc 6555 (happy eyeballs)
Update client connector to attempt a parallel connection using alternative address family, if connection using preferred address family takes too long. Closes: #1316
This commit is contained in:
committed by
Sean McArthur
parent
5b5e309095
commit
02a9c29e2e
@@ -1,5 +1,5 @@
|
||||
language: rust
|
||||
sudo: false
|
||||
sudo: true # Required for functional IPv6 (forces VM instead of Docker).
|
||||
dist: trusty
|
||||
matrix:
|
||||
fast_finish: true
|
||||
@@ -18,6 +18,13 @@ matrix:
|
||||
cache:
|
||||
apt: true
|
||||
|
||||
before_script:
|
||||
# Add an IPv6 config - see the corresponding Travis issue
|
||||
# https://github.com/travis-ci/travis-ci/issues/8361
|
||||
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then
|
||||
sudo sh -c 'echo 0 > /proc/sys/net/ipv6/conf/all/disable_ipv6';
|
||||
fi
|
||||
|
||||
script:
|
||||
- ./.travis/readme.py
|
||||
- cargo build $FEATURES
|
||||
|
||||
@@ -386,7 +386,7 @@ mod http {
|
||||
use std::mem;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use futures::{Async, Poll};
|
||||
use futures::future::{Executor, ExecuteError};
|
||||
@@ -396,6 +396,7 @@ mod http {
|
||||
use net2::TcpBuilder;
|
||||
use tokio_reactor::Handle;
|
||||
use tokio_tcp::{TcpStream, ConnectFuture};
|
||||
use tokio_timer::Delay;
|
||||
|
||||
use super::super::dns;
|
||||
|
||||
@@ -444,6 +445,7 @@ mod http {
|
||||
keep_alive_timeout: Option<Duration>,
|
||||
nodelay: bool,
|
||||
local_address: Option<IpAddr>,
|
||||
happy_eyeballs_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl HttpConnector {
|
||||
@@ -481,6 +483,7 @@ mod http {
|
||||
keep_alive_timeout: None,
|
||||
nodelay: false,
|
||||
local_address: None,
|
||||
happy_eyeballs_timeout: Some(Duration::from_millis(300)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,6 +522,23 @@ mod http {
|
||||
pub fn set_local_address(&mut self, addr: Option<IpAddr>) {
|
||||
self.local_address = addr;
|
||||
}
|
||||
|
||||
/// Set timeout for [RFC 6555 (Happy Eyeballs)][RFC 6555] algorithm.
|
||||
///
|
||||
/// If hostname resolves to both IPv4 and IPv6 addresses and connection
|
||||
/// cannot be established using preferred address family before timeout
|
||||
/// elapses, then connector will in parallel attempt connection using other
|
||||
/// address family.
|
||||
///
|
||||
/// If `None`, parallel connection attempts are disabled.
|
||||
///
|
||||
/// Default is 300 milliseconds.
|
||||
///
|
||||
/// [RFC 6555]: https://tools.ietf.org/html/rfc6555
|
||||
#[inline]
|
||||
pub fn set_happy_eyeballs_timeout(&mut self, dur: Option<Duration>) {
|
||||
self.happy_eyeballs_timeout = dur;
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for HttpConnector {
|
||||
@@ -564,6 +584,7 @@ mod http {
|
||||
handle: self.handle.clone(),
|
||||
keep_alive_timeout: self.keep_alive_timeout,
|
||||
nodelay: self.nodelay,
|
||||
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -575,6 +596,7 @@ mod http {
|
||||
handle: handle.clone(),
|
||||
keep_alive_timeout: None,
|
||||
nodelay: false,
|
||||
happy_eyeballs_timeout: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +629,7 @@ mod http {
|
||||
handle: Option<Handle>,
|
||||
keep_alive_timeout: Option<Duration>,
|
||||
nodelay: bool,
|
||||
happy_eyeballs_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
@@ -628,11 +651,8 @@ mod http {
|
||||
// If the host is already an IP addr (v4 or v6),
|
||||
// skip resolving the dns and start connecting right away.
|
||||
if let Some(addrs) = dns::IpAddrs::try_parse(host, port) {
|
||||
state = State::Connecting(ConnectingTcp {
|
||||
addrs: addrs,
|
||||
local_addr: local_addr,
|
||||
current: None
|
||||
})
|
||||
state = State::Connecting(ConnectingTcp::new(
|
||||
local_addr, addrs, self.happy_eyeballs_timeout));
|
||||
} else {
|
||||
let host = mem::replace(host, String::new());
|
||||
let work = dns::Work::new(host, port);
|
||||
@@ -643,11 +663,8 @@ mod http {
|
||||
match try!(future.poll()) {
|
||||
Async::NotReady => return Ok(Async::NotReady),
|
||||
Async::Ready(addrs) => {
|
||||
state = State::Connecting(ConnectingTcp {
|
||||
addrs: addrs,
|
||||
local_addr: local_addr,
|
||||
current: None,
|
||||
})
|
||||
state = State::Connecting(ConnectingTcp::new(
|
||||
local_addr, addrs, self.happy_eyeballs_timeout));
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -676,14 +693,71 @@ mod http {
|
||||
}
|
||||
|
||||
struct ConnectingTcp {
|
||||
addrs: dns::IpAddrs,
|
||||
local_addr: Option<IpAddr>,
|
||||
current: Option<ConnectFuture>,
|
||||
preferred: ConnectingTcpRemote,
|
||||
fallback: Option<ConnectingTcpFallback>,
|
||||
}
|
||||
|
||||
impl ConnectingTcp {
|
||||
fn new(
|
||||
local_addr: Option<IpAddr>,
|
||||
remote_addrs: dns::IpAddrs,
|
||||
fallback_timeout: Option<Duration>,
|
||||
) -> ConnectingTcp {
|
||||
if let Some(fallback_timeout) = fallback_timeout {
|
||||
let (preferred_addrs, fallback_addrs) = remote_addrs.split_by_preference();
|
||||
if fallback_addrs.is_empty() {
|
||||
return ConnectingTcp {
|
||||
local_addr,
|
||||
preferred: ConnectingTcpRemote::new(preferred_addrs),
|
||||
fallback: None,
|
||||
};
|
||||
}
|
||||
|
||||
ConnectingTcp {
|
||||
local_addr,
|
||||
preferred: ConnectingTcpRemote::new(preferred_addrs),
|
||||
fallback: Some(ConnectingTcpFallback {
|
||||
delay: Delay::new(Instant::now() + fallback_timeout),
|
||||
remote: ConnectingTcpRemote::new(fallback_addrs),
|
||||
}),
|
||||
}
|
||||
} else {
|
||||
ConnectingTcp {
|
||||
local_addr,
|
||||
preferred: ConnectingTcpRemote::new(remote_addrs),
|
||||
fallback: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectingTcpFallback {
|
||||
delay: Delay,
|
||||
remote: ConnectingTcpRemote,
|
||||
}
|
||||
|
||||
struct ConnectingTcpRemote {
|
||||
addrs: dns::IpAddrs,
|
||||
current: Option<ConnectFuture>,
|
||||
}
|
||||
|
||||
impl ConnectingTcpRemote {
|
||||
fn new(addrs: dns::IpAddrs) -> Self {
|
||||
Self {
|
||||
addrs,
|
||||
current: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectingTcpRemote {
|
||||
// not a Future, since passing a &Handle to poll
|
||||
fn poll(&mut self, handle: &Option<Handle>) -> Poll<TcpStream, io::Error> {
|
||||
fn poll(
|
||||
&mut self,
|
||||
local_addr: &Option<IpAddr>,
|
||||
handle: &Option<Handle>,
|
||||
) -> Poll<TcpStream, io::Error> {
|
||||
let mut err = None;
|
||||
loop {
|
||||
if let Some(ref mut current) = self.current {
|
||||
@@ -694,14 +768,14 @@ mod http {
|
||||
err = Some(e);
|
||||
if let Some(addr) = self.addrs.next() {
|
||||
debug!("connecting to {}", addr);
|
||||
*current = connect(&addr, &self.local_addr, handle)?;
|
||||
*current = connect(&addr, local_addr, handle)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(addr) = self.addrs.next() {
|
||||
debug!("connecting to {}", addr);
|
||||
self.current = Some(connect(&addr, &self.local_addr, handle)?);
|
||||
self.current = Some(connect(&addr, local_addr, handle)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -710,6 +784,54 @@ mod http {
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectingTcp {
|
||||
// not a Future, since passing a &Handle to poll
|
||||
fn poll(&mut self, handle: &Option<Handle>) -> Poll<TcpStream, io::Error> {
|
||||
match self.fallback.take() {
|
||||
None => self.preferred.poll(&self.local_addr, handle),
|
||||
Some(mut fallback) => match self.preferred.poll(&self.local_addr, handle) {
|
||||
Ok(Async::Ready(stream)) => {
|
||||
// Preferred successful - drop fallback.
|
||||
Ok(Async::Ready(stream))
|
||||
}
|
||||
Ok(Async::NotReady) => match fallback.delay.poll() {
|
||||
Ok(Async::Ready(_)) => match fallback.remote.poll(&self.local_addr, handle) {
|
||||
Ok(Async::Ready(stream)) => {
|
||||
// Fallback successful - drop current preferred,
|
||||
// but keep fallback as new preferred.
|
||||
self.preferred = fallback.remote;
|
||||
Ok(Async::Ready(stream))
|
||||
}
|
||||
Ok(Async::NotReady) => {
|
||||
// Neither preferred nor fallback are ready.
|
||||
self.fallback = Some(fallback);
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
Err(_) => {
|
||||
// Fallback failed - resume with preferred only.
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
},
|
||||
Ok(Async::NotReady) => {
|
||||
// Too early to attempt fallback.
|
||||
self.fallback = Some(fallback);
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
Err(_) => {
|
||||
// Fallback delay failed - resume with preferred only.
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Preferred failed - use fallback as new preferred.
|
||||
self.preferred = fallback.remote;
|
||||
self.preferred.poll(&self.local_addr, handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make this Future unnameable outside of this crate.
|
||||
mod http_connector {
|
||||
use super::*;
|
||||
@@ -783,6 +905,154 @@ mod http {
|
||||
|
||||
assert_eq!(connector.connect(dst).wait().unwrap_err().kind(), io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_happy_eyeballs() {
|
||||
extern crate pretty_env_logger;
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, TcpListener};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use futures::{Async, Poll};
|
||||
use tokio::runtime::current_thread::Runtime;
|
||||
use tokio_reactor::Handle;
|
||||
|
||||
use super::dns;
|
||||
use super::ConnectingTcp;
|
||||
|
||||
let _ = pretty_env_logger::try_init();
|
||||
let server4 = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = server4.local_addr().unwrap();
|
||||
let _server6 = TcpListener::bind(&format!("[::1]:{}", addr.port())).unwrap();
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
|
||||
let local_timeout = Duration::default();
|
||||
let unreachable_v4_timeout = measure_connect(unreachable_ipv4_addr()).1;
|
||||
let unreachable_v6_timeout = measure_connect(unreachable_ipv6_addr()).1;
|
||||
let fallback_timeout = ::std::cmp::max(unreachable_v4_timeout, unreachable_v6_timeout)
|
||||
+ Duration::from_millis(250);
|
||||
|
||||
let scenarios = &[
|
||||
// Fast primary, without fallback.
|
||||
(&[local_ipv4_addr()][..],
|
||||
4, local_timeout, false),
|
||||
(&[local_ipv6_addr()][..],
|
||||
6, local_timeout, false),
|
||||
|
||||
// Fast primary, with (unused) fallback.
|
||||
(&[local_ipv4_addr(), local_ipv6_addr()][..],
|
||||
4, local_timeout, false),
|
||||
(&[local_ipv6_addr(), local_ipv4_addr()][..],
|
||||
6, local_timeout, false),
|
||||
|
||||
// Unreachable + fast primary, without fallback.
|
||||
(&[unreachable_ipv4_addr(), local_ipv4_addr()][..],
|
||||
4, unreachable_v4_timeout, false),
|
||||
(&[unreachable_ipv6_addr(), local_ipv6_addr()][..],
|
||||
6, unreachable_v6_timeout, false),
|
||||
|
||||
// Unreachable + fast primary, with (unused) fallback.
|
||||
(&[unreachable_ipv4_addr(), local_ipv4_addr(), local_ipv6_addr()][..],
|
||||
4, unreachable_v4_timeout, false),
|
||||
(&[unreachable_ipv6_addr(), local_ipv6_addr(), local_ipv4_addr()][..],
|
||||
6, unreachable_v6_timeout, true),
|
||||
|
||||
// Slow primary, with (used) fallback.
|
||||
(&[slow_ipv4_addr(), local_ipv4_addr(), local_ipv6_addr()][..],
|
||||
6, fallback_timeout, false),
|
||||
(&[slow_ipv6_addr(), local_ipv6_addr(), local_ipv4_addr()][..],
|
||||
4, fallback_timeout, true),
|
||||
|
||||
// Slow primary, with (used) unreachable + fast fallback.
|
||||
(&[slow_ipv4_addr(), unreachable_ipv6_addr(), local_ipv6_addr()][..],
|
||||
6, fallback_timeout + unreachable_v6_timeout, false),
|
||||
(&[slow_ipv6_addr(), unreachable_ipv4_addr(), local_ipv4_addr()][..],
|
||||
4, fallback_timeout + unreachable_v4_timeout, true),
|
||||
];
|
||||
|
||||
// Scenarios for IPv6 -> IPv4 fallback require that host can access IPv6 network.
|
||||
// Otherwise, connection to "slow" IPv6 address will error-out immediatelly.
|
||||
let ipv6_accessible = measure_connect(slow_ipv6_addr()).0;
|
||||
|
||||
for &(hosts, family, timeout, needs_ipv6_access) in scenarios {
|
||||
if needs_ipv6_access && !ipv6_accessible {
|
||||
continue;
|
||||
}
|
||||
|
||||
let addrs = hosts.iter().map(|host| (host.clone(), addr.port()).into()).collect();
|
||||
let connecting_tcp = ConnectingTcp::new(None, dns::IpAddrs::new(addrs), Some(fallback_timeout));
|
||||
let fut = ConnectingTcpFuture(connecting_tcp);
|
||||
|
||||
let start = Instant::now();
|
||||
let res = rt.block_on(fut).unwrap();
|
||||
let duration = start.elapsed();
|
||||
|
||||
// Allow actual duration to be +/- 150ms off.
|
||||
let min_duration = if timeout >= Duration::from_millis(150) {
|
||||
timeout - Duration::from_millis(150)
|
||||
} else {
|
||||
Duration::default()
|
||||
};
|
||||
let max_duration = timeout + Duration::from_millis(150);
|
||||
|
||||
assert_eq!(res, family);
|
||||
assert!(duration >= min_duration);
|
||||
assert!(duration <= max_duration);
|
||||
}
|
||||
|
||||
struct ConnectingTcpFuture(ConnectingTcp);
|
||||
|
||||
impl Future for ConnectingTcpFuture {
|
||||
type Item = u8;
|
||||
type Error = ::std::io::Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
match self.0.poll(&Some(Handle::default())) {
|
||||
Ok(Async::Ready(stream)) => Ok(Async::Ready(
|
||||
if stream.peer_addr().unwrap().is_ipv4() { 4 } else { 6 }
|
||||
)),
|
||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn local_ipv4_addr() -> IpAddr {
|
||||
Ipv4Addr::new(127, 0, 0, 1).into()
|
||||
}
|
||||
|
||||
fn local_ipv6_addr() -> IpAddr {
|
||||
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).into()
|
||||
}
|
||||
|
||||
fn unreachable_ipv4_addr() -> IpAddr {
|
||||
Ipv4Addr::new(127, 0, 0, 2).into()
|
||||
}
|
||||
|
||||
fn unreachable_ipv6_addr() -> IpAddr {
|
||||
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 2).into()
|
||||
}
|
||||
|
||||
fn slow_ipv4_addr() -> IpAddr {
|
||||
// RFC 6890 reserved IPv4 address.
|
||||
Ipv4Addr::new(198, 18, 0, 25).into()
|
||||
}
|
||||
|
||||
fn slow_ipv6_addr() -> IpAddr {
|
||||
// RFC 6890 reserved IPv6 address.
|
||||
Ipv6Addr::new(2001, 2, 0, 0, 0, 0, 0, 254).into()
|
||||
}
|
||||
|
||||
fn measure_connect(addr: IpAddr) -> (bool, Duration) {
|
||||
let start = Instant::now();
|
||||
let result = ::std::net::TcpStream::connect_timeout(
|
||||
&(addr, 80).into(), Duration::from_secs(1));
|
||||
|
||||
let reachable = result.is_ok() || result.unwrap_err().kind() == io::ErrorKind::TimedOut;
|
||||
let duration = start.elapsed();
|
||||
(reachable, duration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ pub struct IpAddrs {
|
||||
}
|
||||
|
||||
impl IpAddrs {
|
||||
pub fn new(addrs: Vec<SocketAddr>) -> Self {
|
||||
IpAddrs { iter: addrs.into_iter() }
|
||||
}
|
||||
|
||||
pub fn try_parse(host: &str, port: u16) -> Option<IpAddrs> {
|
||||
if let Ok(addr) = host.parse::<Ipv4Addr>() {
|
||||
let addr = SocketAddrV4::new(addr, port);
|
||||
@@ -46,6 +50,23 @@ impl IpAddrs {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn split_by_preference(self) -> (IpAddrs, IpAddrs) {
|
||||
let preferring_v6 = self.iter
|
||||
.as_slice()
|
||||
.first()
|
||||
.map(SocketAddr::is_ipv6)
|
||||
.unwrap_or(false);
|
||||
|
||||
let (preferred, fallback) = self.iter
|
||||
.partition::<Vec<_>, _>(|addr| addr.is_ipv6() == preferring_v6);
|
||||
|
||||
(IpAddrs::new(preferred), IpAddrs::new(fallback))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.iter.as_slice().is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for IpAddrs {
|
||||
@@ -55,3 +76,25 @@ impl Iterator for IpAddrs {
|
||||
self.iter.next()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ip_addrs_split_by_preference() {
|
||||
let v4_addr = (Ipv4Addr::new(127, 0, 0, 1), 80).into();
|
||||
let v6_addr = (Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1), 80).into();
|
||||
|
||||
let (mut preferred, mut fallback) =
|
||||
IpAddrs { iter: vec![v4_addr, v6_addr].into_iter() }.split_by_preference();
|
||||
assert!(preferred.next().unwrap().is_ipv4());
|
||||
assert!(fallback.next().unwrap().is_ipv6());
|
||||
|
||||
let (mut preferred, mut fallback) =
|
||||
IpAddrs { iter: vec![v6_addr, v4_addr].into_iter() }.split_by_preference();
|
||||
assert!(preferred.next().unwrap().is_ipv6());
|
||||
assert!(fallback.next().unwrap().is_ipv4());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user