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
						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