use std::{
collections::BTreeMap,
fmt,
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
time::Duration,
};
use iroh_base::RelayUrl;
use serde::{Deserialize, Serialize};
use tracing::{trace, warn};
use super::{ProbeReport, probes::Probe};
#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct Report {
pub udp_v4: bool,
pub udp_v6: bool,
pub mapping_varies_by_dest_ipv4: Option<bool>,
pub mapping_varies_by_dest_ipv6: Option<bool>,
pub preferred_relay: Option<RelayUrl>,
pub relay_latency: RelayLatencies,
pub global_v4: Option<SocketAddrV4>,
pub global_v6: Option<SocketAddrV6>,
pub captive_portal: Option<bool>,
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self, f)
}
}
impl Report {
pub fn has_udp(&self) -> bool {
self.udp_v4 || self.udp_v6
}
pub fn mapping_varies_by_dest(&self) -> Option<bool> {
match (
self.mapping_varies_by_dest_ipv4,
self.mapping_varies_by_dest_ipv6,
) {
(Some(v4), Some(v6)) => Some(v4 || v6),
(None, Some(v6)) => Some(v6),
(Some(v4), None) => Some(v4),
(None, None) => None,
}
}
pub(super) fn update(&mut self, report: &ProbeReport) {
match report {
ProbeReport::Https(report) => {
self.relay_latency
.update_relay(report.relay.clone(), report.latency, Probe::Https);
}
#[cfg(not(wasm_browser))]
ProbeReport::QadIpv4(report) => {
self.relay_latency.update_relay(
report.relay.clone(),
report.latency,
Probe::QadIpv4,
);
let SocketAddr::V4(ipp) = report.addr else {
warn!("received IPv6 address from IPv4 QAD: {}", report.addr);
return;
};
self.udp_v4 = true;
if let Some(global) = self.global_v4 {
if global == ipp {
if self.mapping_varies_by_dest_ipv4.is_none() {
self.mapping_varies_by_dest_ipv4 = Some(false);
}
} else {
self.mapping_varies_by_dest_ipv4 = Some(true);
warn!("IPv4 address detected by QAD varies by destination");
}
} else {
self.global_v4 = Some(ipp);
}
trace!(?self.global_v4, ?self.mapping_varies_by_dest_ipv4, %ipp, "stored report");
}
#[cfg(not(wasm_browser))]
ProbeReport::QadIpv6(report) => {
self.relay_latency.update_relay(
report.relay.clone(),
report.latency,
Probe::QadIpv6,
);
let SocketAddr::V6(ipp) = report.addr else {
warn!("received IPv4 address from IPv6 QAD: {}", report.addr);
return;
};
self.udp_v6 = true;
if let Some(global) = self.global_v6 {
if global == ipp {
if self.mapping_varies_by_dest_ipv6.is_none() {
self.mapping_varies_by_dest_ipv6 = Some(false);
}
} else {
self.mapping_varies_by_dest_ipv6 = Some(true);
warn!("IPv6 address detected by QAD varies by destination");
}
} else {
self.global_v6 = Some(ipp);
}
trace!(?self.global_v6, ?self.mapping_varies_by_dest_ipv6, %ipp, "stored report");
}
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct RelayLatencies {
#[cfg(not(wasm_browser))]
ipv4: BTreeMap<RelayUrl, Duration>,
#[cfg(not(wasm_browser))]
ipv6: BTreeMap<RelayUrl, Duration>,
https: BTreeMap<RelayUrl, Duration>,
}
impl RelayLatencies {
pub(super) fn update_relay(&mut self, url: RelayUrl, latency: Duration, probe: Probe) {
let list = match probe {
Probe::Https => &mut self.https,
#[cfg(not(wasm_browser))]
Probe::QadIpv4 => &mut self.ipv4,
#[cfg(not(wasm_browser))]
Probe::QadIpv6 => &mut self.ipv6,
};
let old_latency = list.entry(url).or_insert(latency);
if latency < *old_latency {
*old_latency = latency;
}
}
pub(super) fn merge(&mut self, other: &RelayLatencies) {
for (url, latency) in other.https.iter() {
self.update_relay(url.clone(), *latency, Probe::Https);
}
#[cfg(not(wasm_browser))]
for (url, latency) in other.ipv4.iter() {
self.update_relay(url.clone(), *latency, Probe::QadIpv4);
}
#[cfg(not(wasm_browser))]
for (url, latency) in other.ipv6.iter() {
self.update_relay(url.clone(), *latency, Probe::QadIpv6);
}
}
#[cfg(not(wasm_browser))]
pub fn iter(&self) -> impl Iterator<Item = (Probe, &'_ RelayUrl, Duration)> + '_ {
self.https
.iter()
.map(|(url, l)| (Probe::Https, url, *l))
.chain(self.ipv4.iter().map(|(url, l)| (Probe::QadIpv4, url, *l)))
.chain(self.ipv6.iter().map(|(url, l)| (Probe::QadIpv6, url, *l)))
}
#[cfg(wasm_browser)]
pub fn iter(&self) -> impl Iterator<Item = (Probe, &'_ RelayUrl, Duration)> + '_ {
self.https.iter().map(|(k, v)| (Probe::Https, k, *v))
}
#[cfg(not(wasm_browser))]
pub(super) fn is_empty(&self) -> bool {
self.https.is_empty() && self.ipv4.is_empty() && self.ipv6.is_empty()
}
#[cfg(wasm_browser)]
pub(super) fn is_empty(&self) -> bool {
self.https.is_empty()
}
pub(super) fn get(&self, url: &RelayUrl) -> Option<Duration> {
let mut list = Vec::with_capacity(3);
if let Some(val) = self.https.get(url) {
list.push(*val);
}
#[cfg(not(wasm_browser))]
if let Some(val) = self.ipv4.get(url) {
list.push(*val);
}
#[cfg(not(wasm_browser))]
if let Some(val) = self.ipv6.get(url) {
list.push(*val);
}
list.into_iter().min()
}
}