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
|
language: rust
|
||||||
sudo: false
|
sudo: true # Required for functional IPv6 (forces VM instead of Docker).
|
||||||
dist: trusty
|
dist: trusty
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
@@ -18,6 +18,13 @@ matrix:
|
|||||||
cache:
|
cache:
|
||||||
apt: true
|
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:
|
script:
|
||||||
- ./.travis/readme.py
|
- ./.travis/readme.py
|
||||||
- cargo build $FEATURES
|
- cargo build $FEATURES
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ mod http {
|
|||||||
use std::mem;
|
use std::mem;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use futures::{Async, Poll};
|
use futures::{Async, Poll};
|
||||||
use futures::future::{Executor, ExecuteError};
|
use futures::future::{Executor, ExecuteError};
|
||||||
@@ -396,6 +396,7 @@ mod http {
|
|||||||
use net2::TcpBuilder;
|
use net2::TcpBuilder;
|
||||||
use tokio_reactor::Handle;
|
use tokio_reactor::Handle;
|
||||||
use tokio_tcp::{TcpStream, ConnectFuture};
|
use tokio_tcp::{TcpStream, ConnectFuture};
|
||||||
|
use tokio_timer::Delay;
|
||||||
|
|
||||||
use super::super::dns;
|
use super::super::dns;
|
||||||
|
|
||||||
@@ -444,6 +445,7 @@ mod http {
|
|||||||
keep_alive_timeout: Option<Duration>,
|
keep_alive_timeout: Option<Duration>,
|
||||||
nodelay: bool,
|
nodelay: bool,
|
||||||
local_address: Option<IpAddr>,
|
local_address: Option<IpAddr>,
|
||||||
|
happy_eyeballs_timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HttpConnector {
|
impl HttpConnector {
|
||||||
@@ -481,6 +483,7 @@ mod http {
|
|||||||
keep_alive_timeout: None,
|
keep_alive_timeout: None,
|
||||||
nodelay: false,
|
nodelay: false,
|
||||||
local_address: None,
|
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>) {
|
pub fn set_local_address(&mut self, addr: Option<IpAddr>) {
|
||||||
self.local_address = addr;
|
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 {
|
impl fmt::Debug for HttpConnector {
|
||||||
@@ -564,6 +584,7 @@ mod http {
|
|||||||
handle: self.handle.clone(),
|
handle: self.handle.clone(),
|
||||||
keep_alive_timeout: self.keep_alive_timeout,
|
keep_alive_timeout: self.keep_alive_timeout,
|
||||||
nodelay: self.nodelay,
|
nodelay: self.nodelay,
|
||||||
|
happy_eyeballs_timeout: self.happy_eyeballs_timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,6 +596,7 @@ mod http {
|
|||||||
handle: handle.clone(),
|
handle: handle.clone(),
|
||||||
keep_alive_timeout: None,
|
keep_alive_timeout: None,
|
||||||
nodelay: false,
|
nodelay: false,
|
||||||
|
happy_eyeballs_timeout: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +629,7 @@ mod http {
|
|||||||
handle: Option<Handle>,
|
handle: Option<Handle>,
|
||||||
keep_alive_timeout: Option<Duration>,
|
keep_alive_timeout: Option<Duration>,
|
||||||
nodelay: bool,
|
nodelay: bool,
|
||||||
|
happy_eyeballs_timeout: Option<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum State {
|
enum State {
|
||||||
@@ -628,11 +651,8 @@ mod http {
|
|||||||
// If the host is already an IP addr (v4 or v6),
|
// If the host is already an IP addr (v4 or v6),
|
||||||
// skip resolving the dns and start connecting right away.
|
// skip resolving the dns and start connecting right away.
|
||||||
if let Some(addrs) = dns::IpAddrs::try_parse(host, port) {
|
if let Some(addrs) = dns::IpAddrs::try_parse(host, port) {
|
||||||
state = State::Connecting(ConnectingTcp {
|
state = State::Connecting(ConnectingTcp::new(
|
||||||
addrs: addrs,
|
local_addr, addrs, self.happy_eyeballs_timeout));
|
||||||
local_addr: local_addr,
|
|
||||||
current: None
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
let host = mem::replace(host, String::new());
|
let host = mem::replace(host, String::new());
|
||||||
let work = dns::Work::new(host, port);
|
let work = dns::Work::new(host, port);
|
||||||
@@ -643,11 +663,8 @@ mod http {
|
|||||||
match try!(future.poll()) {
|
match try!(future.poll()) {
|
||||||
Async::NotReady => return Ok(Async::NotReady),
|
Async::NotReady => return Ok(Async::NotReady),
|
||||||
Async::Ready(addrs) => {
|
Async::Ready(addrs) => {
|
||||||
state = State::Connecting(ConnectingTcp {
|
state = State::Connecting(ConnectingTcp::new(
|
||||||
addrs: addrs,
|
local_addr, addrs, self.happy_eyeballs_timeout));
|
||||||
local_addr: local_addr,
|
|
||||||
current: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -676,14 +693,71 @@ mod http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct ConnectingTcp {
|
struct ConnectingTcp {
|
||||||
addrs: dns::IpAddrs,
|
|
||||||
local_addr: Option<IpAddr>,
|
local_addr: Option<IpAddr>,
|
||||||
current: Option<ConnectFuture>,
|
preferred: ConnectingTcpRemote,
|
||||||
|
fallback: Option<ConnectingTcpFallback>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnectingTcp {
|
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
|
// 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;
|
let mut err = None;
|
||||||
loop {
|
loop {
|
||||||
if let Some(ref mut current) = self.current {
|
if let Some(ref mut current) = self.current {
|
||||||
@@ -694,14 +768,14 @@ mod http {
|
|||||||
err = Some(e);
|
err = Some(e);
|
||||||
if let Some(addr) = self.addrs.next() {
|
if let Some(addr) = self.addrs.next() {
|
||||||
debug!("connecting to {}", addr);
|
debug!("connecting to {}", addr);
|
||||||
*current = connect(&addr, &self.local_addr, handle)?;
|
*current = connect(&addr, local_addr, handle)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let Some(addr) = self.addrs.next() {
|
} else if let Some(addr) = self.addrs.next() {
|
||||||
debug!("connecting to {}", addr);
|
debug!("connecting to {}", addr);
|
||||||
self.current = Some(connect(&addr, &self.local_addr, handle)?);
|
self.current = Some(connect(&addr, local_addr, handle)?);
|
||||||
continue;
|
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.
|
// Make this Future unnameable outside of this crate.
|
||||||
mod http_connector {
|
mod http_connector {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -783,6 +905,154 @@ mod http {
|
|||||||
|
|
||||||
assert_eq!(connector.connect(dst).wait().unwrap_err().kind(), io::ErrorKind::InvalidInput);
|
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 {
|
impl IpAddrs {
|
||||||
|
pub fn new(addrs: Vec<SocketAddr>) -> Self {
|
||||||
|
IpAddrs { iter: addrs.into_iter() }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn try_parse(host: &str, port: u16) -> Option<IpAddrs> {
|
pub fn try_parse(host: &str, port: u16) -> Option<IpAddrs> {
|
||||||
if let Ok(addr) = host.parse::<Ipv4Addr>() {
|
if let Ok(addr) = host.parse::<Ipv4Addr>() {
|
||||||
let addr = SocketAddrV4::new(addr, port);
|
let addr = SocketAddrV4::new(addr, port);
|
||||||
@@ -46,6 +50,23 @@ impl IpAddrs {
|
|||||||
}
|
}
|
||||||
None
|
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 {
|
impl Iterator for IpAddrs {
|
||||||
@@ -55,3 +76,25 @@ impl Iterator for IpAddrs {
|
|||||||
self.iter.next()
|
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