#![cfg_attr(iroh_docsrs, feature(doc_auto_cfg))]
#![deny(missing_docs, rustdoc::broken_intra_doc_links)]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![cfg_attr(wasm_browser, allow(unused))]
use std::{
collections::{BTreeMap, HashMap},
fmt::{self, Debug},
net::{SocketAddr, SocketAddrV4, SocketAddrV6},
sync::Arc,
};
use anyhow::{anyhow, Result};
use bytes::Bytes;
use iroh_base::RelayUrl;
#[cfg(not(wasm_browser))]
use iroh_relay::dns::DnsResolver;
use iroh_relay::{protos::stun, RelayMap};
use n0_future::{
task::{self, AbortOnDropHandle},
time::{Duration, Instant},
};
#[cfg(not(wasm_browser))]
use netwatch::UdpSocket;
use tokio::sync::{self, mpsc, oneshot};
use tracing::{debug, error, info_span, trace, warn, Instrument};
mod defaults;
#[cfg(not(wasm_browser))]
mod dns;
mod ip_mapped_addrs;
mod metrics;
#[cfg(not(wasm_browser))]
mod ping;
mod reportgen;
mod options;
#[cfg(not(wasm_browser))]
pub use stun_utils::bind_local_stun_socket;
#[cfg(wasm_browser)]
pub(crate) mod portmapper {
#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)]
#[display("portmap={{ UPnP: {upnp}, PMP: {nat_pmp}, PCP: {pcp} }}")]
pub struct ProbeOutput {
pub upnp: bool,
pub pcp: bool,
pub nat_pmp: bool,
}
}
pub(crate) use ip_mapped_addrs::{IpMappedAddr, IpMappedAddresses};
pub use metrics::Metrics;
pub use options::Options;
pub use reportgen::QuicConfig;
#[cfg(not(wasm_browser))]
use reportgen::SocketState;
const FULL_REPORT_INTERVAL: Duration = Duration::from_secs(5 * 60);
const DEFAULT_MAX_LATENCY: Duration = Duration::from_millis(100);
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Report {
pub udp: bool,
pub ipv6: bool,
pub ipv4: bool,
pub ipv6_can_send: bool,
pub ipv4_can_send: bool,
pub os_has_ipv6: bool,
pub icmpv4: Option<bool>,
pub icmpv6: Option<bool>,
pub mapping_varies_by_dest_ip: Option<bool>,
pub mapping_varies_by_dest_ipv6: Option<bool>,
pub hair_pinning: Option<bool>,
pub portmap_probe: Option<portmapper::ProbeOutput>,
pub preferred_relay: Option<RelayUrl>,
pub relay_latency: RelayLatencies,
pub relay_v4_latency: RelayLatencies,
pub relay_v6_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)
}
}
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct RelayLatencies(BTreeMap<RelayUrl, Duration>);
impl RelayLatencies {
fn new() -> Self {
Default::default()
}
fn update_relay(&mut self, url: RelayUrl, latency: Duration) {
let val = self.0.entry(url).or_insert(latency);
if latency < *val {
*val = latency;
}
}
fn merge(&mut self, other: &RelayLatencies) {
for (url, latency) in other.iter() {
self.update_relay(url.clone(), latency);
}
}
fn max_latency(&self) -> Duration {
self.0
.values()
.max()
.copied()
.unwrap_or(DEFAULT_MAX_LATENCY)
}
pub fn iter(&self) -> impl Iterator<Item = (&'_ RelayUrl, Duration)> + '_ {
self.0.iter().map(|(k, v)| (k, *v))
}
fn len(&self) -> usize {
self.0.len()
}
fn is_empty(&self) -> bool {
self.0.is_empty()
}
fn get(&self, url: &RelayUrl) -> Option<Duration> {
self.0.get(url).copied()
}
}
#[derive(Debug)]
pub struct Client {
addr: Addr,
_drop_guard: Arc<AbortOnDropHandle<()>>,
}
#[derive(Debug)]
struct Reports {
next_full: bool,
prev: HashMap<Instant, Arc<Report>>,
last: Option<Arc<Report>>,
last_full: Instant,
}
impl Default for Reports {
fn default() -> Self {
Self {
next_full: Default::default(),
prev: Default::default(),
last: Default::default(),
last_full: Instant::now(),
}
}
}
impl Client {
pub fn new(
#[cfg(not(wasm_browser))] port_mapper: Option<portmapper::Client>,
#[cfg(not(wasm_browser))] dns_resolver: DnsResolver,
#[cfg(not(wasm_browser))] ip_mapped_addrs: Option<IpMappedAddresses>,
metrics: Arc<Metrics>,
) -> Result<Self> {
let mut actor = Actor::new(
#[cfg(not(wasm_browser))]
port_mapper,
#[cfg(not(wasm_browser))]
dns_resolver,
#[cfg(not(wasm_browser))]
ip_mapped_addrs,
metrics,
)?;
let addr = actor.addr();
let task = task::spawn(
async move { actor.run().await }.instrument(info_span!("net_report.actor")),
);
let drop_guard = AbortOnDropHandle::new(task);
Ok(Client {
addr,
_drop_guard: Arc::new(drop_guard),
})
}
pub fn addr(&self) -> Addr {
self.addr.clone()
}
#[cfg(test)]
pub async fn get_report_all(
&mut self,
relay_map: RelayMap,
#[cfg(not(wasm_browser))] stun_sock_v4: Option<Arc<UdpSocket>>,
#[cfg(not(wasm_browser))] stun_sock_v6: Option<Arc<UdpSocket>>,
#[cfg(not(wasm_browser))] quic_config: Option<QuicConfig>,
) -> Result<Arc<Report>> {
#[cfg(not(wasm_browser))]
let opts = Options::default()
.stun_v4(stun_sock_v4)
.stun_v6(stun_sock_v6)
.quic_config(quic_config);
#[cfg(wasm_browser)]
let opts = Options::default();
let rx = self.get_report_channel(relay_map.clone(), opts).await?;
match rx.await {
Ok(res) => res,
Err(_) => Err(anyhow!("channel closed, actor awol")),
}
}
pub async fn get_report(&mut self, relay_map: RelayMap, opts: Options) -> Result<Arc<Report>> {
let rx = self.get_report_channel(relay_map, opts).await?;
match rx.await {
Ok(res) => res,
Err(_) => Err(anyhow!("channel closed, actor awol")),
}
}
pub(crate) async fn get_report_channel(
&mut self,
relay_map: RelayMap,
opts: Options,
) -> Result<oneshot::Receiver<Result<Arc<Report>>>> {
let (tx, rx) = oneshot::channel();
self.addr
.send(Message::RunCheck {
relay_map,
opts,
response_tx: tx,
})
.await?;
Ok(rx)
}
}
#[derive(Debug)]
pub(crate) struct Inflight {
txn: stun::TransactionId,
start: Instant,
s: sync::oneshot::Sender<(Duration, SocketAddr)>,
}
#[derive(Debug)]
pub(crate) enum Message {
RunCheck {
relay_map: RelayMap,
opts: Options,
response_tx: oneshot::Sender<Result<Arc<Report>>>,
},
ReportReady { report: Box<Report> },
ReportAborted { err: anyhow::Error },
StunPacket {
payload: Bytes,
from_addr: SocketAddr,
},
InFlightStun(Inflight, oneshot::Sender<()>),
}
#[derive(Debug, Clone)]
pub struct Addr {
sender: mpsc::Sender<Message>,
metrics: Arc<Metrics>,
}
impl Addr {
pub fn receive_stun_packet(&self, payload: Bytes, src: SocketAddr) {
if let Err(mpsc::error::TrySendError::Full(_)) = self.sender.try_send(Message::StunPacket {
payload,
from_addr: src,
}) {
self.metrics.stun_packets_dropped.inc();
warn!("dropping stun packet from {}", src);
}
}
async fn send(&self, msg: Message) -> Result<(), mpsc::error::SendError<Message>> {
self.sender.send(msg).await.inspect_err(|_| {
error!("net_report actor lost");
})
}
}
#[derive(Debug)]
struct Actor {
receiver: mpsc::Receiver<Message>,
sender: mpsc::Sender<Message>,
reports: Reports,
#[cfg(not(wasm_browser))]
port_mapper: Option<portmapper::Client>,
in_flight_stun_requests: HashMap<stun::TransactionId, Inflight>,
current_report_run: Option<ReportRun>,
#[cfg(not(wasm_browser))]
dns_resolver: DnsResolver,
#[cfg(not(wasm_browser))]
ip_mapped_addrs: Option<IpMappedAddresses>,
metrics: Arc<Metrics>,
}
impl Actor {
fn new(
#[cfg(not(wasm_browser))] port_mapper: Option<portmapper::Client>,
#[cfg(not(wasm_browser))] dns_resolver: DnsResolver,
#[cfg(not(wasm_browser))] ip_mapped_addrs: Option<IpMappedAddresses>,
metrics: Arc<Metrics>,
) -> Result<Self> {
let (sender, receiver) = mpsc::channel(32);
Ok(Self {
receiver,
sender,
reports: Default::default(),
#[cfg(not(wasm_browser))]
port_mapper,
in_flight_stun_requests: Default::default(),
current_report_run: None,
#[cfg(not(wasm_browser))]
dns_resolver,
#[cfg(not(wasm_browser))]
ip_mapped_addrs,
metrics,
})
}
fn addr(&self) -> Addr {
Addr {
sender: self.sender.clone(),
metrics: self.metrics.clone(),
}
}
async fn run(&mut self) {
debug!("net_report actor starting");
while let Some(msg) = self.receiver.recv().await {
trace!(?msg, "handling message");
match msg {
Message::RunCheck {
relay_map,
opts,
response_tx,
} => {
self.handle_run_check(relay_map, opts, response_tx);
}
Message::ReportReady { report } => {
self.handle_report_ready(*report);
}
Message::ReportAborted { err } => {
self.handle_report_aborted(err);
}
Message::StunPacket { payload, from_addr } => {
self.handle_stun_packet(&payload, from_addr);
}
Message::InFlightStun(inflight, response_tx) => {
self.handle_in_flight_stun(inflight, response_tx);
}
}
}
}
fn handle_run_check(
&mut self,
relay_map: RelayMap,
opts: Options,
response_tx: oneshot::Sender<Result<Arc<Report>>>,
) {
let protocols = opts.to_protocols();
#[cfg(not(wasm_browser))]
let socket_state = SocketState {
port_mapper: self.port_mapper.clone(),
stun_sock4: opts.stun_sock_v4,
stun_sock6: opts.stun_sock_v6,
quic_config: opts.quic_config,
dns_resolver: self.dns_resolver.clone(),
ip_mapped_addrs: self.ip_mapped_addrs.clone(),
};
trace!("Attempting probes for protocols {protocols:#?}");
if self.current_report_run.is_some() {
response_tx
.send(Err(anyhow!(
"ignoring RunCheck request: reportgen actor already running"
)))
.ok();
return;
}
let now = Instant::now();
let mut do_full = self.reports.next_full
|| now.duration_since(self.reports.last_full) > FULL_REPORT_INTERVAL;
if !do_full {
if let Some(ref last) = self.reports.last {
do_full = !last.udp && last.captive_portal.unwrap_or_default();
}
}
if do_full {
self.reports.last = None; self.reports.next_full = false;
self.reports.last_full = now;
self.metrics.reports_full.inc();
}
self.metrics.reports.inc();
let actor = reportgen::Client::new(
self.addr(),
self.reports.last.clone(),
relay_map,
protocols,
self.metrics.clone(),
#[cfg(not(wasm_browser))]
socket_state,
);
self.current_report_run = Some(ReportRun {
_reportgen: actor,
report_tx: response_tx,
});
}
fn handle_report_ready(&mut self, report: Report) {
let report = self.finish_and_store_report(report);
self.in_flight_stun_requests.clear();
if let Some(ReportRun { report_tx, .. }) = self.current_report_run.take() {
report_tx.send(Ok(report)).ok();
}
}
fn handle_report_aborted(&mut self, err: anyhow::Error) {
self.in_flight_stun_requests.clear();
if let Some(ReportRun { report_tx, .. }) = self.current_report_run.take() {
report_tx.send(Err(err.context("report aborted"))).ok();
}
}
fn handle_stun_packet(&mut self, pkt: &[u8], src: SocketAddr) {
trace!(%src, "received STUN packet");
if self.in_flight_stun_requests.is_empty() {
return;
}
#[cfg(feature = "metrics")]
match &src {
SocketAddr::V4(_) => {
self.metrics.stun_packets_recv_ipv4.inc();
}
SocketAddr::V6(_) => {
self.metrics.stun_packets_recv_ipv6.inc();
}
}
match stun::parse_response(pkt) {
Ok((txn, addr_port)) => match self.in_flight_stun_requests.remove(&txn) {
Some(inf) => {
debug!(%src, %txn, "received known STUN packet");
let elapsed = inf.start.elapsed();
inf.s.send((elapsed, addr_port)).ok();
}
None => {
debug!(%src, %txn, "received unexpected STUN message response");
}
},
Err(err) => {
match stun::parse_binding_request(pkt) {
Ok(txn) => {
match self.in_flight_stun_requests.remove(&txn) {
Some(inf) => {
debug!(%src, %txn, "received our hairpin STUN request");
let elapsed = inf.start.elapsed();
inf.s.send((elapsed, src)).ok();
}
None => {
debug!(%src, %txn, "unknown STUN request");
}
}
}
Err(_) => {
debug!(%src, "received invalid STUN response: {err:#}");
}
}
}
}
}
fn handle_in_flight_stun(&mut self, inflight: Inflight, response_tx: oneshot::Sender<()>) {
self.in_flight_stun_requests.insert(inflight.txn, inflight);
response_tx.send(()).ok();
}
fn finish_and_store_report(&mut self, report: Report) -> Arc<Report> {
let report = self.add_report_history_and_set_preferred_relay(report);
debug!("{report:?}");
report
}
fn add_report_history_and_set_preferred_relay(&mut self, mut r: Report) -> Arc<Report> {
let mut prev_relay = None;
if let Some(ref last) = self.reports.last {
prev_relay.clone_from(&last.preferred_relay);
}
let now = Instant::now();
const MAX_AGE: Duration = Duration::from_secs(5 * 60);
let mut best_recent = RelayLatencies::new();
let prevs_iter = self
.reports
.prev
.iter()
.map(|(a, b)| -> (&Instant, &Report) { (a, b) })
.chain(std::iter::once((&now, &r)));
let mut to_remove = Vec::new();
for (t, pr) in prevs_iter {
if now.duration_since(*t) > MAX_AGE {
to_remove.push(*t);
continue;
}
best_recent.merge(&pr.relay_latency);
}
for t in to_remove {
self.reports.prev.remove(&t);
}
let mut best_any = Duration::default();
let mut old_relay_cur_latency = Duration::default();
{
for (url, duration) in r.relay_latency.iter() {
if Some(url) == prev_relay.as_ref() {
old_relay_cur_latency = duration;
}
if let Some(best) = best_recent.get(url) {
if r.preferred_relay.is_none() || best < best_any {
best_any = best;
r.preferred_relay.replace(url.clone());
}
}
}
if prev_relay.is_some()
&& r.preferred_relay != prev_relay
&& !old_relay_cur_latency.is_zero()
&& best_any > old_relay_cur_latency / 3 * 2
{
r.preferred_relay = prev_relay;
}
}
let r = Arc::new(r);
self.reports.prev.insert(now, r.clone());
self.reports.last = Some(r.clone());
r
}
}
#[derive(Debug)]
struct ReportRun {
_reportgen: reportgen::Client,
report_tx: oneshot::Sender<Result<Arc<Report>>>,
}
#[cfg(not(wasm_browser))]
fn os_has_ipv6() -> bool {
UdpSocket::bind_local_v6(0).is_ok()
}
#[cfg(wasm_browser)]
fn os_has_ipv6() -> bool {
false
}
#[cfg(not(wasm_browser))]
pub(crate) mod stun_utils {
use anyhow::Context as _;
use netwatch::IpFamily;
use tokio_util::sync::CancellationToken;
use super::*;
pub fn bind_local_stun_socket(
network: IpFamily,
actor_addr: Addr,
cancel_token: CancellationToken,
) -> Option<Arc<UdpSocket>> {
let sock = match UdpSocket::bind(network, 0) {
Ok(sock) => Arc::new(sock),
Err(err) => {
debug!("failed to bind STUN socket: {}", err);
return None;
}
};
let span = info_span!(
"stun_udp_listener",
local_addr = sock
.local_addr()
.map(|a| a.to_string())
.unwrap_or(String::from("-")),
);
{
let sock = sock.clone();
task::spawn(
async move {
debug!("udp stun socket listener started");
let mut buf = vec![0u8; 64 << 10];
loop {
tokio::select! {
biased;
_ = cancel_token.cancelled() => break,
res = recv_stun_once(&sock, &mut buf, &actor_addr) => {
if let Err(err) = res {
warn!(%err, "stun recv failed");
break;
}
}
}
}
debug!("udp stun socket listener stopped");
}
.instrument(span),
);
}
Some(sock)
}
async fn recv_stun_once(sock: &UdpSocket, buf: &mut [u8], actor_addr: &Addr) -> Result<()> {
let (count, mut from_addr) = sock
.recv_from(buf)
.await
.context("Error reading from stun socket")?;
let payload = &buf[..count];
from_addr.set_ip(from_addr.ip().to_canonical());
let msg = Message::StunPacket {
payload: Bytes::from(payload.to_vec()),
from_addr,
};
actor_addr.send(msg).await.context("actor stopped")
}
}
#[cfg(test)]
mod test_utils {
use iroh_relay::{server, RelayNode, RelayQuicConfig};
pub(crate) async fn relay() -> (server::Server, RelayNode) {
let server = server::Server::spawn(server::testing::server_config())
.await
.expect("should serve relay");
let quic = Some(RelayQuicConfig {
port: server.quic_addr().expect("server should run quic").port(),
});
let node_desc = RelayNode {
url: server.https_url().expect("should work as relay"),
stun_only: false, stun_port: server.stun_addr().expect("server should serve stun").port(),
quic,
};
(server, node_desc)
}
pub(crate) async fn relay_map(relays: usize) -> (Vec<server::Server>, crate::RelayMap) {
let mut servers = Vec::with_capacity(relays);
let mut nodes = Vec::with_capacity(relays);
for _ in 0..relays {
let (relay_server, node) = relay().await;
servers.push(relay_server);
nodes.push(node);
}
(servers, crate::RelayMap::from_iter(nodes))
}
}
#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use bytes::BytesMut;
use netwatch::IpFamily;
use tokio_util::sync::CancellationToken;
use tracing::info;
use tracing_test::traced_test;
use super::*;
use crate::net_report::{dns, ping::Pinger, stun_utils::bind_local_stun_socket};
mod stun_utils {
use std::{net::IpAddr, sync::Arc};
use anyhow::Result;
use iroh_base::RelayUrl;
use iroh_relay::RelayNode;
use tokio::{
net,
sync::{oneshot, Mutex},
};
use tracing::{debug, trace};
use super::*;
#[derive(Debug)]
pub struct CleanupDropGuard {
_guard: oneshot::Sender<()>,
}
#[derive(Debug, Default, Clone)]
pub struct StunStats(Arc<Mutex<(usize, usize)>>);
impl StunStats {
pub async fn total(&self) -> usize {
let s = self.0.lock().await;
s.0 + s.1
}
}
pub fn relay_map_of(stun: impl Iterator<Item = SocketAddr>) -> RelayMap {
relay_map_of_opts(stun.map(|addr| (addr, true)))
}
pub fn relay_map_of_opts(stun: impl Iterator<Item = (SocketAddr, bool)>) -> RelayMap {
let nodes = stun.map(|(addr, stun_only)| {
let host = addr.ip();
let port = addr.port();
let url: RelayUrl = format!("http://{host}:{port}").parse().unwrap();
RelayNode {
url,
stun_port: port,
stun_only,
quic: None,
}
});
RelayMap::from_iter(nodes)
}
pub(crate) async fn serve_v4() -> Result<(SocketAddr, StunStats, CleanupDropGuard)> {
serve(std::net::Ipv4Addr::UNSPECIFIED.into()).await
}
pub(crate) async fn serve(ip: IpAddr) -> Result<(SocketAddr, StunStats, CleanupDropGuard)> {
let stats = StunStats::default();
let pc = net::UdpSocket::bind((ip, 0)).await?;
let mut addr = pc.local_addr()?;
match addr.ip() {
IpAddr::V4(ip) => {
if ip.octets() == [0, 0, 0, 0] {
addr.set_ip("127.0.0.1".parse().unwrap());
}
}
_ => unreachable!("using ipv4"),
}
println!("STUN listening on {}", addr);
let (_guard, r) = oneshot::channel();
let stats_c = stats.clone();
tokio::task::spawn(async move {
run_stun(pc, stats_c, r).await;
});
Ok((addr, stats, CleanupDropGuard { _guard }))
}
async fn run_stun(pc: net::UdpSocket, stats: StunStats, mut done: oneshot::Receiver<()>) {
let mut buf = vec![0u8; 64 << 10];
loop {
trace!("read loop");
tokio::select! {
_ = &mut done => {
debug!("shutting down");
break;
}
res = pc.recv_from(&mut buf) => match res {
Ok((n, addr)) => {
trace!("read packet {}bytes from {}", n, addr);
let pkt = &buf[..n];
if !stun::is(pkt) {
debug!("received non STUN pkt");
continue;
}
if let Ok(txid) = stun::parse_binding_request(pkt) {
debug!("received binding request");
let mut s = stats.0.lock().await;
if addr.is_ipv4() {
s.0 += 1;
} else {
s.1 += 1;
}
drop(s);
let res = stun::response(txid, addr);
if let Err(err) = pc.send_to(&res, addr).await {
eprintln!("STUN server write failed: {:?}", err);
}
}
}
Err(err) => {
eprintln!("failed to read: {:?}", err);
}
}
}
}
}
}
#[tokio::test]
#[traced_test]
async fn test_basic() -> Result<()> {
let (stun_addr, stun_stats, _cleanup_guard) =
stun_utils::serve("127.0.0.1".parse().unwrap()).await?;
let resolver = dns::tests::resolver();
let mut client = Client::new(None, resolver.clone(), None, Default::default())?;
let dm = stun_utils::relay_map_of([stun_addr].into_iter());
for i in 0..5 {
let cancel = CancellationToken::new();
let sock = bind_local_stun_socket(IpFamily::V4, client.addr(), cancel.clone());
println!("--round {}", i);
let r = client.get_report_all(dm.clone(), sock, None, None).await?;
assert!(r.udp, "want UDP");
assert_eq!(
r.relay_latency.len(),
1,
"expected 1 key in RelayLatency; got {}",
r.relay_latency.len()
);
assert!(
r.relay_latency.iter().next().is_some(),
"expected key 1 in RelayLatency; got {:?}",
r.relay_latency
);
assert!(r.global_v4.is_some(), "expected globalV4 set");
assert!(r.preferred_relay.is_some(),);
cancel.cancel();
}
assert!(
stun_stats.total().await >= 5,
"expected at least 5 stun, got {}",
stun_stats.total().await,
);
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_udp_blocked() -> Result<()> {
let blackhole = tokio::net::UdpSocket::bind("127.0.0.1:0").await?;
let stun_addr = blackhole.local_addr()?;
let dm = stun_utils::relay_map_of_opts([(stun_addr, false)].into_iter());
let resolver = dns::tests::resolver();
let mut client = Client::new(None, resolver.clone(), None, Default::default())?;
let r = client.get_report_all(dm, None, None, None).await?;
let mut r: Report = (*r).clone();
r.portmap_probe = None;
let pinger = Pinger::new();
let can_ping = pinger.send(Ipv4Addr::LOCALHOST.into(), b"aa").await.is_ok();
let want_icmpv4 = match can_ping {
true => Some(true),
false => None,
};
let want = Report {
ipv4_can_send: can_ping,
os_has_ipv6: r.os_has_ipv6,
captive_portal: r.captive_portal,
icmpv4: want_icmpv4,
relay_latency: can_ping
.then(|| r.relay_latency.clone())
.unwrap_or_default(),
preferred_relay: can_ping
.then_some(r.preferred_relay.clone())
.unwrap_or_default(),
..Default::default()
};
assert_eq!(r, want);
Ok(())
}
#[tokio::test(flavor = "current_thread", start_paused = true)]
async fn test_add_report_history_set_preferred_relay() -> Result<()> {
fn relay_url(i: u16) -> RelayUrl {
format!("http://{i}.com").parse().unwrap()
}
fn report(a: impl IntoIterator<Item = (&'static str, u64)>) -> Option<Arc<Report>> {
let mut report = Report::default();
for (s, d) in a {
assert!(s.starts_with('d'), "invalid relay server key");
let id: u16 = s[1..].parse().unwrap();
report
.relay_latency
.0
.insert(relay_url(id), Duration::from_secs(d));
}
Some(Arc::new(report))
}
struct Step {
after: u64,
r: Option<Arc<Report>>,
}
struct Test {
name: &'static str,
steps: Vec<Step>,
want_relay: Option<RelayUrl>,
want_prev_len: usize,
}
let tests = [
Test {
name: "first_reading",
steps: vec![Step {
after: 0,
r: report([("d1", 2), ("d2", 3)]),
}],
want_prev_len: 1,
want_relay: Some(relay_url(1)),
},
Test {
name: "with_two",
steps: vec![
Step {
after: 0,
r: report([("d1", 2), ("d2", 3)]),
},
Step {
after: 1,
r: report([("d1", 4), ("d2", 3)]),
},
],
want_prev_len: 2,
want_relay: Some(relay_url(1)), },
Test {
name: "but_now_d1_gone",
steps: vec![
Step {
after: 0,
r: report([("d1", 2), ("d2", 3)]),
},
Step {
after: 1,
r: report([("d1", 4), ("d2", 3)]),
},
Step {
after: 2,
r: report([("d2", 3)]),
},
],
want_prev_len: 3,
want_relay: Some(relay_url(2)), },
Test {
name: "d1_is_back",
steps: vec![
Step {
after: 0,
r: report([("d1", 2), ("d2", 3)]),
},
Step {
after: 1,
r: report([("d1", 4), ("d2", 3)]),
},
Step {
after: 2,
r: report([("d2", 3)]),
},
Step {
after: 3,
r: report([("d1", 4), ("d2", 3)]),
}, ],
want_prev_len: 4,
want_relay: Some(relay_url(1)), },
Test {
name: "things_clean_up",
steps: vec![
Step {
after: 0,
r: report([("d1", 1), ("d2", 2)]),
},
Step {
after: 1,
r: report([("d1", 1), ("d2", 2)]),
},
Step {
after: 2,
r: report([("d1", 1), ("d2", 2)]),
},
Step {
after: 3,
r: report([("d1", 1), ("d2", 2)]),
},
Step {
after: 10 * 60,
r: report([("d3", 3)]),
},
],
want_prev_len: 1, want_relay: Some(relay_url(3)), },
Test {
name: "preferred_relay_hysteresis_no_switch",
steps: vec![
Step {
after: 0,
r: report([("d1", 4), ("d2", 5)]),
},
Step {
after: 1,
r: report([("d1", 4), ("d2", 3)]),
},
],
want_prev_len: 2,
want_relay: Some(relay_url(1)), },
Test {
name: "preferred_relay_hysteresis_do_switch",
steps: vec![
Step {
after: 0,
r: report([("d1", 4), ("d2", 5)]),
},
Step {
after: 1,
r: report([("d1", 4), ("d2", 1)]),
},
],
want_prev_len: 2,
want_relay: Some(relay_url(2)), },
];
let resolver = dns::tests::resolver();
for mut tt in tests {
println!("test: {}", tt.name);
let mut actor = Actor::new(None, resolver.clone(), None, Default::default()).unwrap();
for s in &mut tt.steps {
tokio::time::advance(Duration::from_secs(s.after)).await;
let r = Arc::try_unwrap(s.r.take().unwrap()).unwrap();
s.r = Some(actor.add_report_history_and_set_preferred_relay(r));
}
let last_report = tt.steps.last().unwrap().r.clone().unwrap();
let got = actor.reports.prev.len();
let want = tt.want_prev_len;
assert_eq!(got, want, "prev length");
let got = &last_report.preferred_relay;
let want = &tt.want_relay;
assert_eq!(got, want, "preferred_relay");
}
Ok(())
}
#[tokio::test]
async fn test_hairpin() -> Result<()> {
let (stun_addr, _stun_stats, _done) = stun_utils::serve_v4().await?;
let dm = stun_utils::relay_map_of([stun_addr].into_iter());
dbg!(&dm);
let resolver = dns::tests::resolver().clone();
let mut client = Client::new(None, resolver, None, Default::default())?;
let sock = UdpSocket::bind_local(netwatch::IpFamily::V4, 0)?;
let sock = Arc::new(sock);
info!(addr=?sock.local_addr().unwrap(), "Using local addr");
let task = {
let sock = sock.clone();
let addr = client.addr.clone();
tokio::spawn(
async move {
let mut buf = BytesMut::zeroed(64 << 10);
loop {
let (count, src) = sock.recv_from(&mut buf).await.unwrap();
info!(
addr=?sock.local_addr().unwrap(),
%count,
"Forwarding payload to net_report client",
);
let payload = buf.split_to(count).freeze();
addr.receive_stun_packet(payload, src);
}
}
.instrument(info_span!("pkt-fwd")),
)
};
let r = client.get_report_all(dm, Some(sock), None, None).await?;
dbg!(&r);
assert_eq!(r.hair_pinning, Some(true));
task.abort();
Ok(())
}
}