use std::{
collections::BTreeSet,
future::Future,
net::{IpAddr, SocketAddr},
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use anyhow::{anyhow, bail, Context as _, Result};
use iroh_base::RelayUrl;
#[cfg(not(wasm_browser))]
use iroh_relay::dns::DnsResolver;
use iroh_relay::{
defaults::{DEFAULT_RELAY_QUIC_PORT, DEFAULT_STUN_PORT},
http::RELAY_PROBE_PATH,
protos::stun,
RelayMap, RelayNode,
};
#[cfg(wasm_browser)]
use n0_future::future::Pending;
use n0_future::{
task::{self, AbortOnDropHandle, JoinSet},
time::{self, Duration, Instant},
StreamExt as _,
};
#[cfg(not(wasm_browser))]
use netwatch::{interfaces, UdpSocket};
use rand::seq::IteratorRandom;
use tokio::sync::{mpsc, oneshot};
use tracing::{debug, debug_span, error, info_span, trace, warn, Instrument, Span};
use url::Host;
#[cfg(wasm_browser)]
use crate::net_report::portmapper; use crate::net_report::{self, Metrics, Report};
#[cfg(not(wasm_browser))]
use crate::net_report::{
defaults::timeouts::DNS_TIMEOUT,
dns::DNS_STAGGERING_MS,
ip_mapped_addrs::IpMappedAddresses,
ping::{PingError, Pinger},
};
#[cfg(not(wasm_browser))]
mod hairpin;
mod probes;
pub use probes::ProbeProto;
use probes::{Probe, ProbePlan};
use crate::net_report::defaults::timeouts::{
CAPTIVE_PORTAL_DELAY, CAPTIVE_PORTAL_TIMEOUT, OVERALL_REPORT_TIMEOUT, PROBES_TIMEOUT,
};
const ENOUGH_NODES: usize = 3;
#[derive(Debug)]
pub(super) struct Client {
_drop_guard: AbortOnDropHandle<()>,
}
#[cfg(not(wasm_browser))]
#[derive(Debug, Clone)]
pub(crate) struct SocketState {
pub(crate) port_mapper: Option<portmapper::Client>,
pub(crate) stun_sock4: Option<Arc<UdpSocket>>,
pub(crate) stun_sock6: Option<Arc<UdpSocket>>,
pub(crate) quic_config: Option<QuicConfig>,
pub(crate) dns_resolver: DnsResolver,
pub(crate) ip_mapped_addrs: Option<IpMappedAddresses>,
}
impl Client {
pub(super) fn new(
net_report: net_report::Addr,
last_report: Option<Arc<Report>>,
relay_map: RelayMap,
protocols: BTreeSet<ProbeProto>,
metrics: Arc<Metrics>,
#[cfg(not(wasm_browser))] socket_state: SocketState,
) -> Self {
let (msg_tx, msg_rx) = mpsc::channel(32);
let addr = Addr {
sender: msg_tx.clone(),
};
let mut actor = Actor {
msg_tx,
msg_rx,
net_report: net_report.clone(),
last_report,
relay_map,
report: Report::default(),
outstanding_tasks: OutstandingTasks::default(),
protocols,
#[cfg(not(wasm_browser))]
socket_state,
#[cfg(not(wasm_browser))]
hairpin_actor: hairpin::Client::new(net_report, addr),
metrics,
};
let task =
task::spawn(async move { actor.run().await }.instrument(info_span!("reportgen.actor")));
Self {
_drop_guard: AbortOnDropHandle::new(task),
}
}
}
#[derive(Debug, Clone)]
pub(super) struct Addr {
sender: mpsc::Sender<Message>,
}
impl Addr {
async fn send(&self, msg: Message) -> Result<(), mpsc::error::SendError<Message>> {
trace!(
"sending {:?} to channel with cap {}",
msg,
self.sender.capacity()
);
self.sender.send(msg).await
}
}
#[derive(Debug)]
enum Message {
HairpinResult(bool),
ProbeWouldHelp(Probe, Arc<RelayNode>, oneshot::Sender<bool>),
AbortProbes,
}
#[derive(Debug)]
struct Actor {
msg_tx: mpsc::Sender<Message>,
msg_rx: mpsc::Receiver<Message>,
net_report: super::Addr,
last_report: Option<Arc<Report>>,
relay_map: RelayMap,
report: Report,
outstanding_tasks: OutstandingTasks,
protocols: BTreeSet<ProbeProto>,
#[cfg(not(wasm_browser))]
socket_state: SocketState,
#[cfg(not(wasm_browser))]
hairpin_actor: hairpin::Client,
metrics: Arc<Metrics>,
}
impl Actor {
fn addr(&self) -> Addr {
Addr {
sender: self.msg_tx.clone(),
}
}
async fn run(&mut self) {
match self.run_inner().await {
Ok(_) => debug!("reportgen actor finished"),
Err(err) => {
self.net_report
.send(net_report::Message::ReportAborted { err })
.await
.ok();
}
}
}
async fn run_inner(&mut self) -> Result<()> {
#[cfg(not(wasm_browser))]
let port_mapper = self.socket_state.port_mapper.is_some();
#[cfg(wasm_browser)]
let port_mapper = false;
debug!(%port_mapper, "reportstate actor starting");
self.report.os_has_ipv6 = super::os_has_ipv6();
let mut port_mapping = self.prepare_portmapper_task();
let mut captive_task = self.prepare_captive_portal_task();
let mut probes = self.spawn_probes_task().await?;
let total_timer = time::sleep(OVERALL_REPORT_TIMEOUT);
tokio::pin!(total_timer);
let probe_timer = time::sleep(PROBES_TIMEOUT);
tokio::pin!(probe_timer);
loop {
trace!(awaiting = ?self.outstanding_tasks, "tick; awaiting tasks");
if self.outstanding_tasks.all_done() {
debug!("all tasks done");
break;
}
tokio::select! {
biased;
_ = &mut total_timer => {
trace!("tick: total_timer expired");
bail!("report timed out");
}
_ = &mut probe_timer => {
warn!("tick: probes timed out");
probe_timer.as_mut().reset(Instant::now() + PROBES_TIMEOUT);
probes.abort_all();
self.handle_abort_probes();
}
pm = &mut port_mapping, if self.outstanding_tasks.port_mapper => {
debug!(report=?pm, "tick: portmapper probe report");
self.report.portmap_probe = pm;
port_mapping.inner = None;
self.outstanding_tasks.port_mapper = false;
}
set_result = probes.join_next(), if self.outstanding_tasks.probes => {
trace!("tick: probes done: {:?}", set_result);
match set_result {
Some(Ok(Ok(report))) => self.handle_probe_report(report),
Some(Ok(Err(_))) => (),
Some(Err(e)) => {
warn!("probes task error: {:?}", e);
}
None => {
self.handle_abort_probes();
}
}
trace!("tick: probes handled");
}
found = &mut captive_task, if self.outstanding_tasks.captive_task => {
trace!("tick: captive portal task done");
self.report.captive_portal = found;
captive_task.inner = None;
self.outstanding_tasks.captive_task = false;
}
msg = self.msg_rx.recv() => {
trace!("tick: msg recv: {:?}", msg);
match msg {
Some(msg) => self.handle_message(msg),
None => bail!("msg_rx closed, reportgen client must be dropped"),
}
}
}
}
if !probes.is_empty() {
debug!(
"aborting {} probe sets, already have enough reports",
probes.len()
);
drop(probes);
}
debug!("Sending report to net_report actor");
self.net_report
.send(net_report::Message::ReportReady {
report: Box::new(self.report.clone()),
})
.await?;
Ok(())
}
fn handle_message(&mut self, msg: Message) {
trace!(?msg, "handling message");
match msg {
Message::HairpinResult(works) => {
self.report.hair_pinning = Some(works);
self.outstanding_tasks.hairpin = false;
}
Message::ProbeWouldHelp(probe, relay_node, response_tx) => {
let res = self.probe_would_help(probe, relay_node);
if response_tx.send(res).is_err() {
debug!("probe dropped before ProbeWouldHelp response sent");
}
}
Message::AbortProbes => {
self.handle_abort_probes();
}
}
}
fn handle_probe_report(&mut self, probe_report: ProbeReport) {
debug!(?probe_report, "finished probe");
update_report(&mut self.report, probe_report);
#[cfg(not(wasm_browser))]
if let Some(ref addr) = self.report.global_v4 {
if !self.hairpin_actor.has_started() {
self.hairpin_actor.start_check(*addr);
self.outstanding_tasks.hairpin = true;
}
}
let enough_relays = std::cmp::min(self.relay_map.len(), ENOUGH_NODES);
if self.report.relay_latency.len() == enough_relays {
let timeout = self.report.relay_latency.max_latency();
let timeout = match self.last_report.is_some() {
true => timeout,
false => timeout * 2,
};
let reportcheck = self.addr();
debug!(
reports=self.report.relay_latency.len(),
delay=?timeout,
"Have enough probe reports, aborting further probes soon",
);
task::spawn(
async move {
time::sleep(timeout).await;
reportcheck
.send(Message::AbortProbes)
.await
.map_err(|err| trace!("Failed to abort all probes: {err:#}"))
.ok();
}
.instrument(Span::current()),
);
}
}
fn probe_would_help(&mut self, probe: Probe, relay_node: Arc<RelayNode>) -> bool {
if self.report.relay_latency.get(&relay_node.url).is_none() {
return true;
}
#[cfg(not(wasm_browser))]
if probe.proto() == ProbeProto::StunIpv6 && self.report.relay_v6_latency.is_empty() {
return true;
}
#[cfg(not(wasm_browser))]
if probe.proto() == ProbeProto::StunIpv4 && self.report.mapping_varies_by_dest_ip.is_none()
{
return true;
}
false
}
fn handle_abort_probes(&mut self) {
trace!("handle abort probes");
self.outstanding_tasks.probes = false;
if self.report.udp {
self.outstanding_tasks.port_mapper = false;
self.outstanding_tasks.captive_task = false;
}
}
fn prepare_portmapper_task(
&mut self,
) -> MaybeFuture<Pin<Box<impl Future<Output = Option<portmapper::ProbeOutput>>>>> {
#[cfg(wasm_browser)]
let port_mapping: MaybeFuture<Pin<Box<Pending<Option<portmapper::ProbeOutput>>>>> =
MaybeFuture::default();
#[cfg(not(wasm_browser))]
let mut port_mapping = MaybeFuture::default();
#[cfg(not(wasm_browser))]
if let Some(port_mapper) = self.socket_state.port_mapper.clone() {
port_mapping.inner = Some(Box::pin(async move {
match port_mapper.probe().await {
Ok(Ok(res)) => Some(res),
Ok(Err(err)) => {
debug!("skipping port mapping: {err:?}");
None
}
Err(recv_err) => {
warn!("skipping port mapping: {recv_err:?}");
None
}
}
}));
self.outstanding_tasks.port_mapper = true;
}
port_mapping
}
fn prepare_captive_portal_task(
&mut self,
) -> MaybeFuture<Pin<Box<impl Future<Output = Option<bool>>>>> {
#[cfg(wasm_browser)]
let captive_task: MaybeFuture<Pin<Box<Pending<Option<bool>>>>> = MaybeFuture::default();
#[cfg(not(wasm_browser))]
let mut captive_task = MaybeFuture::default();
#[cfg(not(wasm_browser))]
if self.last_report.is_none() {
let preferred_relay = self
.last_report
.as_ref()
.and_then(|l| l.preferred_relay.clone());
let dns_resolver = self.socket_state.dns_resolver.clone();
let dm = self.relay_map.clone();
self.outstanding_tasks.captive_task = true;
captive_task.inner = Some(Box::pin(async move {
time::sleep(CAPTIVE_PORTAL_DELAY).await;
debug!("Captive portal check started after {CAPTIVE_PORTAL_DELAY:?}");
let captive_portal_check = time::timeout(
CAPTIVE_PORTAL_TIMEOUT,
check_captive_portal(&dns_resolver, &dm, preferred_relay)
.instrument(debug_span!("captive-portal")),
);
match captive_portal_check.await {
Ok(Ok(found)) => Some(found),
Ok(Err(err)) => {
let err: Result<reqwest::Error, _> = err.downcast();
match err {
Ok(req_err) if req_err.is_connect() => {
debug!("check_captive_portal failed: {req_err:#}");
}
Ok(req_err) => warn!("check_captive_portal error: {req_err:#}"),
Err(any_err) => warn!("check_captive_portal error: {any_err:#}"),
}
None
}
Err(_) => {
warn!("check_captive_portal timed out");
None
}
}
}));
}
self.outstanding_tasks.captive_task = false;
captive_task
}
async fn spawn_probes_task(&mut self) -> Result<JoinSet<Result<ProbeReport>>> {
#[cfg(not(wasm_browser))]
let if_state = interfaces::State::new().await;
#[cfg(not(wasm_browser))]
debug!(%if_state, "Local interfaces");
let plan = match self.last_report {
Some(ref report) => ProbePlan::with_last_report(
&self.relay_map,
report,
&self.protocols,
#[cfg(not(wasm_browser))]
&if_state,
),
None => ProbePlan::initial(
&self.relay_map,
&self.protocols,
#[cfg(not(wasm_browser))]
&if_state,
),
};
trace!(%plan, "probe plan");
#[cfg(not(wasm_browser))]
let pinger = Pinger::new();
let mut probes = JoinSet::default();
for probe_set in plan.iter() {
let mut set = JoinSet::default();
for probe in probe_set {
let reportstate = self.addr();
let relay_node = probe.node().clone();
let probe = probe.clone();
let net_report = self.net_report.clone();
#[cfg(not(wasm_browser))]
let pinger = pinger.clone();
#[cfg(not(wasm_browser))]
let socket_state = self.socket_state.clone();
let metrics = self.metrics.clone();
set.spawn(
run_probe(
reportstate,
relay_node,
probe.clone(),
net_report,
metrics,
#[cfg(not(wasm_browser))]
pinger,
#[cfg(not(wasm_browser))]
socket_state,
)
.instrument(debug_span!("run_probe", %probe)),
);
}
probes.spawn(
async move {
let mut probe_proto = None;
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(report)) => return Ok(report),
Ok(Err(ProbeError::Error(err, probe))) => {
probe_proto = Some(probe.proto());
warn!(?probe, "probe failed: {:#}", err);
continue;
}
Ok(Err(ProbeError::AbortSet(err, probe))) => {
debug!(?probe, "probe set aborted: {:#}", err);
set.abort_all();
return Err(err);
}
Err(err) => {
warn!("fatal probe set error, aborting: {:#}", err);
continue;
}
}
}
warn!(?probe_proto, "no successful probes in ProbeSet");
Err(anyhow!("All probes in ProbeSet failed"))
}
.instrument(info_span!("probe")),
);
}
self.outstanding_tasks.probes = true;
Ok(probes)
}
}
#[derive(Debug, Default)]
struct OutstandingTasks {
probes: bool,
port_mapper: bool,
captive_task: bool,
hairpin: bool,
}
impl OutstandingTasks {
fn all_done(&self) -> bool {
!(self.probes || self.port_mapper || self.captive_task || self.hairpin)
}
}
#[derive(Debug, Clone)]
struct ProbeReport {
ipv4_can_send: bool,
ipv6_can_send: bool,
icmpv4: Option<bool>,
icmpv6: Option<bool>,
latency: Option<Duration>,
probe: Probe,
addr: Option<SocketAddr>,
}
impl ProbeReport {
fn new(probe: Probe) -> Self {
ProbeReport {
probe,
ipv4_can_send: false,
ipv6_can_send: false,
icmpv4: None,
icmpv6: None,
latency: None,
addr: None,
}
}
}
#[derive(Debug)]
enum ProbeError {
AbortSet(anyhow::Error, Probe),
Error(anyhow::Error, Probe),
}
#[derive(Debug, Clone)]
pub struct QuicConfig {
pub ep: quinn::Endpoint,
pub client_config: rustls::ClientConfig,
pub ipv4: bool,
pub ipv6: bool,
}
async fn run_probe(
reportstate: Addr,
relay_node: Arc<RelayNode>,
probe: Probe,
net_report: net_report::Addr,
metrics: Arc<Metrics>,
#[cfg(not(wasm_browser))] pinger: Pinger,
#[cfg(not(wasm_browser))] socket_state: SocketState,
) -> Result<ProbeReport, ProbeError> {
if !probe.delay().is_zero() {
trace!("delaying probe");
time::sleep(probe.delay()).await;
}
debug!("starting probe");
let (would_help_tx, would_help_rx) = oneshot::channel();
if let Err(err) = reportstate
.send(Message::ProbeWouldHelp(
probe.clone(),
relay_node.clone(),
would_help_tx,
))
.await
{
debug!("Failed to check if probe would help: {err:#}");
return Err(ProbeError::AbortSet(err.into(), probe.clone()));
}
if !would_help_rx.await.map_err(|_| {
ProbeError::AbortSet(
anyhow!("ReportCheck actor dropped sender while waiting for ProbeWouldHelp response"),
probe.clone(),
)
})? {
return Err(ProbeError::AbortSet(
anyhow!("ReportCheck says probe set no longer useful"),
probe,
));
}
#[cfg(not(wasm_browser))]
let relay_addr = get_relay_addr(&socket_state.dns_resolver, &relay_node, probe.proto())
.await
.context("no relay node addr")
.map_err(|e| ProbeError::AbortSet(e, probe.clone()))?;
let mut result = ProbeReport::new(probe.clone());
match probe {
#[cfg(not(wasm_browser))]
Probe::StunIpv4 { .. } | Probe::StunIpv6 { .. } => {
let maybe_sock = if matches!(probe, Probe::StunIpv4 { .. }) {
socket_state.stun_sock4.as_ref()
} else {
socket_state.stun_sock6.as_ref()
};
match maybe_sock {
Some(sock) => {
result = run_stun_probe(sock, relay_addr, net_report, probe, &metrics).await?;
}
None => {
return Err(ProbeError::AbortSet(
anyhow!("No socket for {}, aborting probeset", probe.proto()),
probe.clone(),
));
}
}
}
#[cfg(not(wasm_browser))]
Probe::IcmpV4 { .. } | Probe::IcmpV6 { .. } => {
result = run_icmp_probe(probe, relay_addr, pinger).await?
}
Probe::Https { ref node, .. } => {
debug!("sending probe HTTPS");
match measure_https_latency(
#[cfg(not(wasm_browser))]
&socket_state.dns_resolver,
node,
None,
)
.await
{
Ok((latency, ip)) => {
debug!(?latency, "latency");
result.latency = Some(latency);
match ip {
IpAddr::V4(_) => result.ipv4_can_send = true,
IpAddr::V6(_) => result.ipv6_can_send = true,
}
}
Err(err) => {
warn!("https latency measurement failed: {:?}", err);
}
}
}
#[cfg(not(wasm_browser))]
Probe::QuicIpv4 { ref node, .. } | Probe::QuicIpv6 { ref node, .. } => {
debug!("sending QUIC address discovery probe");
let url = node.url.clone();
match socket_state.quic_config {
Some(quic_config) => {
result = run_quic_probe(
quic_config,
url,
relay_addr,
probe,
socket_state.ip_mapped_addrs,
)
.await?;
}
None => {
return Err(ProbeError::AbortSet(
anyhow!("No QUIC endpoint for {}", probe.proto()),
probe.clone(),
));
}
}
}
}
trace!("probe successful");
Ok(result)
}
#[cfg(not(wasm_browser))]
async fn run_stun_probe(
sock: &Arc<UdpSocket>,
relay_addr: SocketAddr,
net_report: net_report::Addr,
probe: Probe,
metrics: &Metrics,
) -> Result<ProbeReport, ProbeError> {
match probe.proto() {
ProbeProto::StunIpv4 => debug_assert!(relay_addr.is_ipv4()),
ProbeProto::StunIpv6 => debug_assert!(relay_addr.is_ipv6()),
_ => debug_assert!(false, "wrong probe"),
}
let txid = stun::TransactionId::default();
let req = stun::request(txid);
let (stun_tx, stun_rx) = oneshot::channel();
let (inflight_ready_tx, inflight_ready_rx) = oneshot::channel();
net_report
.send(net_report::Message::InFlightStun(
net_report::Inflight {
txn: txid,
start: Instant::now(),
s: stun_tx,
},
inflight_ready_tx,
))
.await
.map_err(|e| ProbeError::Error(e.into(), probe.clone()))?;
inflight_ready_rx
.await
.map_err(|e| ProbeError::Error(e.into(), probe.clone()))?;
match sock.send_to(&req, relay_addr).await {
Ok(n) if n == req.len() => {
debug!(%relay_addr, %txid, "sending {} probe", probe.proto());
let mut result = ProbeReport::new(probe.clone());
if matches!(probe, Probe::StunIpv4 { .. }) {
result.ipv4_can_send = true;
metrics.stun_packets_sent_ipv4.inc();
} else {
result.ipv6_can_send = true;
metrics.stun_packets_sent_ipv6.inc();
}
let (delay, addr) = stun_rx
.await
.map_err(|e| ProbeError::Error(e.into(), probe.clone()))?;
result.latency = Some(delay);
result.addr = Some(addr);
Ok(result)
}
Ok(n) => {
let err = anyhow!("Failed to send full STUN request: {}", probe.proto());
error!(%relay_addr, sent_len=n, req_len=req.len(), "{err:#}");
Err(ProbeError::Error(err, probe.clone()))
}
Err(err) => {
let kind = err.kind();
let err = anyhow::Error::new(err)
.context(format!("Failed to send STUN request: {}", probe.proto()));
match format!("{kind:?}").as_str() {
"NetworkUnreachable" | "HostUnreachable" => {
debug!(%relay_addr, "{err:#}");
Err(ProbeError::AbortSet(err, probe.clone()))
}
_ => {
Err(ProbeError::Error(err, probe.clone()))
}
}
}
}
}
#[cfg(not(wasm_browser))]
fn maybe_to_mapped_addr(
ip_mapped_addrs: Option<IpMappedAddresses>,
addr: SocketAddr,
) -> SocketAddr {
if let Some(ip_mapped_addrs) = ip_mapped_addrs.as_ref() {
return ip_mapped_addrs.get_or_register(addr).private_socket_addr();
}
addr
}
#[cfg(not(wasm_browser))]
async fn run_quic_probe(
quic_config: QuicConfig,
url: RelayUrl,
relay_addr: SocketAddr,
probe: Probe,
ip_mapped_addrs: Option<IpMappedAddresses>,
) -> Result<ProbeReport, ProbeError> {
match probe.proto() {
ProbeProto::QuicIpv4 => debug_assert!(relay_addr.is_ipv4()),
ProbeProto::QuicIpv6 => debug_assert!(relay_addr.is_ipv6()),
_ => debug_assert!(false, "wrong probe"),
}
let relay_addr = maybe_to_mapped_addr(ip_mapped_addrs, relay_addr);
let host = match url.host_str() {
Some(host) => host,
None => {
return Err(ProbeError::Error(
anyhow!("URL must have 'host' to use QUIC address discovery probes"),
probe.clone(),
));
}
};
let quic_client = iroh_relay::quic::QuicClient::new(quic_config.ep, quic_config.client_config)
.map_err(|e| ProbeError::Error(e, probe.clone()))?;
let (addr, latency) = quic_client
.get_addr_and_latency(relay_addr, host)
.await
.map_err(|e| ProbeError::Error(e, probe.clone()))?;
let mut result = ProbeReport::new(probe.clone());
if matches!(probe, Probe::QuicIpv4 { .. }) {
result.ipv4_can_send = true;
} else {
result.ipv6_can_send = true;
}
result.addr = Some(addr);
result.latency = Some(latency);
Ok(result)
}
#[cfg(not(wasm_browser))]
async fn check_captive_portal(
dns_resolver: &DnsResolver,
dm: &RelayMap,
preferred_relay: Option<RelayUrl>,
) -> Result<bool> {
let preferred_relay = preferred_relay.and_then(|url| match dm.get_node(&url) {
Some(node) if node.stun_only => Some(url),
_ => None,
});
let url = match preferred_relay {
Some(url) => url,
None => {
let urls: Vec<_> = dm
.nodes()
.filter(|n| !n.stun_only)
.map(|n| n.url.clone())
.collect();
if urls.is_empty() {
debug!("No suitable relay node for captive portal check");
return Ok(false);
}
let i = (0..urls.len())
.choose(&mut rand::thread_rng())
.unwrap_or_default();
urls[i].clone()
}
};
let mut builder = reqwest::ClientBuilder::new().redirect(reqwest::redirect::Policy::none());
if let Some(Host::Domain(domain)) = url.host() {
let addrs: Vec<_> = dns_resolver
.lookup_ipv4_ipv6_staggered(domain, DNS_TIMEOUT, DNS_STAGGERING_MS)
.await?
.map(|ipaddr| SocketAddr::new(ipaddr, 0))
.collect();
builder = builder.resolve_to_addrs(domain, &addrs);
}
let client = builder.build()?;
let host_name = url.host_str().unwrap_or_default();
let challenge = format!("ts_{}", host_name);
let portal_url = format!("http://{}/generate_204", host_name);
let res = client
.request(reqwest::Method::GET, portal_url)
.header("X-Tailscale-Challenge", &challenge)
.send()
.await?;
let expected_response = format!("response {challenge}");
let is_valid_response = res
.headers()
.get("X-Tailscale-Response")
.map(|s| s.to_str().unwrap_or_default())
== Some(&expected_response);
debug!(
"check_captive_portal url={} status_code={} valid_response={}",
res.url(),
res.status(),
is_valid_response,
);
let has_captive = res.status() != 204 || !is_valid_response;
Ok(has_captive)
}
fn get_port(relay_node: &RelayNode, proto: &ProbeProto) -> Result<u16> {
match proto {
#[cfg(not(wasm_browser))]
ProbeProto::QuicIpv4 | ProbeProto::QuicIpv6 => {
if let Some(ref quic) = relay_node.quic {
if quic.port == 0 {
Ok(DEFAULT_RELAY_QUIC_PORT)
} else {
Ok(quic.port)
}
} else {
bail!("Relay node not suitable for QUIC address discovery probes");
}
}
_ => {
if relay_node.stun_port == 0 {
Ok(DEFAULT_STUN_PORT)
} else {
Ok(relay_node.stun_port)
}
}
}
}
#[cfg(not(wasm_browser))]
async fn get_relay_addr(
dns_resolver: &DnsResolver,
relay_node: &RelayNode,
proto: ProbeProto,
) -> Result<SocketAddr> {
if relay_node.stun_only && !matches!(proto, ProbeProto::StunIpv4 | ProbeProto::StunIpv6) {
bail!("Relay node not suitable for non-STUN probes");
}
let port = get_port(relay_node, &proto)?;
match proto {
ProbeProto::StunIpv4 | ProbeProto::IcmpV4 | ProbeProto::QuicIpv4 => {
relay_lookup_ipv4_staggered(dns_resolver, relay_node, port).await
}
ProbeProto::StunIpv6 | ProbeProto::IcmpV6 | ProbeProto::QuicIpv6 => {
relay_lookup_ipv6_staggered(dns_resolver, relay_node, port).await
}
ProbeProto::Https => Err(anyhow!("Not implemented")),
}
}
#[cfg(not(wasm_browser))]
async fn relay_lookup_ipv4_staggered(
dns_resolver: &DnsResolver,
relay: &RelayNode,
port: u16,
) -> Result<SocketAddr> {
match relay.url.host() {
Some(url::Host::Domain(hostname)) => {
debug!(%hostname, "Performing DNS A lookup for relay addr");
match dns_resolver
.lookup_ipv4_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS)
.await
{
Ok(mut addrs) => addrs
.next()
.map(|ip| ip.to_canonical())
.map(|addr| SocketAddr::new(addr, port))
.ok_or(anyhow!("No suitable relay addr found")),
Err(err) => Err(err.context("No suitable relay addr found")),
}
}
Some(url::Host::Ipv4(addr)) => Ok(SocketAddr::new(addr.into(), port)),
Some(url::Host::Ipv6(_addr)) => Err(anyhow!("No suitable relay addr found")),
None => Err(anyhow!("No valid hostname in RelayUrl")),
}
}
#[cfg(not(wasm_browser))]
async fn relay_lookup_ipv6_staggered(
dns_resolver: &DnsResolver,
relay: &RelayNode,
port: u16,
) -> Result<SocketAddr> {
match relay.url.host() {
Some(url::Host::Domain(hostname)) => {
debug!(%hostname, "Performing DNS AAAA lookup for relay addr");
match dns_resolver
.lookup_ipv6_staggered(hostname, DNS_TIMEOUT, DNS_STAGGERING_MS)
.await
{
Ok(mut addrs) => addrs
.next()
.map(|ip| ip.to_canonical())
.map(|addr| SocketAddr::new(addr, port))
.ok_or(anyhow!("No suitable relay addr found")),
Err(err) => Err(err.context("No suitable relay addr found")),
}
}
Some(url::Host::Ipv4(_addr)) => Err(anyhow!("No suitable relay addr found")),
Some(url::Host::Ipv6(addr)) => Ok(SocketAddr::new(addr.into(), port)),
None => Err(anyhow!("No valid hostname in RelayUrl")),
}
}
#[cfg(not(wasm_browser))]
async fn run_icmp_probe(
probe: Probe,
relay_addr: SocketAddr,
pinger: Pinger,
) -> Result<ProbeReport, ProbeError> {
match probe.proto() {
ProbeProto::IcmpV4 => debug_assert!(relay_addr.is_ipv4()),
ProbeProto::IcmpV6 => debug_assert!(relay_addr.is_ipv6()),
_ => debug_assert!(false, "wrong probe"),
}
const DATA: &[u8; 15] = b"iroh icmp probe";
debug!(dst = %relay_addr, len = DATA.len(), "ICMP Ping started");
let latency = pinger
.send(relay_addr.ip(), DATA)
.await
.map_err(|err| match err {
PingError::Client(err) => ProbeError::AbortSet(
anyhow!("Failed to create pinger ({err:#}), aborting probeset"),
probe.clone(),
),
#[cfg(not(wasm_browser))]
PingError::Ping(err) => ProbeError::Error(err.into(), probe.clone()),
})?;
debug!(dst = %relay_addr, len = DATA.len(), ?latency, "ICMP ping done");
let mut report = ProbeReport::new(probe);
report.latency = Some(latency);
match relay_addr {
SocketAddr::V4(_) => {
report.ipv4_can_send = true;
report.icmpv4 = Some(true);
}
SocketAddr::V6(_) => {
report.ipv6_can_send = true;
report.icmpv6 = Some(true);
}
}
Ok(report)
}
#[allow(clippy::unused_async)]
async fn measure_https_latency(
#[cfg(not(wasm_browser))] dns_resolver: &DnsResolver,
node: &RelayNode,
certs: Option<Vec<rustls::pki_types::CertificateDer<'static>>>,
) -> Result<(Duration, IpAddr)> {
let url = node.url.join(RELAY_PROBE_PATH)?;
let mut builder = reqwest::ClientBuilder::new();
#[cfg(not(wasm_browser))]
{
builder = builder.redirect(reqwest::redirect::Policy::none());
}
#[cfg(not(wasm_browser))]
if let Some(Host::Domain(domain)) = url.host() {
let addrs: Vec<_> = dns_resolver
.lookup_ipv4_ipv6_staggered(domain, DNS_TIMEOUT, DNS_STAGGERING_MS)
.await?
.map(|ipaddr| SocketAddr::new(ipaddr, 0))
.collect();
builder = builder.resolve_to_addrs(domain, &addrs);
}
#[cfg(not(wasm_browser))]
if let Some(certs) = certs {
for cert in certs {
let cert = reqwest::Certificate::from_der(&cert)?;
builder = builder.add_root_certificate(cert);
}
}
let client = builder.build()?;
let start = Instant::now();
let response = client.request(reqwest::Method::GET, url).send().await?;
let latency = start.elapsed();
if response.status().is_success() {
#[cfg(not(wasm_browser))]
let remote_ip = response
.remote_addr()
.context("missing HttpInfo from HttpConnector")?
.ip();
#[cfg(wasm_browser)]
let remote_ip = IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED);
const MAX_BODY_SIZE: usize = 8 << 10; let mut body_size = 0;
let mut stream = response.bytes_stream();
while let Some(Ok(chunk)) = stream.next().await {
body_size += chunk.len();
if body_size >= MAX_BODY_SIZE {
break;
}
}
Ok((latency, remote_ip))
} else {
Err(anyhow!(
"Error response from server: '{}'",
response.status().canonical_reason().unwrap_or_default()
))
}
}
fn update_report(report: &mut Report, probe_report: ProbeReport) {
let relay_node = probe_report.probe.node();
if let Some(latency) = probe_report.latency {
report
.relay_latency
.update_relay(relay_node.url.clone(), latency);
#[cfg(not(wasm_browser))]
if matches!(
probe_report.probe.proto(),
ProbeProto::StunIpv4
| ProbeProto::StunIpv6
| ProbeProto::QuicIpv4
| ProbeProto::QuicIpv6
) {
report.udp = true;
match probe_report.addr {
Some(SocketAddr::V4(ipp)) => {
report.ipv4 = true;
report
.relay_v4_latency
.update_relay(relay_node.url.clone(), latency);
if report.global_v4.is_none() {
report.global_v4 = Some(ipp);
} else if report.global_v4 != Some(ipp) {
report.mapping_varies_by_dest_ip = Some(true);
} else if report.mapping_varies_by_dest_ip.is_none() {
report.mapping_varies_by_dest_ip = Some(false);
}
}
Some(SocketAddr::V6(ipp)) => {
report.ipv6 = true;
report
.relay_v6_latency
.update_relay(relay_node.url.clone(), latency);
if report.global_v6.is_none() {
report.global_v6 = Some(ipp);
} else if report.global_v6 != Some(ipp) {
report.mapping_varies_by_dest_ipv6 = Some(true);
warn!("IPv6 Address detected by STUN varies by destination");
} else if report.mapping_varies_by_dest_ipv6.is_none() {
report.mapping_varies_by_dest_ipv6 = Some(false);
}
}
None => {
debug_assert!(probe_report.addr.is_some());
}
}
}
}
report.ipv4_can_send |= probe_report.ipv4_can_send;
report.ipv6_can_send |= probe_report.ipv6_can_send;
report.icmpv4 = report
.icmpv4
.map(|val| val || probe_report.icmpv4.unwrap_or_default())
.or(probe_report.icmpv4);
report.icmpv6 = report
.icmpv6
.map(|val| val || probe_report.icmpv6.unwrap_or_default())
.or(probe_report.icmpv6);
}
#[derive(Debug)]
pub(crate) struct MaybeFuture<T> {
pub inner: Option<T>,
}
impl<T> Default for MaybeFuture<T> {
fn default() -> Self {
MaybeFuture { inner: None }
}
}
impl<T: Future + Unpin> Future for MaybeFuture<T> {
type Output = T::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.inner {
Some(ref mut t) => Pin::new(t).poll(cx),
None => Poll::Pending,
}
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use testresult::TestResult;
use tracing_test::traced_test;
use super::{super::test_utils, *};
use crate::net_report::dns;
#[tokio::test]
#[traced_test]
async fn test_update_report_stun_working() {
let (_server_a, relay_a) = test_utils::relay().await;
let (_server_b, relay_b) = test_utils::relay().await;
let mut report = Report::default();
let relay_a = Arc::new(relay_a);
let relay_b = Arc::new(relay_b);
let probe_report_a = ProbeReport {
ipv4_can_send: true,
ipv6_can_send: false,
icmpv4: None,
icmpv6: None,
latency: Some(Duration::from_millis(5)),
probe: Probe::StunIpv4 {
delay: Duration::ZERO,
node: relay_a.clone(),
},
addr: Some((Ipv4Addr::new(203, 0, 113, 1), 1234).into()),
};
update_report(&mut report, probe_report_a.clone());
assert!(report.udp);
assert_eq!(
report.relay_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(5)
);
assert_eq!(
report.relay_v4_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(5)
);
assert!(report.ipv4_can_send);
assert!(!report.ipv6_can_send);
let probe_report_b = ProbeReport {
latency: Some(Duration::from_millis(8)),
probe: Probe::StunIpv4 {
delay: Duration::ZERO,
node: relay_b.clone(),
},
..probe_report_a
};
update_report(&mut report, probe_report_b);
assert!(report.udp);
assert_eq!(
report.relay_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(5)
);
assert_eq!(
report.relay_v4_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(5)
);
assert!(report.ipv4_can_send);
assert!(!report.ipv6_can_send);
let probe_report_a_ipv6 = ProbeReport {
ipv4_can_send: false,
ipv6_can_send: true,
icmpv4: None,
icmpv6: None,
latency: Some(Duration::from_millis(4)),
probe: Probe::StunIpv6 {
delay: Duration::ZERO,
node: relay_a.clone(),
},
addr: Some((Ipv6Addr::new(2001, 0xdb8, 0, 0, 0, 0, 0, 1), 1234).into()),
};
update_report(&mut report, probe_report_a_ipv6);
assert!(report.udp);
assert_eq!(
report.relay_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(4)
);
assert_eq!(
report.relay_v6_latency.get(&relay_a.url).unwrap(),
Duration::from_millis(4)
);
assert!(report.ipv4_can_send);
assert!(report.ipv6_can_send);
}
#[tokio::test]
#[traced_test]
async fn test_update_report_icmp() {
let (_server_a, relay_a) = test_utils::relay().await;
let (_server_b, relay_b) = test_utils::relay().await;
let relay_a = Arc::new(relay_a);
let relay_b = Arc::new(relay_b);
let mut report = Report::default();
let probe_report_eu = ProbeReport {
ipv4_can_send: true,
ipv6_can_send: false,
icmpv4: Some(true),
icmpv6: None,
latency: Some(Duration::from_millis(5)),
probe: Probe::IcmpV4 {
delay: Duration::ZERO,
node: relay_a.clone(),
},
addr: Some((Ipv4Addr::new(203, 0, 113, 1), 1234).into()),
};
update_report(&mut report, probe_report_eu.clone());
assert!(!report.udp);
assert!(report.ipv4_can_send);
assert_eq!(report.icmpv4, Some(true));
let probe_report_na = ProbeReport {
ipv4_can_send: false,
ipv6_can_send: false,
icmpv4: Some(false),
icmpv6: None,
latency: None,
probe: Probe::IcmpV4 {
delay: Duration::ZERO,
node: relay_b.clone(),
},
addr: None,
};
update_report(&mut report, probe_report_na);
assert_eq!(report.icmpv4, Some(true));
let probe_report_eu_stun = ProbeReport {
ipv4_can_send: true,
ipv6_can_send: false,
icmpv4: None,
icmpv6: None,
latency: Some(Duration::from_millis(5)),
probe: Probe::StunIpv4 {
delay: Duration::ZERO,
node: relay_a.clone(),
},
addr: Some((Ipv4Addr::new(203, 0, 113, 1), 1234).into()),
};
update_report(&mut report, probe_report_eu_stun);
assert!(report.udp);
assert_eq!(report.icmpv4, Some(true));
}
#[tokio::test]
#[traced_test]
async fn test_icmpk_probe() {
let pinger = Pinger::new();
let (server, node) = test_utils::relay().await;
let addr = server.stun_addr().expect("test relay serves stun");
let probe = Probe::IcmpV4 {
delay: Duration::from_secs(0),
node: Arc::new(node),
};
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let mut tasks = JoinSet::new();
for i in 0..8 {
let probe = probe.clone();
let pinger = pinger.clone();
let tx = tx.clone();
tasks.spawn(async move {
time::sleep(Duration::from_millis(i * 100)).await;
let res = run_icmp_probe(probe, addr, pinger).await;
tx.send(res).ok();
});
}
let mut last_err = None;
while let Some(res) = rx.recv().await {
match res {
Ok(report) => {
dbg!(&report);
assert_eq!(report.icmpv4, Some(true));
assert!(
report.latency.expect("should have a latency") > Duration::from_secs(0)
);
break;
}
Err(ProbeError::Error(err, _probe)) => {
last_err = Some(err);
}
Err(ProbeError::AbortSet(_err, _probe)) => {
break;
}
}
}
if let Some(err) = last_err {
panic!("Ping error: {err:#}");
}
}
#[tokio::test]
async fn test_measure_https_latency() -> TestResult {
let (server, relay) = test_utils::relay().await;
let dns_resolver = dns::tests::resolver();
tracing::info!(relay_url = ?relay.url , "RELAY_URL");
let (latency, ip) =
measure_https_latency(&dns_resolver, &relay, server.certificates()).await?;
assert!(latency > Duration::ZERO);
let relay_url_ip = relay
.url
.host_str()
.context("host")?
.parse::<std::net::IpAddr>()?;
assert_eq!(ip, relay_url_ip);
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_quic_probe() -> TestResult {
let (server, relay) = test_utils::relay().await;
let relay = Arc::new(relay);
let client_config = iroh_relay::client::make_dangerous_client_config();
let ep = quinn::Endpoint::client(SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0))?;
let client_addr = ep.local_addr()?;
let quic_addr_disc = QuicConfig {
ep: ep.clone(),
client_config,
ipv4: true,
ipv6: true,
};
let url = relay.url.clone();
let port = server.quic_addr().unwrap().port();
let probe = Probe::QuicIpv4 {
delay: Duration::from_secs(0),
node: relay,
};
let probe = match run_quic_probe(
quic_addr_disc,
url,
(Ipv4Addr::LOCALHOST, port).into(),
probe,
None,
)
.await
{
Ok(probe) => probe,
Err(e) => match e {
ProbeError::AbortSet(err, _) | ProbeError::Error(err, _) => {
return Err(err.into());
}
},
};
assert!(probe.ipv4_can_send);
assert_eq!(probe.addr.unwrap(), client_addr);
ep.wait_idle().await;
server.shutdown().await?;
Ok(())
}
}