use std::collections::HashSet;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::sync::broadcast::error::RecvError;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, trace, warn};
use crate::dht_network_manager::{DhtNetworkEvent, DhtNetworkManager};
use crate::reachability::canary::{
RelayCanaryPolicy, RelayCanaryVerdict, verify_relay_with_canaries,
};
use crate::reachability::session::{RelayAcquisitionOutcome, run_relay_acquisition};
use crate::self_address::build_self_address_set;
use crate::transport_handle::TransportHandle;
use crate::{PeerId, TransportAddressRecord};
use saorsa_transport::nat_traversal_api::PreparedRelay;
const HEALTH_POLL_INTERVAL: Duration = Duration::from_secs(5);
const RELAY_REVALIDATION_INTERVAL: Duration = Duration::from_secs(2 * 60 * 60);
const RELAY_REVALIDATION_JITTER_MAX: Duration = RELAY_REVALIDATION_INTERVAL;
const BACKOFF_INITIAL: Duration = Duration::from_secs(30);
const BACKOFF_MAX: Duration = Duration::from_secs(300);
const BACKOFF_FACTOR: u32 = 2;
pub(crate) fn spawn_acquisition_driver(
dht: Arc<DhtNetworkManager>,
transport: Arc<TransportHandle>,
relayer_peer_id: Arc<RwLock<Option<PeerId>>>,
relay_address: Arc<RwLock<Option<SocketAddr>>>,
shutdown: CancellationToken,
) {
tokio::spawn(async move {
let mut driver = AcquisitionDriver {
dht,
transport,
relayer_peer_id,
relay_address,
shutdown,
current_backoff: BACKOFF_INITIAL,
last_publication_attempt: None,
canary_rejected_relayers: HashSet::new(),
};
driver.run().await;
});
}
struct AcquisitionDriver {
dht: Arc<DhtNetworkManager>,
transport: Arc<TransportHandle>,
relayer_peer_id: Arc<RwLock<Option<PeerId>>>,
relay_address: Arc<RwLock<Option<SocketAddr>>>,
shutdown: CancellationToken,
current_backoff: Duration,
last_publication_attempt: Option<PublicationAttempt>,
canary_rejected_relayers: HashSet<PeerId>,
}
#[derive(Clone, Debug, PartialEq)]
struct PublicationAttempt {
records: Vec<TransportAddressRecord>,
target_peers: HashSet<PeerId>,
}
fn publication_targets(
previous: Option<&PublicationAttempt>,
records: &[TransportAddressRecord],
target_peers: &HashSet<PeerId>,
force: bool,
) -> HashSet<PeerId> {
let Some(previous) =
previous.filter(|previous| !force && previous.records.as_slice() == records)
else {
return target_peers.clone();
};
target_peers
.difference(&previous.target_peers)
.copied()
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CanaryRejectionEvent {
Verified,
Rejected(PeerId),
InsufficientWitnesses,
AcquisitionFailed,
}
fn apply_canary_rejection_event(
rejected_relayers: &mut HashSet<PeerId>,
event: CanaryRejectionEvent,
) {
match event {
CanaryRejectionEvent::Verified
| CanaryRejectionEvent::InsufficientWitnesses
| CanaryRejectionEvent::AcquisitionFailed => {
rejected_relayers.clear();
}
CanaryRejectionEvent::Rejected(relayer) => {
rejected_relayers.insert(relayer);
}
}
}
impl AcquisitionDriver {
async fn run(&mut self) {
info!("relay acquisition driver starting");
self.transport.clear_relay_address();
self.force_publish_typed_set(None).await;
loop {
if self.shutdown.is_cancelled() {
debug!("relay acquisition driver: shutdown, exiting");
return;
}
let outcome = run_relay_acquisition(
self.dht.as_ref(),
&self.transport,
&self.canary_rejected_relayers,
)
.await;
match outcome {
RelayAcquisitionOutcome::Acquired(relay) => {
let relay_addr = relay.allocation.public_addr();
match verify_relay_with_canaries(
&self.dht,
relay.relayer,
relay_addr,
RelayCanaryPolicy::Admission,
)
.await
{
RelayCanaryVerdict::Verified {
successes,
attempts,
} => {
if let Err(error) = self
.transport
.publish_proactive_relay_session(relay.allocation)
.await
{
warn!(
relayer = ?relay.relayer,
allocated = %relay_addr,
%error,
"driver: failed to commit canary-verified relay"
);
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::AcquisitionFailed,
);
self.clear_unpublished_relay_state(relay.allocation).await;
self.publish_typed_set(None).await;
if self.wait_backoff_or_event().await {
return;
}
self.advance_backoff();
continue;
}
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::Verified,
);
self.current_backoff = BACKOFF_INITIAL;
*self.relayer_peer_id.write().await = Some(relay.relayer);
*self.relay_address.write().await = Some(relay_addr);
self.transport.set_relay_address(relay_addr);
self.force_publish_typed_set(Some(relay_addr)).await;
info!(
relayer = ?relay.relayer,
allocated = %relay_addr,
successes,
attempts,
"driver: relay canary verified and published"
);
if self.hold_until_lost().await {
return;
}
self.lose_relay_and_republish(relay.allocation).await;
}
RelayCanaryVerdict::Rejected {
successes,
attempts,
} => {
warn!(
relayer = ?relay.relayer,
allocated = %relay_addr,
successes,
attempts,
"driver: relay failed canary quorum, entering backoff before trying next candidate"
);
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::Rejected(relay.relayer),
);
self.clear_unpublished_relay_state(relay.allocation).await;
self.publish_typed_set(None).await;
if self.wait_backoff_or_event().await {
return; }
self.advance_backoff();
}
RelayCanaryVerdict::Inconclusive {
successes,
failures,
unavailable,
} => {
warn!(
relayer = ?relay.relayer,
allocated = %relay_addr,
successes,
failures,
unavailable,
"driver: relay canary evidence inconclusive, entering backoff without publishing relay"
);
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::InsufficientWitnesses,
);
self.clear_unpublished_relay_state(relay.allocation).await;
self.publish_typed_set(None).await;
if self.wait_backoff_or_event().await {
return; }
self.advance_backoff();
}
}
}
RelayAcquisitionOutcome::Failed(reason) => {
warn!(
reason,
rejected_relayers = self.canary_rejected_relayers.len(),
"driver: acquisition failed, clearing canary exclusions and entering backoff"
);
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::AcquisitionFailed,
);
*self.relayer_peer_id.write().await = None;
*self.relay_address.write().await = None;
self.transport.clear_relay_address();
self.publish_typed_set(None).await;
if self.wait_backoff_or_event().await {
return; }
self.advance_backoff();
}
}
}
}
async fn publish_typed_set(&mut self, relay: Option<SocketAddr>) {
self.publish_typed_set_with_policy(relay, false).await;
}
async fn force_publish_typed_set(&mut self, relay: Option<SocketAddr>) {
self.publish_typed_set_with_policy(relay, true).await;
}
async fn clear_unpublished_relay_state(&mut self, allocation: PreparedRelay) {
let relay_public_addr = allocation.public_addr();
if let Err(error) = self
.transport
.abort_proactive_relay_session(allocation)
.await
{
warn!(
relay_addr = %relay_public_addr,
%error,
"driver: failed to abort unpublished relay"
);
}
*self.relayer_peer_id.write().await = None;
*self.relay_address.write().await = None;
self.transport.clear_relay_address();
}
async fn publish_typed_set_with_policy(&mut self, relay: Option<SocketAddr>, force: bool) {
let listen = self.transport.listen_addrs().await;
let observed = self.transport.non_relay_external_addresses();
debug!(
relay = ?relay,
observed = ?observed,
listen = ?listen,
"driver: preparing typed self address set"
);
let self_addresses = build_self_address_set(observed, listen, relay, |sa| {
self.transport.is_external_proven(sa)
});
let typed = self_addresses.into_typed_vec();
let records = match self.dht.complete_transport_address_records(&typed).await {
Ok(records) => records,
Err(error) => {
warn!(%error, "driver: address publication rejected");
return;
}
};
if records.is_empty() {
debug!("driver: publish skipped, no self addresses");
return;
}
let own_key = *self.dht.peer_id().to_bytes();
let all_peers = self
.dht
.find_closest_nodes_local(&own_key, self.dht.k_value())
.await;
let target_peers: HashSet<PeerId> = all_peers
.iter()
.map(|node| node.peer_id)
.filter(|peer| peer != self.dht.peer_id())
.collect();
let attempted_peers = publication_targets(
self.last_publication_attempt.as_ref(),
&records,
&target_peers,
force,
);
self.last_publication_attempt = Some(PublicationAttempt {
records: records.clone(),
target_peers,
});
if attempted_peers.is_empty() {
debug!(
peers = all_peers.len(),
typed_addresses = ?typed,
relay = ?relay,
"driver: publish skipped, complete self address set unchanged"
);
return;
}
let peers_to_publish: Vec<_> = all_peers
.into_iter()
.filter(|peer| attempted_peers.contains(&peer.peer_id))
.collect();
debug!(
peers = peers_to_publish.len(),
typed_addresses = ?typed,
relay = ?relay,
"driver: publishing typed self address set"
);
trace!(
peers = peers_to_publish.len(),
addrs = typed.len(),
relay = ?relay,
"driver: publishing typed address set to all routing table peers"
);
self.dht
.publish_address_records_to_peers(records, &peers_to_publish)
.await;
}
async fn hold_until_lost(&mut self) -> bool {
let mut events = self.dht.subscribe_events();
let mut health = tokio::time::interval(HEALTH_POLL_INTERVAL);
health.tick().await; let first_revalidation =
tokio::time::Instant::now() + relay_revalidation_initial_delay(self.dht.peer_id());
let revalidation = tokio::time::sleep_until(first_revalidation);
tokio::pin!(revalidation);
loop {
tokio::select! {
biased;
_ = self.shutdown.cancelled() => {
return true;
}
lost = self.transport.recv_relay_lost() => {
match lost {
Some(addr) => {
info!(
relay = %addr,
"driver: RelayLost event received, rebinding"
);
return false;
}
None => {
return true;
}
}
}
promoted = self.transport.recv_direct_address_promoted() => {
match promoted {
Some(addr) => {
let relay = *self.relay_address.read().await;
info!(
address = %addr,
relay = ?relay,
"driver: direct address promoted, republishing typed self address set"
);
self.publish_typed_set(relay).await;
}
None => {
return true;
}
}
}
updated = self.transport.recv_self_address_updated() => {
match updated {
Some(addr) => {
let relay = *self.relay_address.read().await;
debug!(
address = %addr,
relay = ?relay,
"driver: self address updated, refreshing typed self address set"
);
self.publish_typed_set(relay).await;
}
None => {
return true;
}
}
}
event = events.recv() => {
match event {
Ok(DhtNetworkEvent::KClosestPeersChanged {
added,
removed,
..
}) => {
let relay = *self.relay_address.read().await;
self.publish_typed_set(relay).await;
debug!(
added = added.len(),
removed = removed.len(),
"driver: K-closest changed; published current relay state only to new targets"
);
}
Ok(_) => continue,
Err(RecvError::Closed) => return true,
Err(RecvError::Lagged(skipped)) => {
let relay = *self.relay_address.read().await;
self.publish_typed_set(relay).await;
debug!(
skipped,
"driver: refreshed publication after lagging DHT events"
);
}
}
}
_ = health.tick() => {
if !self.transport.is_relay_healthy().await {
info!("driver: relay tunnel unhealthy, rebinding");
return false;
}
}
_ = &mut revalidation => {
let relayer = *self.relayer_peer_id.read().await;
let relay = *self.relay_address.read().await;
let (Some(relayer), Some(relay)) = (relayer, relay) else {
warn!("driver: relay state disappeared before revalidation");
return false;
};
let verdict = verify_relay_with_canaries(
&self.dht,
relayer,
relay,
RelayCanaryPolicy::Maintenance,
)
.await;
let retry_delay = match verdict {
RelayCanaryVerdict::Verified { successes, attempts } => {
info!(
relayer = %relayer.to_hex(),
relay = %relay,
successes,
attempts,
"driver: established relay passed periodic canary revalidation"
);
RELAY_REVALIDATION_INTERVAL
}
RelayCanaryVerdict::Rejected { successes, attempts } => {
warn!(
relayer = %relayer.to_hex(),
relay = %relay,
successes,
attempts,
"driver: established relay failed periodic canary revalidation; withdrawing"
);
apply_canary_rejection_event(
&mut self.canary_rejected_relayers,
CanaryRejectionEvent::Rejected(relayer),
);
return false;
}
RelayCanaryVerdict::Inconclusive {
successes,
failures,
unavailable,
} => {
info!(
relayer = %relayer.to_hex(),
relay = %relay,
successes,
failures,
unavailable,
"driver: established relay canary evidence inconclusive; retaining relay until the next scheduled check"
);
RELAY_REVALIDATION_INTERVAL
}
};
revalidation
.as_mut()
.reset(tokio::time::Instant::now() + retry_delay);
}
}
}
}
async fn lose_relay_and_republish(&mut self, allocation: PreparedRelay) {
let relay_public_addr = self.relay_address.write().await.take();
*self.relayer_peer_id.write().await = None;
self.transport.clear_relay_address();
let transport = Arc::clone(&self.transport);
let teardown = async move { transport.abort_proactive_relay_session(allocation).await };
let (teardown_result, ()) = tokio::join!(teardown, self.force_publish_typed_set(None));
if let Err(error) = teardown_result {
warn!(
relay_addr = ?relay_public_addr,
%error,
"driver: failed to tear down lost or evicted relay"
);
}
}
async fn wait_backoff_or_event(&mut self) -> bool {
let mut events = self.dht.subscribe_events();
let sleep = tokio::time::sleep(self.current_backoff);
tokio::pin!(sleep);
loop {
tokio::select! {
biased;
_ = self.shutdown.cancelled() => return true,
_ = &mut sleep => {
trace!(window = ?self.current_backoff, "driver: backoff window expired");
return false;
}
promoted = self.transport.recv_direct_address_promoted() => {
match promoted {
Some(addr) => {
info!(
address = %addr,
"driver: direct address promoted during relay backoff, republishing typed self address set"
);
self.publish_typed_set(None).await;
}
None => {
return true;
}
}
}
updated = self.transport.recv_self_address_updated() => {
match updated {
Some(addr) => {
debug!(
address = %addr,
"driver: self address updated during relay backoff, refreshing typed self address set"
);
self.publish_typed_set(None).await;
}
None => {
return true;
}
}
}
event = events.recv() => {
match event {
Ok(DhtNetworkEvent::KClosestPeersChanged { .. }) => {
debug!("driver: K-closest changed, retrying relay acquisition early");
return false;
}
Ok(_) => continue,
Err(RecvError::Closed) => return true,
Err(RecvError::Lagged(skipped)) => {
self.publish_typed_set(None).await;
debug!(
skipped,
"driver: refreshed publication after lagging DHT events during backoff"
);
}
}
}
}
}
}
fn advance_backoff(&mut self) {
let next = self.current_backoff.saturating_mul(BACKOFF_FACTOR);
self.current_backoff = next.min(BACKOFF_MAX);
}
}
fn relay_revalidation_initial_delay(peer_id: &PeerId) -> Duration {
let mut prefix = [0u8; std::mem::size_of::<u64>()];
prefix.copy_from_slice(&peer_id.to_bytes()[..std::mem::size_of::<u64>()]);
let jitter_bound = RELAY_REVALIDATION_JITTER_MAX.as_secs().saturating_add(1);
let jitter = u64::from_be_bytes(prefix) % jitter_bound;
RELAY_REVALIDATION_INTERVAL.saturating_add(Duration::from_secs(jitter))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{KnownReachability, MultiAddr, WebRtcCertificateHash, WebRtcDirectAddr};
const REJECTED_RELAYER_SEED: u8 = 7;
const SECOND_RELAYER_SEED: u8 = 8;
const PEER_ID_BYTES: usize = 32;
fn peer_id(seed: u8) -> PeerId {
PeerId::from_bytes([seed; PEER_ID_BYTES])
}
#[test]
fn publication_targets_only_include_new_peers_for_unchanged_records() {
let departed = peer_id(1);
let retained = peer_id(2);
let joined = peer_id(3);
let previous = PublicationAttempt {
records: Vec::new(),
target_peers: HashSet::from([departed, retained]),
};
let current = HashSet::from([retained, joined]);
assert_eq!(
publication_targets(Some(&previous), &[], ¤t, false),
HashSet::from([joined])
);
}
#[test]
fn first_publication_attempt_includes_current_peers() {
let peer = peer_id(1);
let current = HashSet::from([peer]);
assert_eq!(publication_targets(None, &[], ¤t, false), current);
}
#[test]
fn changed_or_forced_publication_targets_every_current_peer() {
let first = peer_id(1);
let second = peer_id(2);
let previous = PublicationAttempt {
records: Vec::new(),
target_peers: HashSet::from([first, second]),
};
let current = HashSet::from([first, second]);
let changed = [TransportAddressRecord::from_multiaddr(
&MultiAddr::from_ipv4(std::net::Ipv4Addr::new(203, 0, 113, 7), 9000),
KnownReachability::Direct,
)
.unwrap()
.unwrap()];
assert_eq!(
publication_targets(Some(&previous), &changed, ¤t, false),
current
);
assert_eq!(
publication_targets(Some(&previous), &[], ¤t, true),
current
);
}
fn browser_record(certificate: u8) -> TransportAddressRecord {
let address = MultiAddr::webrtc_direct(
WebRtcDirectAddr::new(
"203.0.113.7:42768".parse().unwrap(),
WebRtcCertificateHash::new([certificate; 32]),
)
.unwrap(),
)
.with_peer_id(peer_id(9));
TransportAddressRecord::from_multiaddr(&address, KnownReachability::Unverified)
.unwrap()
.unwrap()
}
#[test]
fn browser_certificate_changes_allow_new_attempts_without_retrying_failed_sends() {
let peers = HashSet::from([peer_id(1), peer_id(2)]);
let quic = TransportAddressRecord::from_multiaddr(
&"/ip4/203.0.113.7/udp/9000/quic".parse().unwrap(),
KnownReachability::Direct,
)
.unwrap()
.unwrap();
let mut previous = PublicationAttempt {
records: vec![quic.clone(), browser_record(1)],
target_peers: peers.clone(),
};
let rotated = vec![quic.clone(), browser_record(2)];
assert_eq!(
publication_targets(Some(&previous), &rotated, &peers, false),
peers
);
previous.records = rotated.clone();
assert!(publication_targets(Some(&previous), &rotated, &peers, false).is_empty());
assert_eq!(
publication_targets(Some(&previous), &[quic], &peers, false),
peers
);
}
#[tokio::test]
async fn publication_driver_skips_empty_replacements_even_when_forced() {
let node = crate::P2PNode::new(
crate::NodeConfig::builder()
.local(true)
.port(0)
.ipv6(false)
.build()
.unwrap(),
)
.await
.unwrap();
let mut driver = AcquisitionDriver {
dht: Arc::clone(node.dht_manager()),
transport: Arc::clone(node.transport()),
relayer_peer_id: Arc::new(RwLock::new(None)),
relay_address: Arc::new(RwLock::new(None)),
shutdown: CancellationToken::new(),
current_backoff: BACKOFF_INITIAL,
last_publication_attempt: None,
canary_rejected_relayers: HashSet::new(),
};
let address = browser_record(1)
.decode_known()
.unwrap()
.unwrap()
.with_peer_id(*node.peer_id());
node.dht_manager()
.set_supplemental_self_addresses(vec![address.clone()])
.await;
driver.publish_typed_set(None).await;
let published = driver.last_publication_attempt.as_ref().unwrap();
assert_eq!(published.records.len(), 1);
assert_eq!(
published.records[0].decode_known().unwrap(),
Some(address.clone())
);
node.dht_manager()
.set_supplemental_self_addresses(Vec::new())
.await;
for force in [false, true] {
driver.publish_typed_set_with_policy(None, force).await;
let published = driver.last_publication_attempt.as_ref().unwrap();
assert_eq!(published.records.len(), 1);
assert_eq!(
published.records[0].decode_known().unwrap(),
Some(address.clone())
);
}
}
#[test]
fn acquisition_failure_clears_canary_rejected_relayers() {
let mut rejected_relayers =
HashSet::from([peer_id(REJECTED_RELAYER_SEED), peer_id(SECOND_RELAYER_SEED)]);
apply_canary_rejection_event(
&mut rejected_relayers,
CanaryRejectionEvent::AcquisitionFailed,
);
assert!(rejected_relayers.is_empty());
}
#[test]
fn verified_relay_clears_canary_rejected_relayers() {
let mut rejected_relayers =
HashSet::from([peer_id(REJECTED_RELAYER_SEED), peer_id(SECOND_RELAYER_SEED)]);
apply_canary_rejection_event(&mut rejected_relayers, CanaryRejectionEvent::Verified);
assert!(rejected_relayers.is_empty());
}
#[test]
fn insufficient_witnesses_clear_canary_rejected_relayers() {
let mut rejected_relayers =
HashSet::from([peer_id(REJECTED_RELAYER_SEED), peer_id(SECOND_RELAYER_SEED)]);
apply_canary_rejection_event(
&mut rejected_relayers,
CanaryRejectionEvent::InsufficientWitnesses,
);
assert!(rejected_relayers.is_empty());
}
#[test]
fn canary_rejection_adds_relayer_to_exclusion_set() {
let relayer = peer_id(REJECTED_RELAYER_SEED);
let mut rejected_relayers = HashSet::new();
apply_canary_rejection_event(
&mut rejected_relayers,
CanaryRejectionEvent::Rejected(relayer),
);
assert!(rejected_relayers.contains(&relayer));
}
#[test]
fn relay_revalidation_delay_is_bounded_and_peer_stable() {
let peer = peer_id(REJECTED_RELAYER_SEED);
let delay = relay_revalidation_initial_delay(&peer);
assert_eq!(delay, relay_revalidation_initial_delay(&peer));
assert!(delay >= RELAY_REVALIDATION_INTERVAL);
assert!(delay <= RELAY_REVALIDATION_INTERVAL.saturating_add(RELAY_REVALIDATION_JITTER_MAX));
}
}