pub mod classify;
pub mod config;
#[cfg(feature = "port-mapping")]
pub mod portmap;
pub mod reflex;
pub mod rendezvous;
pub use config::TraversalConfig;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
#[cfg(feature = "port-mapping")]
use std::sync::Arc;
use arc_swap::ArcSwapOption;
#[derive(Debug, Default)]
pub struct TraversalStats {
punches_attempted: AtomicU64,
punches_succeeded: AtomicU64,
relay_fallbacks: AtomicU64,
port_mapping_active: AtomicBool,
port_mapping_external: ArcSwapOption<std::net::SocketAddr>,
port_mapping_renewals: AtomicU64,
upgrades_attempted: AtomicU64,
upgrades_succeeded: AtomicU64,
upgrades_deferred_busy: AtomicU64,
punch_timeouts: AtomicU64,
punch_rejections: AtomicU64,
rendezvous_no_relay: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TraversalStatsSnapshot {
pub punches_attempted: u64,
pub punches_succeeded: u64,
pub relay_fallbacks: u64,
pub port_mapping_active: bool,
pub port_mapping_external: Option<std::net::SocketAddr>,
pub port_mapping_renewals: u64,
pub upgrades_attempted: u64,
pub upgrades_succeeded: u64,
pub upgrades_deferred_busy: u64,
pub punches_failed: u64,
pub punch_timeouts: u64,
pub punch_rejections: u64,
pub rendezvous_no_relay: u64,
}
impl TraversalStats {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> TraversalStatsSnapshot {
let punches_attempted = self.punches_attempted.load(Ordering::Relaxed);
let punches_succeeded = self.punches_succeeded.load(Ordering::Relaxed);
TraversalStatsSnapshot {
punches_attempted,
punches_succeeded,
relay_fallbacks: self.relay_fallbacks.load(Ordering::Relaxed),
port_mapping_active: self.port_mapping_active.load(Ordering::Relaxed),
port_mapping_external: self.port_mapping_external.load_full().map(|arc| *arc),
port_mapping_renewals: self.port_mapping_renewals.load(Ordering::Relaxed),
upgrades_attempted: self.upgrades_attempted.load(Ordering::Relaxed),
upgrades_succeeded: self.upgrades_succeeded.load(Ordering::Relaxed),
upgrades_deferred_busy: self.upgrades_deferred_busy.load(Ordering::Relaxed),
punches_failed: punches_attempted.saturating_sub(punches_succeeded),
punch_timeouts: self.punch_timeouts.load(Ordering::Relaxed),
punch_rejections: self.punch_rejections.load(Ordering::Relaxed),
rendezvous_no_relay: self.rendezvous_no_relay.load(Ordering::Relaxed),
}
}
pub(crate) fn record_punch_attempt(&self) {
self.punches_attempted.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_punch_success(&self) {
self.punches_succeeded.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn record_relay_fallback(&self) {
self.relay_fallbacks.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_upgrade_attempt(&self) {
self.upgrades_attempted.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_upgrade_success(&self) {
self.upgrades_succeeded.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_upgrade_deferred_busy(&self) {
self.upgrades_deferred_busy.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_punch_timeout(&self) {
self.punch_timeouts.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_punch_rejection(&self) {
self.punch_rejections.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "nat-traversal")]
pub(crate) fn record_rendezvous_no_relay(&self) {
self.rendezvous_no_relay.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "port-mapping")]
pub(crate) fn record_port_mapping_install(&self, external: std::net::SocketAddr) {
self.port_mapping_external.store(Some(Arc::new(external)));
self.port_mapping_renewals.store(0, Ordering::Release);
self.port_mapping_active.store(true, Ordering::Release);
}
#[cfg(feature = "port-mapping")]
pub(crate) fn record_port_mapping_renewal(&self) {
self.port_mapping_renewals.fetch_add(1, Ordering::Relaxed);
}
#[cfg(feature = "port-mapping")]
pub(crate) fn replace_port_mapping_external(&self, external: std::net::SocketAddr) {
self.port_mapping_external.store(Some(Arc::new(external)));
}
#[cfg(feature = "port-mapping")]
pub(crate) fn record_port_mapping_revoke(&self) {
self.port_mapping_active.store(false, Ordering::Release);
self.port_mapping_external.store(None);
}
}
#[derive(Debug, thiserror::Error)]
pub enum TraversalError {
#[error("reflex-timeout")]
ReflexTimeout,
#[error("peer-not-reachable")]
PeerNotReachable,
#[error("transport: {0}")]
Transport(String),
#[error("rendezvous-no-relay")]
RendezvousNoRelay,
#[error("rendezvous-rejected: {0}")]
RendezvousRejected(String),
#[error("punch-failed")]
PunchFailed,
#[error("port-map-unavailable")]
PortMapUnavailable,
#[error("unsupported")]
Unsupported,
}
impl TraversalError {
pub fn kind(&self) -> &'static str {
match self {
Self::ReflexTimeout => "reflex-timeout",
Self::PeerNotReachable => "peer-not-reachable",
Self::Transport(_) => "transport",
Self::RendezvousNoRelay => "rendezvous-no-relay",
Self::RendezvousRejected(_) => "rendezvous-rejected",
Self::PunchFailed => "punch-failed",
Self::PortMapUnavailable => "port-map-unavailable",
Self::Unsupported => "unsupported",
}
}
}
pub const SUBPROTOCOL_REFLEX: u16 = 0x0D00;
pub const SUBPROTOCOL_RENDEZVOUS: u16 = 0x0D01;
#[cfg(test)]
mod error_kind_tests {
use super::*;
#[test]
fn rendezvous_error_kinds_are_stable() {
assert_eq!(
TraversalError::RendezvousRejected("rate-limited".into()).kind(),
"rendezvous-rejected",
);
assert_eq!(
TraversalError::RendezvousNoRelay.kind(),
"rendezvous-no-relay"
);
}
#[test]
fn rendezvous_rejected_display_carries_reason() {
let e = TraversalError::RendezvousRejected("reflex-mismatch".into());
assert_eq!(e.to_string(), "rendezvous-rejected: reflex-mismatch");
}
}
#[cfg(all(test, feature = "nat-traversal"))]
mod stats_snapshot_tests {
use super::*;
#[test]
fn punches_failed_derives_and_reason_counters_are_independent() {
let stats = TraversalStats::new();
let zero = stats.snapshot();
assert_eq!(zero.punches_failed, 0);
assert_eq!(zero.punch_timeouts, 0);
assert_eq!(zero.punch_rejections, 0);
assert_eq!(zero.rendezvous_no_relay, 0);
stats.record_punch_attempt();
stats.record_punch_attempt();
stats.record_punch_attempt();
stats.record_punch_success();
stats.record_punch_timeout();
stats.record_punch_rejection();
stats.record_rendezvous_no_relay();
let snap = stats.snapshot();
assert_eq!(snap.punches_attempted, 3);
assert_eq!(snap.punches_succeeded, 1);
assert_eq!(snap.punches_failed, 2, "derived: attempted - succeeded");
assert_eq!(snap.punch_timeouts, 1);
assert_eq!(snap.punch_rejections, 1);
assert_eq!(snap.rendezvous_no_relay, 1);
}
#[test]
fn punches_failed_saturates_instead_of_underflowing() {
let stats = TraversalStats::new();
stats.record_punch_success();
assert_eq!(stats.snapshot().punches_failed, 0, "saturating_sub");
}
#[test]
fn punches_failed_can_decrease_across_snapshots() {
let stats = TraversalStats::new();
stats.record_punch_attempt();
let mid = stats.snapshot();
assert_eq!(
mid.punches_failed, 1,
"an in-flight attempt reads as failed until it resolves",
);
stats.record_punch_success();
let after = stats.snapshot();
assert!(
after.punches_failed < mid.punches_failed,
"the same punch resolving must DECREASE the derived field \
({} -> {})",
mid.punches_failed,
after.punches_failed,
);
assert_eq!(after.punches_failed, 0);
}
}