use std::future::Future;
use std::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::ClientConfig;
use tokio::net::TcpStream;
use tokio_rustls::TlsConnector;
use crate::config::LocalIdentity;
use crate::error::MethodError;
use crate::method::{MethodOutcome, TraversalKind};
use crate::mtls::{CapturedPeerId, PeerIdPinningVerifier};
use crate::peer::{PeerConnection, PeerTarget};
use crate::strategy::Dialer;
#[derive(Debug, Clone, Copy)]
pub struct HappyEyeballsConfig {
pub per_attempt_timeout: Duration,
pub stagger: Duration,
}
impl Default for HappyEyeballsConfig {
fn default() -> Self {
HappyEyeballsConfig {
per_attempt_timeout: Duration::from_secs(10),
stagger: Duration::from_millis(250),
}
}
}
pub async fn happy_eyeballs_connect<T, E, F, Fut>(
candidates: &[std::net::SocketAddr],
config: HappyEyeballsConfig,
connect_one: F,
) -> Result<T, String>
where
E: std::fmt::Display,
F: Fn(std::net::SocketAddr) -> Fut + Sync,
Fut: Future<Output = Result<T, E>> + Send,
T: Send,
{
if candidates.is_empty() {
return Err("no candidate addresses to dial".to_string());
}
let mut ordered: Vec<std::net::SocketAddr> = candidates.to_vec();
if !crate::peer::is_ipv6_first(&ordered) {
crate::peer::sort_ipv6_first(&mut ordered);
}
let total = ordered.len();
type Attempt<'f, U> = std::pin::Pin<
Box<dyn Future<Output = (usize, std::net::SocketAddr, Result<U, String>)> + Send + 'f>,
>;
let mut attempts: futures::stream::FuturesUnordered<Attempt<'_, T>> =
futures::stream::FuturesUnordered::new();
let mut live: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut errors: Vec<String> = Vec::with_capacity(total);
let mut next_prio = 0usize;
let mut best_success: Option<(usize, T)> = None;
macro_rules! launch {
() => {
if next_prio < total {
let prio = next_prio;
let addr = ordered[prio];
next_prio += 1;
live.insert(prio);
let fut = &connect_one;
attempts.push(Box::pin(async move {
let res =
match tokio::time::timeout(config.per_attempt_timeout, fut(addr)).await {
Ok(Ok(conn)) => Ok(conn),
Ok(Err(e)) => Err(e.to_string()),
Err(_) => Err("connect timed out".to_string()),
};
(prio, addr, res)
}));
}
};
}
launch!();
loop {
if let Some((p, _)) = &best_success {
let more_preferred_live = live.iter().next().map(|lo| *lo < *p).unwrap_or(false);
let more_preferred_unlaunched = next_prio <= *p; if !more_preferred_live && !more_preferred_unlaunched {
return Ok(best_success.take().unwrap().1);
}
}
if live.is_empty() && next_prio >= total {
break;
}
let stagger = tokio::time::sleep(config.stagger);
tokio::select! {
biased;
finished = futures::StreamExt::next(&mut attempts), if !live.is_empty() => {
match finished {
Some((prio, addr, Ok(conn))) => {
live.remove(&prio);
if prio == 0 {
return Ok(conn);
}
let keep = best_success.as_ref().map(|(bp, _)| prio < *bp).unwrap_or(true);
if keep {
best_success = Some((prio, conn));
}
let _ = addr;
launch!();
}
Some((prio, addr, Err(e))) => {
live.remove(&prio);
errors.push(format!("{addr}: {e}"));
launch!();
}
None => break,
}
}
_ = stagger, if !live.is_empty() && next_prio < total => {
launch!();
}
}
}
if let Some((_, conn)) = best_success {
return Ok(conn);
}
Err(format!("all candidates failed: [{}]", errors.join("; ")))
}
#[derive(Debug, Clone)]
pub struct MtlsDialer {
identity: LocalIdentity,
happy_eyeballs: HappyEyeballsConfig,
}
impl MtlsDialer {
pub fn new(identity: LocalIdentity) -> Self {
MtlsDialer {
identity,
happy_eyeballs: HappyEyeballsConfig::default(),
}
}
pub fn with_happy_eyeballs(mut self, config: HappyEyeballsConfig) -> Self {
self.happy_eyeballs = config;
self
}
fn client_config(
&self,
expected: crate::identity::PeerId,
captured: CapturedPeerId,
) -> Result<ClientConfig, String> {
let cert = CertificateDer::from(self.identity.cert_der.clone());
let key = PrivateKeyDer::try_from(self.identity.key_der.to_vec())
.map_err(|e| format!("invalid private key: {e}"))?;
let verifier = Arc::new(PeerIdPinningVerifier::new(Some(expected), captured));
ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(verifier)
.with_client_auth_cert(vec![cert], key)
.map_err(|e| format!("client cert config: {e}"))
}
}
#[async_trait]
impl Dialer for MtlsDialer {
async fn dial(
&self,
peer: &PeerTarget,
outcome: &MethodOutcome,
) -> Result<PeerConnection, MethodError> {
let kind = outcome.kind;
let (tcp, addr) = happy_eyeballs_connect(
&outcome.dial_addrs,
self.happy_eyeballs,
|addr| async move {
TcpStream::connect(addr)
.await
.map(|s| (s, addr))
.map_err(|e| format!("tcp connect {addr}: {e}"))
},
)
.await
.map_err(|e| MethodError::failed(kind, e))?;
let captured = CapturedPeerId::default();
let config = self
.client_config(peer.peer_id, captured.clone())
.map_err(|e| MethodError::failed(kind, e))?;
let connector = TlsConnector::from(Arc::new(config));
let server_name = ServerName::try_from("peer.dig.invalid")
.map_err(|e| MethodError::failed(kind, format!("server name: {e}")))?;
let tls = connector
.connect(server_name, tcp)
.await
.map_err(|e| classify_tls_error(kind, &e))?;
let verified = captured
.get()
.ok_or_else(|| MethodError::failed(kind, "peer presented no certificate"))?;
let session = crate::mux::PeerSession::client(tls);
Ok(PeerConnection {
peer_id: verified,
method: kind,
remote_addr: addr,
session,
})
}
}
fn classify_tls_error(kind: TraversalKind, e: &std::io::Error) -> MethodError {
let msg = e.to_string();
MethodError::failed(kind, format!("mtls handshake: {msg}"))
}