#[cfg(test)]
use std::net::SocketAddr;
use std::{
collections::{BTreeMap, BTreeSet},
future::Future,
net::IpAddr,
pin::{Pin, pin},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use backon::{Backoff, BackoffBuilder, ExponentialBuilder};
use iroh_base::{NodeId, RelayUrl, SecretKey};
use iroh_relay::{
self as relay, PingTracker,
client::{Client, ConnectError, RecvError, SendError},
protos::relay::{ClientToRelayMsg, Datagrams, RelayToClientMsg},
};
use n0_future::{
FuturesUnorderedBounded, SinkExt, StreamExt,
task::JoinSet,
time::{self, Duration, Instant, MissedTickBehavior},
};
use n0_watcher::Watchable;
use nested_enum_utils::common_fields;
use netwatch::interfaces;
use snafu::{IntoError, ResultExt, Snafu};
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn};
use url::Url;
#[cfg(not(wasm_browser))]
use crate::dns::DnsResolver;
use crate::{magicsock::Metrics as MagicsockMetrics, net_report::Report, util::MaybeFuture};
const RELAY_INACTIVE_CLEANUP_TIME: Duration = Duration::from_secs(60);
const PING_INTERVAL: Duration = Duration::from_secs(15);
const SEND_DATAGRAM_BATCH_SIZE: usize = 20;
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const UNDELIVERABLE_DATAGRAM_TIMEOUT: Duration = Duration::from_secs(3);
#[derive(Debug)]
struct ActiveRelayActor {
prio_inbox: mpsc::Receiver<ActiveRelayPrioMessage>,
inbox: mpsc::Receiver<ActiveRelayMessage>,
relay_datagrams_recv: mpsc::Sender<RelayRecvDatagram>,
relay_datagrams_send: mpsc::Receiver<RelaySendItem>,
url: RelayUrl,
relay_client_builder: relay::client::ClientBuilder,
is_home_relay: bool,
inactive_timeout: Pin<Box<time::Sleep>>,
stop_token: CancellationToken,
metrics: Arc<MagicsockMetrics>,
}
#[derive(Debug)]
enum ActiveRelayMessage {
CheckConnection(Vec<IpAddr>),
SetHomeRelay(bool),
#[cfg(test)]
GetLocalAddr(oneshot::Sender<Option<SocketAddr>>),
#[cfg(test)]
PingServer(oneshot::Sender<()>),
}
#[derive(Debug)]
enum ActiveRelayPrioMessage {
HasNodeRoute(NodeId, oneshot::Sender<bool>),
}
#[derive(Debug)]
struct ActiveRelayActorOptions {
url: RelayUrl,
prio_inbox_: mpsc::Receiver<ActiveRelayPrioMessage>,
inbox: mpsc::Receiver<ActiveRelayMessage>,
relay_datagrams_send: mpsc::Receiver<RelaySendItem>,
relay_datagrams_recv: mpsc::Sender<RelayRecvDatagram>,
connection_opts: RelayConnectionOptions,
stop_token: CancellationToken,
metrics: Arc<MagicsockMetrics>,
}
#[derive(Debug, Clone)]
struct RelayConnectionOptions {
secret_key: SecretKey,
#[cfg(not(wasm_browser))]
dns_resolver: DnsResolver,
proxy_url: Option<Url>,
prefer_ipv6: Arc<AtomicBool>,
#[cfg(any(test, feature = "test-utils"))]
insecure_skip_cert_verify: bool,
}
#[allow(missing_docs)]
#[common_fields({
backtrace: Option<snafu::Backtrace>,
#[snafu(implicit)]
span_trace: n0_snafu::SpanTrace,
})]
#[derive(Debug, Snafu)]
enum RelayConnectionError {
#[snafu(display("Failed to connect to relay server"))]
Dial { source: DialError },
#[snafu(display("Failed to handshake with relay server"))]
Handshake { source: RunError },
#[snafu(display("Lost connection to relay server"))]
Established { source: RunError },
}
#[allow(missing_docs)]
#[common_fields({
backtrace: Option<snafu::Backtrace>,
#[snafu(implicit)]
span_trace: n0_snafu::SpanTrace,
})]
#[derive(Debug, Snafu)]
enum RunError {
#[snafu(display("Send timeout"))]
SendTimeout {},
#[snafu(display("Ping timeout"))]
PingTimeout {},
#[snafu(display("Local IP no longer valid"))]
LocalIpInvalid {},
#[snafu(display("No local address"))]
LocalAddrMissing {},
#[snafu(display("Stream closed by server."))]
StreamClosedServer {},
#[snafu(display("Client stream read failed"))]
ClientStreamRead { source: RecvError },
#[snafu(display("Client stream write failed"))]
ClientStreamWrite { source: SendError },
}
#[allow(missing_docs)]
#[common_fields({
backtrace: Option<snafu::Backtrace>,
#[snafu(implicit)]
span_trace: n0_snafu::SpanTrace,
})]
#[derive(Debug, Snafu)]
enum DialError {
#[snafu(display("timeout (>{timeout:?}) trying to establish a connection"))]
Timeout { timeout: Duration },
#[snafu(display("unable to connect"))]
Connect {
#[snafu(source(from(ConnectError, Box::new)))]
source: Box<ConnectError>,
},
}
impl ActiveRelayActor {
fn new(opts: ActiveRelayActorOptions) -> Self {
let ActiveRelayActorOptions {
url,
prio_inbox_: prio_inbox,
inbox,
relay_datagrams_send,
relay_datagrams_recv,
connection_opts,
stop_token,
metrics,
} = opts;
let relay_client_builder = Self::create_relay_builder(url.clone(), connection_opts);
ActiveRelayActor {
prio_inbox,
inbox,
relay_datagrams_recv,
relay_datagrams_send,
url,
relay_client_builder,
is_home_relay: false,
inactive_timeout: Box::pin(time::sleep(RELAY_INACTIVE_CLEANUP_TIME)),
stop_token,
metrics,
}
}
fn create_relay_builder(
url: RelayUrl,
opts: RelayConnectionOptions,
) -> relay::client::ClientBuilder {
let RelayConnectionOptions {
secret_key,
#[cfg(not(wasm_browser))]
dns_resolver,
proxy_url,
prefer_ipv6,
#[cfg(any(test, feature = "test-utils"))]
insecure_skip_cert_verify,
} = opts;
let mut builder = relay::client::ClientBuilder::new(
url,
secret_key,
#[cfg(not(wasm_browser))]
dns_resolver,
)
.address_family_selector(move || prefer_ipv6.load(Ordering::Relaxed));
if let Some(proxy_url) = proxy_url {
builder = builder.proxy_url(proxy_url);
}
#[cfg(any(test, feature = "test-utils"))]
let builder = builder.insecure_skip_cert_verify(insecure_skip_cert_verify);
builder
}
async fn run(mut self) {
let mut backoff = Self::build_backoff();
while let Err(err) = self.run_once().await {
warn!("{err}");
match err {
RelayConnectionError::Dial { .. } | RelayConnectionError::Handshake { .. } => {
let Some(delay) = backoff.next() else {
warn!("retries exceeded");
break;
};
debug!("retry in {delay:?}");
time::sleep(delay).await;
}
RelayConnectionError::Established { .. } => {
backoff = Self::build_backoff();
}
}
}
debug!("exiting");
}
fn build_backoff() -> impl Backoff {
ExponentialBuilder::new()
.with_min_delay(Duration::from_millis(10))
.with_max_delay(Duration::from_secs(16))
.with_jitter()
.without_max_times()
.build()
}
async fn run_once(&mut self) -> Result<(), RelayConnectionError> {
let client = match self.run_dialing().instrument(info_span!("dialing")).await {
Some(client_res) => client_res.context(DialSnafu)?,
None => return Ok(()),
};
self.run_connected(client)
.instrument(info_span!("connected"))
.await
}
fn reset_inactive_timeout(&mut self) {
self.inactive_timeout
.as_mut()
.reset(Instant::now() + RELAY_INACTIVE_CLEANUP_TIME);
}
fn set_home_relay(&mut self, is_home: bool) {
let prev = std::mem::replace(&mut self.is_home_relay, is_home);
if self.is_home_relay != prev {
event!(
target: "iroh::_events::relay::home_changed",
Level::DEBUG,
url = %self.url,
home_relay = self.is_home_relay,
);
}
}
async fn run_dialing(&mut self) -> Option<Result<iroh_relay::client::Client, DialError>> {
debug!("Actor loop: connecting to relay.");
let mut send_datagram_flush = time::interval(UNDELIVERABLE_DATAGRAM_TIMEOUT);
send_datagram_flush.set_missed_tick_behavior(MissedTickBehavior::Delay);
send_datagram_flush.reset();
let dialing_fut = self.dial_relay();
tokio::pin!(dialing_fut);
loop {
tokio::select! {
biased;
_ = self.stop_token.cancelled() => {
debug!("Shutdown.");
break None;
}
msg = self.prio_inbox.recv() => {
let Some(msg) = msg else {
warn!("Priority inbox closed, shutdown.");
break None;
};
match msg {
ActiveRelayPrioMessage::HasNodeRoute(_peer, sender) => {
sender.send(false).ok();
}
}
}
res = &mut dialing_fut => {
match res {
Ok(client) => {
break Some(Ok(client));
}
Err(err) => {
break Some(Err(err));
}
}
}
msg = self.inbox.recv() => {
let Some(msg) = msg else {
debug!("Inbox closed, shutdown.");
break None;
};
match msg {
ActiveRelayMessage::SetHomeRelay(is_home) => {
self.set_home_relay(is_home);
}
ActiveRelayMessage::CheckConnection(_local_ips) => {}
#[cfg(test)]
ActiveRelayMessage::GetLocalAddr(sender) => {
sender.send(None).ok();
}
#[cfg(test)]
ActiveRelayMessage::PingServer(sender) => {
drop(sender);
}
}
}
_ = send_datagram_flush.tick() => {
self.reset_inactive_timeout();
let mut logged = false;
while self.relay_datagrams_send.try_recv().is_ok() {
if !logged {
debug!(?UNDELIVERABLE_DATAGRAM_TIMEOUT, "Dropping datagrams to send.");
logged = true;
}
}
}
_ = &mut self.inactive_timeout, if !self.is_home_relay => {
debug!(?RELAY_INACTIVE_CLEANUP_TIME, "Inactive, exiting.");
break None;
}
}
}
}
fn dial_relay(&self) -> impl Future<Output = Result<Client, DialError>> + use<> {
let client_builder = self.relay_client_builder.clone();
async move {
match time::timeout(CONNECT_TIMEOUT, client_builder.connect()).await {
Ok(Ok(client)) => Ok(client),
Ok(Err(err)) => Err(ConnectSnafu.into_error(err)),
Err(_) => Err(TimeoutSnafu {
timeout: CONNECT_TIMEOUT,
}
.build()),
}
}
}
async fn run_connected(
&mut self,
client: iroh_relay::client::Client,
) -> Result<(), RelayConnectionError> {
debug!("Actor loop: connected to relay");
event!(
target: "iroh::_events::relay::connected",
Level::DEBUG,
url = %self.url,
home_relay = self.is_home_relay,
);
let (mut client_stream, client_sink) = client.split();
let mut client_sink = client_sink.sink_map_err(|e| ClientStreamWriteSnafu.into_error(e));
let mut state = ConnectedRelayState {
ping_tracker: PingTracker::default(),
nodes_present: BTreeSet::new(),
last_packet_src: None,
pong_pending: None,
established: false,
#[cfg(test)]
test_pong: None,
};
let mut send_datagrams_buf = Vec::with_capacity(SEND_DATAGRAM_BATCH_SIZE);
let mut ping_interval = time::interval(PING_INTERVAL);
ping_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
let res = loop {
if let Some(data) = state.pong_pending.take() {
let fut = client_sink.send(ClientToRelayMsg::Pong(data));
self.run_sending(fut, &mut state, &mut client_stream)
.await?;
}
tokio::select! {
biased;
_ = self.stop_token.cancelled() => {
debug!("Shutdown.");
break Ok(());
}
msg = self.prio_inbox.recv() => {
let Some(msg) = msg else {
warn!("Priority inbox closed, shutdown.");
break Ok(());
};
match msg {
ActiveRelayPrioMessage::HasNodeRoute(peer, sender) => {
let has_peer = state.nodes_present.contains(&peer);
sender.send(has_peer).ok();
}
}
}
_ = state.ping_tracker.timeout() => {
break Err(PingTimeoutSnafu.build());
}
_ = ping_interval.tick() => {
let data = state.ping_tracker.new_ping();
let fut = client_sink.send(ClientToRelayMsg::Ping(data));
self.run_sending(fut, &mut state, &mut client_stream).await?;
}
msg = self.inbox.recv() => {
let Some(msg) = msg else {
warn!("Inbox closed, shutdown.");
break Ok(());
};
match msg {
ActiveRelayMessage::SetHomeRelay(is_home) => {
self.set_home_relay(is_home);
}
ActiveRelayMessage::CheckConnection(local_ips) => {
match client_stream.local_addr() {
Some(addr) if local_ips.contains(&addr.ip()) => {
let data = state.ping_tracker.new_ping();
let fut = client_sink.send(ClientToRelayMsg::Ping(data));
self.run_sending(fut, &mut state, &mut client_stream).await?;
}
Some(_) => break Err(LocalIpInvalidSnafu.build()),
None => break Err(LocalAddrMissingSnafu.build()),
}
}
#[cfg(test)]
ActiveRelayMessage::GetLocalAddr(sender) => {
let addr = client_stream.local_addr();
sender.send(addr).ok();
}
#[cfg(test)]
ActiveRelayMessage::PingServer(sender) => {
let data = rand::random();
state.test_pong = Some((data, sender));
let fut = client_sink.send(ClientToRelayMsg::Ping(data));
self.run_sending(fut, &mut state, &mut client_stream).await?;
}
}
}
count = self.relay_datagrams_send.recv_many(
&mut send_datagrams_buf,
SEND_DATAGRAM_BATCH_SIZE,
) => {
if count == 0 {
warn!("Datagram inbox closed, shutdown");
break Ok(());
};
self.reset_inactive_timeout();
let metrics = self.metrics.clone();
let packet_iter = send_datagrams_buf.drain(..).map(|item| {
metrics.send_relay.inc_by(item.datagrams.contents.len() as _);
Ok(ClientToRelayMsg::Datagrams {
dst_node_id: item.remote_node,
datagrams: item.datagrams,
})
});
let mut packet_stream = n0_future::stream::iter(packet_iter);
let fut = client_sink.send_all(&mut packet_stream);
self.run_sending(fut, &mut state, &mut client_stream).await?;
}
msg = client_stream.next() => {
let Some(msg) = msg else {
break Err(StreamClosedServerSnafu.build());
};
match msg {
Ok(msg) => {
self.handle_relay_msg(msg, &mut state);
ping_interval.reset();
},
Err(err) => break Err(ClientStreamReadSnafu.into_error(err)),
}
}
_ = &mut self.inactive_timeout, if !self.is_home_relay => {
debug!("Inactive for {RELAY_INACTIVE_CLEANUP_TIME:?}, exiting.");
break Ok(());
}
}
};
if res.is_ok() {
if let Err(err) = client_sink.close().await {
debug!("Failed to close client sink gracefully: {err:#}");
}
}
res.map_err(|err| state.map_err(err))
}
fn handle_relay_msg(&mut self, msg: RelayToClientMsg, state: &mut ConnectedRelayState) {
match msg {
RelayToClientMsg::Datagrams {
remote_node_id,
datagrams,
} => {
trace!(len = datagrams.contents.len(), "received msg");
if state
.last_packet_src
.as_ref()
.map(|p| *p != remote_node_id)
.unwrap_or(true)
{
state.last_packet_src = Some(remote_node_id);
state.nodes_present.insert(remote_node_id);
}
if let Err(err) = self.relay_datagrams_recv.try_send(RelayRecvDatagram {
url: self.url.clone(),
src: remote_node_id,
datagrams,
}) {
warn!("Dropping received relay packet: {err:#}");
}
}
RelayToClientMsg::NodeGone(node_id) => {
state.nodes_present.remove(&node_id);
}
RelayToClientMsg::Ping(data) => state.pong_pending = Some(data),
RelayToClientMsg::Pong(data) => {
#[cfg(test)]
{
if let Some((expected_data, sender)) = state.test_pong.take() {
if data == expected_data {
sender.send(()).ok();
} else {
state.test_pong = Some((expected_data, sender));
}
}
}
state.ping_tracker.pong_received(data);
state.established = true;
}
RelayToClientMsg::Health { problem } => {
warn!("Relay server reports problem: {problem}");
}
RelayToClientMsg::Restarting { .. } => {
trace!("Ignoring {msg:?}")
}
}
}
#[instrument(name = "tx", skip_all)]
async fn run_sending<T>(
&mut self,
sending_fut: impl Future<Output = Result<T, RunError>>,
state: &mut ConnectedRelayState,
client_stream: &mut iroh_relay::client::ClientStream,
) -> Result<(), RelayConnectionError> {
let send_timeout = PING_INTERVAL;
let mut timeout = pin!(time::sleep(send_timeout));
let mut sending_fut = pin!(sending_fut);
let res = loop {
tokio::select! {
biased;
_ = self.stop_token.cancelled() => {
break Ok(());
}
_ = &mut timeout => {
break Err(SendTimeoutSnafu.build());
}
msg = self.prio_inbox.recv() => {
let Some(msg) = msg else {
warn!("Priority inbox closed, shutdown.");
break Ok(());
};
match msg {
ActiveRelayPrioMessage::HasNodeRoute(peer, sender) => {
let has_peer = state.nodes_present.contains(&peer);
sender.send(has_peer).ok();
}
}
}
res = &mut sending_fut => {
match res {
Ok(_) => break Ok(()),
Err(err) => break Err(err),
}
}
_ = state.ping_tracker.timeout() => {
break Err(PingTimeoutSnafu.build());
}
msg = client_stream.next() => {
let Some(msg) = msg else {
break Err(StreamClosedServerSnafu.build());
};
match msg {
Ok(msg) => self.handle_relay_msg(msg, state),
Err(err) => break Err(ClientStreamReadSnafu.into_error(err)),
}
}
_ = &mut self.inactive_timeout, if !self.is_home_relay => {
debug!("Inactive for {RELAY_INACTIVE_CLEANUP_TIME:?}, exiting.");
break Ok(());
}
}
};
res.map_err(|err| state.map_err(err))
}
}
#[derive(Debug)]
struct ConnectedRelayState {
ping_tracker: PingTracker,
nodes_present: BTreeSet<NodeId>,
last_packet_src: Option<NodeId>,
pong_pending: Option<[u8; 8]>,
established: bool,
#[cfg(test)]
test_pong: Option<([u8; 8], oneshot::Sender<()>)>,
}
impl ConnectedRelayState {
fn map_err(&self, error: RunError) -> RelayConnectionError {
if self.established {
EstablishedSnafu.into_error(error)
} else {
HandshakeSnafu.into_error(error)
}
}
}
pub(super) enum RelayActorMessage {
MaybeCloseRelaysOnRebind,
NetworkChange { report: Report },
}
#[derive(Debug, Clone)]
pub(crate) struct RelaySendItem {
pub(crate) remote_node: NodeId,
pub(crate) url: RelayUrl,
pub(crate) datagrams: Datagrams,
}
pub(super) struct RelayActor {
config: Config,
relay_datagram_recv_queue: mpsc::Sender<RelayRecvDatagram>,
active_relays: BTreeMap<RelayUrl, ActiveRelayHandle>,
active_relay_tasks: JoinSet<()>,
cancel_token: CancellationToken,
}
#[derive(Debug)]
pub struct Config {
pub my_relay: Watchable<Option<RelayUrl>>,
pub secret_key: SecretKey,
#[cfg(not(wasm_browser))]
pub dns_resolver: DnsResolver,
pub proxy_url: Option<Url>,
pub ipv6_reported: Arc<AtomicBool>,
#[cfg(any(test, feature = "test-utils"))]
pub insecure_skip_relay_cert_verify: bool,
pub metrics: Arc<MagicsockMetrics>,
}
impl RelayActor {
pub(super) fn new(
config: Config,
relay_datagram_recv_queue: mpsc::Sender<RelayRecvDatagram>,
) -> Self {
let cancel_token = CancellationToken::new();
Self {
config,
relay_datagram_recv_queue,
active_relays: Default::default(),
active_relay_tasks: JoinSet::new(),
cancel_token,
}
}
pub(super) async fn run(
mut self,
mut receiver: mpsc::Receiver<RelayActorMessage>,
mut datagram_send_channel: mpsc::Receiver<RelaySendItem>,
) {
let mut datagram_send_fut = std::pin::pin!(MaybeFuture::none());
loop {
tokio::select! {
biased;
_ = self.cancel_token.cancelled() => {
debug!("shutting down");
break;
}
Some(res) = self.active_relay_tasks.join_next() => {
match res {
Ok(()) => (),
Err(err) if err.is_panic() => {
error!("ActiveRelayActor task panicked: {err:#?}");
}
Err(err) if err.is_cancelled() => {
error!("ActiveRelayActor cancelled: {err:#?}");
}
Err(err) => error!("ActiveRelayActor failed: {err:#?}"),
}
self.reap_active_relays();
}
msg = receiver.recv() => {
let Some(msg) = msg else {
debug!("Inbox dropped, shutting down.");
break;
};
let cancel_token = self.cancel_token.child_token();
cancel_token.run_until_cancelled(self.handle_msg(msg)).await;
}
item = datagram_send_channel.recv(), if datagram_send_fut.is_none() => {
let Some(item) = item else {
debug!("Datagram send channel dropped, shutting down.");
break;
};
let token = self.cancel_token.child_token();
if let Some(Some(fut)) = token.run_until_cancelled(
self.try_send_datagram(item)
).await {
datagram_send_fut.as_mut().set_future(fut);
}
}
_ = &mut datagram_send_fut, if datagram_send_fut.is_some() => {
datagram_send_fut.as_mut().set_none();
}
}
}
if time::timeout(Duration::from_secs(3), self.close_all_active_relays())
.await
.is_err()
{
warn!("Failed to shut down all ActiveRelayActors");
}
}
async fn handle_msg(&mut self, msg: RelayActorMessage) {
match msg {
RelayActorMessage::NetworkChange { report } => {
self.on_network_change(report).await;
}
RelayActorMessage::MaybeCloseRelaysOnRebind => {
self.maybe_close_relays_on_rebind().await;
}
}
}
async fn try_send_datagram(
&mut self,
item: RelaySendItem,
) -> Option<impl Future<Output = ()> + use<>> {
let url = item.url.clone();
let handle = self
.active_relay_handle_for_node(&item.url, &item.remote_node)
.await;
match handle.datagrams_send_queue.try_send(item) {
Ok(()) => None,
Err(mpsc::error::TrySendError::Closed(_)) => {
warn!(?url, "Dropped datagram(s): ActiveRelayActor closed.");
None
}
Err(mpsc::error::TrySendError::Full(item)) => {
let sender = handle.datagrams_send_queue.clone();
let fut = async move {
if sender.send(item).await.is_err() {
warn!(?url, "Dropped datagram(s): ActiveRelayActor closed.");
}
};
Some(fut)
}
}
}
async fn on_network_change(&mut self, report: Report) {
let my_relay = self.config.my_relay.get();
if report.preferred_relay == my_relay {
return;
}
let old_relay = self
.config
.my_relay
.set(report.preferred_relay.clone())
.unwrap_or_else(|e| e);
if let Some(relay_url) = report.preferred_relay {
self.config.metrics.relay_home_change.inc();
info!("home is now relay {}, was {:?}", relay_url, old_relay);
self.set_home_relay(relay_url).await;
}
}
async fn set_home_relay(&mut self, home_url: RelayUrl) {
let home_url_ref = &home_url;
n0_future::join_all(self.active_relays.iter().map(|(url, handle)| async move {
let is_preferred = url == home_url_ref;
handle
.inbox_addr
.send(ActiveRelayMessage::SetHomeRelay(is_preferred))
.await
.ok()
}))
.await;
self.active_relay_handle(home_url);
}
async fn active_relay_handle_for_node(
&mut self,
url: &RelayUrl,
remote_node: &NodeId,
) -> ActiveRelayHandle {
if let Some(handle) = self.active_relays.get(url) {
return handle.clone();
}
let mut found_relay: Option<RelayUrl> = None;
{
let check_futs = self.active_relays.iter().map(|(url, handle)| async move {
let (tx, rx) = oneshot::channel();
handle
.prio_inbox_addr
.send(ActiveRelayPrioMessage::HasNodeRoute(*remote_node, tx))
.await
.ok();
match rx.await {
Ok(true) => Some(url.clone()),
_ => None,
}
});
let mut futures = FuturesUnorderedBounded::from_iter(check_futs);
while let Some(maybe_url) = futures.next().await {
if maybe_url.is_some() {
found_relay = maybe_url;
break;
}
}
}
let url = found_relay.unwrap_or(url.clone());
self.active_relay_handle(url)
}
fn active_relay_handle(&mut self, url: RelayUrl) -> ActiveRelayHandle {
match self.active_relays.get(&url) {
Some(e) => e.clone(),
None => {
let handle = self.start_active_relay(url.clone());
if Some(&url) == self.config.my_relay.get().as_ref() {
if let Err(err) = handle
.inbox_addr
.try_send(ActiveRelayMessage::SetHomeRelay(true))
{
error!("Home relay not set, send to new actor failed: {err:#}.");
}
}
self.active_relays.insert(url, handle.clone());
self.log_active_relay();
handle
}
}
}
fn start_active_relay(&mut self, url: RelayUrl) -> ActiveRelayHandle {
debug!(?url, "Adding relay connection");
let connection_opts = RelayConnectionOptions {
secret_key: self.config.secret_key.clone(),
#[cfg(not(wasm_browser))]
dns_resolver: self.config.dns_resolver.clone(),
proxy_url: self.config.proxy_url.clone(),
prefer_ipv6: self.config.ipv6_reported.clone(),
#[cfg(any(test, feature = "test-utils"))]
insecure_skip_cert_verify: self.config.insecure_skip_relay_cert_verify,
};
let (send_datagram_tx, send_datagram_rx) = mpsc::channel(64);
let (prio_inbox_tx, prio_inbox_rx) = mpsc::channel(32);
let (inbox_tx, inbox_rx) = mpsc::channel(64);
let span = info_span!("active-relay", %url);
let opts = ActiveRelayActorOptions {
url,
prio_inbox_: prio_inbox_rx,
inbox: inbox_rx,
relay_datagrams_send: send_datagram_rx,
relay_datagrams_recv: self.relay_datagram_recv_queue.clone(),
connection_opts,
stop_token: self.cancel_token.child_token(),
metrics: self.config.metrics.clone(),
};
let actor = ActiveRelayActor::new(opts);
self.active_relay_tasks.spawn(
async move {
actor.run().await;
}
.instrument(span),
);
let handle = ActiveRelayHandle {
prio_inbox_addr: prio_inbox_tx,
inbox_addr: inbox_tx,
datagrams_send_queue: send_datagram_tx,
};
self.log_active_relay();
handle
}
async fn maybe_close_relays_on_rebind(&mut self) {
#[cfg(not(wasm_browser))]
let ifs = interfaces::State::new().await;
#[cfg(not(wasm_browser))]
let local_ips: Vec<_> = ifs
.interfaces
.values()
.flat_map(|netif| netif.addrs())
.map(|ipnet| ipnet.addr())
.collect();
#[cfg(wasm_browser)]
let local_ips = Vec::new();
let send_futs = self.active_relays.values().map(|handle| {
let local_ips = local_ips.clone();
async move {
handle
.inbox_addr
.send(ActiveRelayMessage::CheckConnection(local_ips))
.await
.ok();
}
});
n0_future::join_all(send_futs).await;
self.log_active_relay();
}
fn reap_active_relays(&mut self) {
self.active_relays
.retain(|_url, handle| !handle.inbox_addr.is_closed());
if let Some(url) = self.config.my_relay.get() {
self.active_relay_handle(url);
}
self.log_active_relay();
}
async fn close_all_active_relays(&mut self) {
self.cancel_token.cancel();
let tasks = std::mem::take(&mut self.active_relay_tasks);
tasks.join_all().await;
self.log_active_relay();
}
fn log_active_relay(&self) {
debug!("{} active relay conns{}", self.active_relays.len(), {
let mut s = String::new();
if !self.active_relays.is_empty() {
s += ":";
for node in self.active_relay_sorted() {
s += &format!(" relay-{node}");
}
}
s
});
}
fn active_relay_sorted(&self) -> impl Iterator<Item = RelayUrl> + use<> {
let mut ids: Vec<_> = self.active_relays.keys().cloned().collect();
ids.sort();
ids.into_iter()
}
}
#[derive(Debug, Clone)]
struct ActiveRelayHandle {
prio_inbox_addr: mpsc::Sender<ActiveRelayPrioMessage>,
inbox_addr: mpsc::Sender<ActiveRelayMessage>,
datagrams_send_queue: mpsc::Sender<RelaySendItem>,
}
#[derive(Debug)]
pub(crate) struct RelayRecvDatagram {
pub(crate) url: RelayUrl,
pub(crate) src: NodeId,
pub(crate) datagrams: Datagrams,
}
#[cfg(test)]
mod tests {
use std::{
sync::{Arc, atomic::AtomicBool},
time::Duration,
};
use iroh_base::{NodeId, RelayUrl, SecretKey};
use iroh_relay::{PingTracker, protos::relay::Datagrams};
use n0_snafu::{Error, Result, ResultExt};
use tokio::sync::{mpsc, oneshot};
use tokio_util::{sync::CancellationToken, task::AbortOnDropHandle};
use tracing::{Instrument, info, info_span};
use tracing_test::traced_test;
use super::{
ActiveRelayActor, ActiveRelayActorOptions, ActiveRelayMessage, ActiveRelayPrioMessage,
RELAY_INACTIVE_CLEANUP_TIME, RelayConnectionOptions, RelayRecvDatagram, RelaySendItem,
UNDELIVERABLE_DATAGRAM_TIMEOUT,
};
use crate::{dns::DnsResolver, test_utils};
#[allow(clippy::too_many_arguments)]
fn start_active_relay_actor(
secret_key: SecretKey,
stop_token: CancellationToken,
url: RelayUrl,
prio_inbox_rx: mpsc::Receiver<ActiveRelayPrioMessage>,
inbox_rx: mpsc::Receiver<ActiveRelayMessage>,
relay_datagrams_send: mpsc::Receiver<RelaySendItem>,
relay_datagrams_recv: mpsc::Sender<RelayRecvDatagram>,
span: tracing::Span,
) -> AbortOnDropHandle<()> {
let opts = ActiveRelayActorOptions {
url,
prio_inbox_: prio_inbox_rx,
inbox: inbox_rx,
relay_datagrams_send,
relay_datagrams_recv,
connection_opts: RelayConnectionOptions {
secret_key,
dns_resolver: DnsResolver::new(),
proxy_url: None,
prefer_ipv6: Arc::new(AtomicBool::new(true)),
insecure_skip_cert_verify: true,
},
stop_token,
metrics: Default::default(),
};
let task = tokio::spawn(ActiveRelayActor::new(opts).run().instrument(span));
AbortOnDropHandle::new(task)
}
fn start_echo_node(relay_url: RelayUrl) -> (NodeId, AbortOnDropHandle<()>) {
let secret_key = SecretKey::from_bytes(&[8u8; 32]);
let (recv_datagram_tx, mut recv_datagram_rx) = mpsc::channel(16);
let (send_datagram_tx, send_datagram_rx) = mpsc::channel(16);
let (prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8);
let (inbox_tx, inbox_rx) = mpsc::channel(16);
let cancel_token = CancellationToken::new();
let actor_task = start_active_relay_actor(
secret_key.clone(),
cancel_token.clone(),
relay_url.clone(),
prio_inbox_rx,
inbox_rx,
send_datagram_rx,
recv_datagram_tx,
info_span!("echo-node"),
);
let echo_task = tokio::spawn({
let relay_url = relay_url.clone();
async move {
loop {
let datagram = recv_datagram_rx.recv().await;
if let Some(recv) = datagram {
let RelayRecvDatagram {
url: _,
src,
datagrams,
} = recv;
info!(from = src.fmt_short(), "Received datagram");
let send = RelaySendItem {
remote_node: src,
url: relay_url.clone(),
datagrams,
};
send_datagram_tx.send(send).await.ok();
}
}
}
.instrument(info_span!("echo-task"))
});
let echo_task = AbortOnDropHandle::new(echo_task);
let supervisor_task = tokio::spawn(async move {
let _guard = cancel_token.drop_guard();
let _prio_inbox_tx = prio_inbox_tx;
let _inbox_tx = inbox_tx;
tokio::select! {
biased;
_ = actor_task => (),
_ = echo_task => (),
};
});
let supervisor_task = AbortOnDropHandle::new(supervisor_task);
(secret_key.public(), supervisor_task)
}
async fn send_recv_echo(
item: RelaySendItem,
tx: &mpsc::Sender<RelaySendItem>,
rx: &mut mpsc::Receiver<RelayRecvDatagram>,
) -> Result<()> {
tokio::time::timeout(Duration::from_secs(10), async move {
loop {
let res = tokio::time::timeout(UNDELIVERABLE_DATAGRAM_TIMEOUT, async {
tx.send(item.clone()).await.context("send item")?;
let RelayRecvDatagram {
url: _,
src: _,
datagrams,
} = rx.recv().await.unwrap();
assert_eq!(datagrams, item.datagrams);
Ok::<_, Error>(())
})
.await;
if res.is_ok() {
break;
}
}
})
.await
.expect("overall timeout exceeded");
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_active_relay_reconnect() -> Result {
let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?;
let (peer_node, _echo_node_task) = start_echo_node(relay_url.clone());
let secret_key = SecretKey::from_bytes(&[1u8; 32]);
let (datagram_recv_tx, mut datagram_recv_rx) = mpsc::channel(16);
let (send_datagram_tx, send_datagram_rx) = mpsc::channel(16);
let (_prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8);
let (inbox_tx, inbox_rx) = mpsc::channel(16);
let cancel_token = CancellationToken::new();
let task = start_active_relay_actor(
secret_key,
cancel_token.clone(),
relay_url.clone(),
prio_inbox_rx,
inbox_rx,
send_datagram_rx,
datagram_recv_tx.clone(),
info_span!("actor-under-test"),
);
info!("first echo");
let hello_send_item = RelaySendItem {
remote_node: peer_node,
url: relay_url.clone(),
datagrams: Datagrams::from(b"hello"),
};
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&mut datagram_recv_rx,
)
.await?;
let (tx, rx) = oneshot::channel();
inbox_tx
.send(ActiveRelayMessage::GetLocalAddr(tx))
.await
.context("send get local addr msg")?;
let local_addr = rx
.await
.context("wait for local addr msg")?
.context("no local addr")?;
info!(?local_addr, "check connection with addr");
inbox_tx
.send(ActiveRelayMessage::CheckConnection(vec![local_addr.ip()]))
.await
.context("send check connection message")?;
let (tx, rx) = oneshot::channel();
inbox_tx
.send(ActiveRelayMessage::GetLocalAddr(tx))
.await
.context("send get local addr msg")?;
rx.await.context("recv send local addr msg")?;
info!("second echo");
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&mut datagram_recv_rx,
)
.await?;
info!("check connection");
inbox_tx
.send(ActiveRelayMessage::CheckConnection(Vec::new()))
.await
.context("send check connection msg")?;
tokio::time::sleep(Duration::from_millis(10)).await;
info!("third echo");
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&mut datagram_recv_rx,
)
.await?;
cancel_token.cancel();
task.await.context("wait for task to finish")?;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_active_relay_inactive() -> Result {
let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?;
let secret_key = SecretKey::from_bytes(&[1u8; 32]);
let (datagram_recv_tx, _datagram_recv_rx) = mpsc::channel(16);
let (_send_datagram_tx, send_datagram_rx) = mpsc::channel(16);
let (_prio_inbox_tx, prio_inbox_rx) = mpsc::channel(8);
let (inbox_tx, inbox_rx) = mpsc::channel(16);
let cancel_token = CancellationToken::new();
let mut task = start_active_relay_actor(
secret_key,
cancel_token.clone(),
relay_url,
prio_inbox_rx,
inbox_rx,
send_datagram_rx,
datagram_recv_tx,
info_span!("actor-under-test"),
);
tokio::time::timeout(Duration::from_millis(200), async {
loop {
let (tx, rx) = oneshot::channel();
inbox_tx.send(ActiveRelayMessage::PingServer(tx)).await.ok();
if tokio::time::timeout(Duration::from_millis(100), rx)
.await
.map(|resp| resp.is_ok())
.unwrap_or_default()
{
break;
}
}
})
.await
.context("timeout")?;
tokio::time::pause();
info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME / 2");
tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME / 2).await;
assert!(
tokio::time::timeout(Duration::from_millis(100), &mut task)
.await
.is_err(),
"actor task terminated"
);
info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME");
tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME).await;
assert!(
tokio::time::timeout(Duration::from_millis(100), task)
.await
.is_ok(),
"actor task still running"
);
cancel_token.cancel();
Ok(())
}
#[tokio::test]
async fn test_ping_tracker() {
tokio::time::pause();
let mut tracker = PingTracker::default();
let ping0 = tracker.new_ping();
let res = tokio::time::timeout(Duration::from_secs(1), tracker.timeout()).await;
assert!(res.is_err(), "no ping timeout has elapsed yet");
tracker.pong_received(ping0);
let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await;
assert!(res.is_err(), "ping completed before timeout");
let _ping1 = tracker.new_ping();
let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await;
assert!(res.is_ok(), "ping timeout should have happened");
let _ping2 = tracker.new_ping();
tokio::time::sleep(Duration::from_secs(10)).await;
let res = tokio::time::timeout(Duration::from_millis(1), tracker.timeout()).await;
assert!(res.is_ok(), "ping timeout happened in the past");
let res = tokio::time::timeout(Duration::from_secs(10), tracker.timeout()).await;
assert!(res.is_err(), "ping timeout should only happen once");
}
}