#[cfg(test)]
use std::net::SocketAddr;
use std::{
collections::{BTreeMap, BTreeSet},
future::Future,
net::IpAddr,
pin::{pin, Pin},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use anyhow::{anyhow, Context, Result};
use backon::{Backoff, BackoffBuilder, ExponentialBuilder};
use bytes::{Bytes, BytesMut};
use iroh_base::{NodeId, PublicKey, RelayUrl, SecretKey};
use iroh_relay::{
self as relay,
client::{Client, ReceivedMessage, SendMessage},
PingTracker, MAX_PACKET_SIZE,
};
use n0_future::{
task::JoinSet,
time::{self, Duration, Instant, MissedTickBehavior},
FuturesUnorderedBounded, SinkExt, StreamExt,
};
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, event, info_span, instrument, trace, warn, Instrument, Level};
use url::Url;
use super::RelayDatagramSendChannelReceiver;
#[cfg(not(wasm_browser))]
use crate::dns::DnsResolver;
use crate::{
magicsock::{MagicSock, Metrics as MagicsockMetrics, RelayContents, RelayDatagramRecvQueue},
util::MaybeFuture,
};
const RELAY_INACTIVE_CLEANUP_TIME: Duration = Duration::from_secs(60);
const MAX_PAYLOAD_SIZE: usize = MAX_PACKET_SIZE - PublicKey::LENGTH;
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: Arc<RelayDatagramRecvQueue>,
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: Arc<RelayDatagramRecvQueue>,
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,
protocol: iroh_relay::http::Protocol,
}
#[derive(Debug, thiserror::Error)]
enum RelayConnectionError {
#[error("Failed to connect to relay server: {0:#}")]
Connecting(#[source] anyhow::Error),
#[error("Failed to handshake with relay server: {0:#}")]
Handshake(#[source] anyhow::Error),
#[error("Lost connection to relay server: {0:#}")]
Established(#[source] anyhow::Error),
}
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,
protocol,
} = opts;
let mut builder = relay::client::ClientBuilder::new(
url,
secret_key,
#[cfg(not(wasm_browser))]
dns_resolver,
)
.protocol(protocol)
.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) -> Result<()> {
let mut backoff = Self::build_backoff();
while let Err(err) = self.run_once().await {
warn!("{err}");
match err {
RelayConnectionError::Connecting(_) | RelayConnectionError::Handshake(_) => {
let delay = backoff.next().context("Retries exceeded")?;
debug!("Retry in {delay:?}");
time::sleep(delay).await;
}
RelayConnectionError::Established(_) => {
backoff = Self::build_backoff();
}
}
}
debug!("exiting");
Ok(())
}
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(Ok(client)) => client,
Some(Err(err)) => return Err(RelayConnectionError::Connecting(err)),
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>> {
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>> {
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(err),
Err(_) => Err(anyhow!("Connecting timed out after {CONNECT_TIMEOUT:?}")),
}
}
}
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, mut client_sink) = client.split();
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(SendMessage::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(anyhow!("Ping timeout"));
}
_ = ping_interval.tick() => {
let data = state.ping_tracker.new_ping();
let fut = client_sink.send(SendMessage::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(SendMessage::Ping(data));
self.run_sending(fut, &mut state, &mut client_stream).await?;
}
Some(_) => break Err(anyhow!("Local IP no longer valid")),
None => break Err(anyhow!("No local addr, reconnecting")),
}
}
#[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(SendMessage::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 dgrams = std::mem::replace(
&mut send_datagrams_buf,
Vec::with_capacity(SEND_DATAGRAM_BATCH_SIZE),
);
let metrics = self.metrics.clone();
let packet_iter = dgrams.into_iter().flat_map(|datagrams| {
PacketizeIter::<_, MAX_PAYLOAD_SIZE>::new(
datagrams.remote_node,
datagrams.datagrams.clone(),
)
.map(|p| {
Ok(SendMessage::SendPacket(p.node_id, p.payload))
})
});
let mut packet_stream = n0_future::stream::iter(packet_iter).inspect(|m| {
if let Ok(SendMessage::SendPacket(_node_id, payload)) = m {
metrics.send_relay.inc_by(payload.len() as _);
}
});
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(anyhow!("Stream closed by server."));
};
match msg {
Ok(msg) => {
self.handle_relay_msg(msg, &mut state);
ping_interval.reset();
},
Err(err) => break Err(anyhow!("Client stream read 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: ReceivedMessage, state: &mut ConnectedRelayState) {
match msg {
ReceivedMessage::ReceivedPacket {
remote_node_id,
data,
} => {
trace!(len = %data.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);
}
for datagram in PacketSplitIter::new(self.url.clone(), remote_node_id, data) {
let Ok(datagram) = datagram else {
warn!("Invalid packet split");
break;
};
if let Err(err) = self.relay_datagrams_recv.try_send(datagram) {
warn!("Dropping received relay packet: {err:#}");
}
}
}
ReceivedMessage::NodeGone(node_id) => {
state.nodes_present.remove(&node_id);
}
ReceivedMessage::Ping(data) => state.pong_pending = Some(data),
ReceivedMessage::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;
}
ReceivedMessage::Health { problem } => {
let problem = problem.as_deref().unwrap_or("unknown");
warn!("Relay server reports problem: {problem}");
}
ReceivedMessage::KeepAlive | ReceivedMessage::ServerRestarting { .. } => {
trace!("Ignoring {msg:?}")
}
}
}
#[instrument(name = "tx", skip_all)]
async fn run_sending<T, E: Into<anyhow::Error>>(
&mut self,
sending_fut: impl Future<Output = Result<T, E>>,
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(anyhow!("Send timeout"));
}
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.into()),
}
}
_ = state.ping_tracker.timeout() => {
break Err(anyhow!("Ping timeout"));
}
msg = client_stream.next() => {
let Some(msg) = msg else {
break Err(anyhow!("Stream closed by server."));
};
match msg {
Ok(msg) => self.handle_relay_msg(msg, state),
Err(err) => break Err(anyhow!("Client stream read 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: anyhow::Error) -> RelayConnectionError {
if self.established {
RelayConnectionError::Established(error)
} else {
RelayConnectionError::Handshake(error)
}
}
}
pub(super) enum RelayActorMessage {
MaybeCloseRelaysOnRebind(Vec<IpAddr>),
SetHome { url: RelayUrl },
}
#[derive(Debug, Clone)]
pub(super) struct RelaySendItem {
pub(super) remote_node: NodeId,
pub(super) url: RelayUrl,
pub(super) datagrams: RelayContents,
}
pub(super) struct RelayActor {
msock: Arc<MagicSock>,
relay_datagram_recv_queue: Arc<RelayDatagramRecvQueue>,
active_relays: BTreeMap<RelayUrl, ActiveRelayHandle>,
active_relay_tasks: JoinSet<()>,
cancel_token: CancellationToken,
protocol: iroh_relay::http::Protocol,
}
impl RelayActor {
pub(super) fn new(
msock: Arc<MagicSock>,
relay_datagram_recv_queue: Arc<RelayDatagramRecvQueue>,
protocol: iroh_relay::http::Protocol,
) -> Self {
let cancel_token = CancellationToken::new();
Self {
msock,
relay_datagram_recv_queue,
active_relays: Default::default(),
active_relay_tasks: JoinSet::new(),
cancel_token,
protocol,
}
}
pub(super) fn cancel_token(&self) -> CancellationToken {
self.cancel_token.clone()
}
pub(super) async fn run(
mut self,
mut receiver: mpsc::Receiver<RelayActorMessage>,
mut datagram_send_channel: RelayDatagramSendChannelReceiver,
) {
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::SetHome { url } => {
self.set_home_relay(url).await;
}
RelayActorMessage::MaybeCloseRelaysOnRebind(ifs) => {
self.maybe_close_relays_on_rebind(&ifs).await;
}
}
}
async fn try_send_datagram(&mut self, item: RelaySendItem) -> Option<impl Future<Output = ()>> {
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 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.msock.my_relay().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.msock.secret_key.clone(),
#[cfg(not(wasm_browser))]
dns_resolver: self.msock.dns_resolver.clone(),
proxy_url: self.msock.proxy_url().cloned(),
prefer_ipv6: self.msock.ipv6_reported.clone(),
#[cfg(any(test, feature = "test-utils"))]
insecure_skip_cert_verify: self.msock.insecure_skip_relay_cert_verify,
protocol: self.protocol,
};
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.msock.metrics.magicsock.clone(),
};
let actor = ActiveRelayActor::new(opts);
self.active_relay_tasks.spawn(
async move {
if let Err(err) = actor.run().await {
warn!("actor error: {err:#}");
}
}
.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, okay_local_ips: &[IpAddr]) {
let send_futs = self.active_relays.values().map(|handle| async move {
handle
.inbox_addr
.send(ActiveRelayMessage::CheckConnection(okay_local_ips.to_vec()))
.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(ref url) = self.msock.my_relay() {
self.active_relay_handle(url.clone());
}
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> {
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, PartialEq, Eq)]
struct RelaySendPacket {
node_id: NodeId,
payload: Bytes,
}
#[derive(Debug)]
pub(super) struct RelayRecvDatagram {
pub(super) url: RelayUrl,
pub(super) src: NodeId,
pub(super) buf: Bytes,
}
struct PacketizeIter<I: Iterator, const N: usize> {
node_id: NodeId,
iter: std::iter::Peekable<I>,
buffer: BytesMut,
}
impl<I: Iterator, const N: usize> PacketizeIter<I, N> {
fn new(node_id: NodeId, iter: impl IntoIterator<IntoIter = I>) -> Self {
Self {
node_id,
iter: iter.into_iter().peekable(),
buffer: BytesMut::with_capacity(N),
}
}
}
impl<I: Iterator, const N: usize> Iterator for PacketizeIter<I, N>
where
I::Item: AsRef<[u8]>,
{
type Item = RelaySendPacket;
fn next(&mut self) -> Option<Self::Item> {
use bytes::BufMut;
while let Some(next_bytes) = self.iter.peek() {
let next_bytes = next_bytes.as_ref();
assert!(next_bytes.len() + 2 <= N);
let next_length: u16 = next_bytes.len().try_into().expect("items < 64k size");
if self.buffer.len() + next_bytes.len() + 2 > N {
break;
}
self.buffer.put_u16_le(next_length);
self.buffer.put_slice(next_bytes);
self.iter.next();
}
if !self.buffer.is_empty() {
Some(RelaySendPacket {
node_id: self.node_id,
payload: self.buffer.split().freeze(),
})
} else {
None
}
}
}
#[derive(Debug)]
struct PacketSplitIter {
url: RelayUrl,
src: NodeId,
bytes: Bytes,
}
impl PacketSplitIter {
fn new(url: RelayUrl, src: NodeId, bytes: Bytes) -> Self {
Self { url, src, bytes }
}
fn fail(&mut self) -> Option<std::io::Result<RelayRecvDatagram>> {
self.bytes.clear();
Some(Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"",
)))
}
}
impl Iterator for PacketSplitIter {
type Item = std::io::Result<RelayRecvDatagram>;
fn next(&mut self) -> Option<Self::Item> {
use bytes::Buf;
if self.bytes.has_remaining() {
if self.bytes.remaining() < 2 {
return self.fail();
}
let len = self.bytes.get_u16_le() as usize;
if self.bytes.remaining() < len {
return self.fail();
}
let buf = self.bytes.split_to(len);
Some(Ok(RelayRecvDatagram {
url: self.url.clone(),
src: self.src,
buf,
}))
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use anyhow::Context;
use iroh_base::SecretKey;
use n0_future::future;
use smallvec::smallvec;
use testresult::TestResult;
use tokio_util::task::AbortOnDropHandle;
use tracing::info;
use tracing_test::traced_test;
use super::*;
use crate::{dns::DnsResolver, test_utils};
#[test]
fn test_packetize_iter() {
let node_id = SecretKey::generate(rand::thread_rng()).public();
let empty_vec: Vec<Bytes> = Vec::new();
let mut iter = PacketizeIter::<_, MAX_PACKET_SIZE>::new(node_id, empty_vec);
assert_eq!(None, iter.next());
let single_vec = vec!["Hello"];
let iter = PacketizeIter::<_, MAX_PACKET_SIZE>::new(node_id, single_vec);
let result = iter.collect::<Vec<_>>();
assert_eq!(1, result.len());
assert_eq!(
&[5, 0, b'H', b'e', b'l', b'l', b'o'],
&result[0].payload[..]
);
let spacer = vec![0u8; MAX_PACKET_SIZE - 10];
let multiple_vec = vec![&b"Hello"[..], &spacer, &b"World"[..]];
let iter = PacketizeIter::<_, MAX_PACKET_SIZE>::new(node_id, multiple_vec);
let result = iter.collect::<Vec<_>>();
assert_eq!(2, result.len());
assert_eq!(
&[5, 0, b'H', b'e', b'l', b'l', b'o'],
&result[0].payload[..7]
);
assert_eq!(
&[5, 0, b'W', b'o', b'r', b'l', b'd'],
&result[1].payload[..]
);
}
#[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: Arc<RelayDatagramRecvQueue>,
span: tracing::Span,
) -> AbortOnDropHandle<anyhow::Result<()>> {
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,
protocol: iroh_relay::http::Protocol::default(),
},
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_queue = Arc::new(RelayDatagramRecvQueue::new());
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_queue.clone(),
info_span!("echo-node"),
);
let echo_task = tokio::spawn({
let relay_url = relay_url.clone();
async move {
loop {
let datagram = future::poll_fn(|cx| recv_datagram_queue.poll_recv(cx)).await;
if let Ok(recv) = datagram {
let RelayRecvDatagram { url: _, src, buf } = recv;
info!(from = src.fmt_short(), "Received datagram");
let send = RelaySendItem {
remote_node: src,
url: relay_url.clone(),
datagrams: smallvec![buf],
};
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: &Arc<RelayDatagramRecvQueue>,
) -> Result<()> {
assert!(item.datagrams.len() == 1);
tokio::time::timeout(Duration::from_secs(10), async move {
loop {
let res = tokio::time::timeout(UNDELIVERABLE_DATAGRAM_TIMEOUT, async {
tx.send(item.clone()).await?;
let RelayRecvDatagram {
url: _,
src: _,
buf,
} = future::poll_fn(|cx| rx.poll_recv(cx)).await?;
assert_eq!(buf.as_ref(), item.datagrams[0]);
Ok::<_, anyhow::Error>(())
})
.await;
if res.is_ok() {
break;
}
}
})
.await
.expect("overall timeout exceeded");
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_active_relay_reconnect() -> TestResult {
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_queue = Arc::new(RelayDatagramRecvQueue::new());
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_queue.clone(),
info_span!("actor-under-test"),
);
info!("first echo");
let hello_send_item = RelaySendItem {
remote_node: peer_node,
url: relay_url.clone(),
datagrams: smallvec![Bytes::from_static(b"hello")],
};
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&datagram_recv_queue,
)
.await?;
let (tx, rx) = oneshot::channel();
inbox_tx.send(ActiveRelayMessage::GetLocalAddr(tx)).await?;
let local_addr = rx.await?.context("no local addr")?;
info!(?local_addr, "check connection with addr");
inbox_tx
.send(ActiveRelayMessage::CheckConnection(vec![local_addr.ip()]))
.await?;
let (tx, rx) = oneshot::channel();
inbox_tx.send(ActiveRelayMessage::GetLocalAddr(tx)).await?;
rx.await?;
info!("second echo");
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&datagram_recv_queue,
)
.await?;
info!("check connection");
inbox_tx
.send(ActiveRelayMessage::CheckConnection(Vec::new()))
.await?;
tokio::time::sleep(Duration::from_millis(10)).await;
info!("third echo");
send_recv_echo(
hello_send_item.clone(),
&send_datagram_tx,
&datagram_recv_queue,
)
.await?;
cancel_token.cancel();
task.await??;
Ok(())
}
#[tokio::test]
#[traced_test]
async fn test_active_relay_inactive() -> TestResult {
let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?;
let secret_key = SecretKey::from_bytes(&[1u8; 32]);
let datagram_recv_queue = Arc::new(RelayDatagramRecvQueue::new());
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_queue.clone(),
info_span!("actor-under-test"),
);
tokio::time::timeout(Duration::from_secs(5), async {
loop {
let (tx, rx) = oneshot::channel();
inbox_tx.send(ActiveRelayMessage::PingServer(tx)).await.ok();
if tokio::time::timeout(Duration::from_millis(200), rx)
.await
.map(|resp| resp.is_ok())
.unwrap_or_default()
{
break;
}
}
})
.await?;
info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME / 2");
tokio::time::pause();
tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME / 2).await;
tokio::time::resume();
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::pause();
tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME).await;
tokio::time::resume();
assert!(
tokio::time::timeout(Duration::from_secs(1), 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");
}
}