use crate::{
runtime::{Counter, MetricsHandle, Runtime},
transport::{
ssu2::metrics::{IPV4_PEER_TEST_RESULT, IPV6_PEER_TEST_RESULT},
FirewallStatus,
},
};
use core::net::SocketAddr;
const LOG_TARGET: &str = "emissary::ssu2::detector";
const CONFIRMATION_THRESHOLD: usize = 3usize;
pub struct Detector<R: Runtime> {
confirmation_count: usize,
external_address: Option<SocketAddr>,
firewall_status: FirewallStatus,
force_firewall: bool,
metrics_handle: R::MetricsHandle,
pending_status: Option<FirewallStatus>,
}
impl<R: Runtime> Detector<R> {
pub fn new(firewalled: bool, metrics_handle: R::MetricsHandle) -> Self {
Self {
confirmation_count: 0usize,
external_address: None,
firewall_status: if firewalled {
FirewallStatus::Firewalled
} else {
FirewallStatus::Unknown
},
force_firewall: firewalled,
metrics_handle,
pending_status: None,
}
}
pub fn status(&self) -> FirewallStatus {
self.firewall_status
}
pub fn add_external_address(&mut self, address: SocketAddr) -> Option<SocketAddr> {
if self.external_address.is_none() {
tracing::info!(
target: LOG_TARGET,
?address,
"discovered external address",
);
self.external_address = Some(address);
return self.external_address;
}
self.external_address = Some(address);
None
}
pub fn add_peer_test_result(
&mut self,
message4: bool,
message5: bool,
message7: Option<SocketAddr>,
) -> Option<FirewallStatus> {
if self.force_firewall || (!message4 && !message5 && message7.is_none()) {
return None;
}
let detected = self.determine_status(message4, message5, message7);
tracing::debug!(
target: LOG_TARGET,
?message4,
?message5,
?message7,
?detected,
current = ?self.firewall_status,
"peer test result",
);
if let Some(address) = self.external_address {
self.metrics_handle
.counter(if address.is_ipv4() {
IPV4_PEER_TEST_RESULT
} else {
IPV6_PEER_TEST_RESULT
})
.increment_with_label(1, "result", detected.into());
}
match self.pending_status {
None if self.firewall_status != detected => {
self.pending_status = Some(detected);
self.confirmation_count = 1;
return None;
}
None => return None,
Some(pending) if pending != detected => {
self.pending_status = Some(detected);
self.confirmation_count = 1;
return None;
}
Some(_) => {
self.confirmation_count += 1;
}
}
if self.confirmation_count >= CONFIRMATION_THRESHOLD {
let old_status = self.firewall_status;
self.firewall_status = detected;
self.pending_status = None;
self.confirmation_count = 0;
if old_status != detected {
tracing::debug!(
target: LOG_TARGET,
?old_status,
new_status = ?detected,
"firewall status changed",
);
return Some(detected);
}
}
None
}
fn determine_status(
&self,
message4: bool,
message5: bool,
message7: Option<SocketAddr>,
) -> FirewallStatus {
match (message4, message5, message7) {
(false, false, None) => FirewallStatus::Unknown,
(true, false, None) =>
if self.firewall_status == FirewallStatus::SymmetricNat {
FirewallStatus::SymmetricNat
} else {
FirewallStatus::Firewalled
},
(false, true, None) =>
if self.firewall_status == FirewallStatus::SymmetricNat {
FirewallStatus::Unknown
} else {
FirewallStatus::Ok
},
(true, true, None) =>
if self.firewall_status == FirewallStatus::SymmetricNat {
FirewallStatus::Unknown
} else {
FirewallStatus::Ok
},
(false, false, Some(_)) => FirewallStatus::Unknown,
(true, false, Some(reported_address)) => match self.external_address {
None => FirewallStatus::Firewalled,
Some(external_address) => match (
external_address.ip() == reported_address.ip(),
external_address.port() == reported_address.port(),
) {
(true, true) => FirewallStatus::Firewalled,
(true, false) => FirewallStatus::SymmetricNat,
(false, true) => FirewallStatus::Firewalled,
(false, false) => FirewallStatus::SymmetricNat,
},
},
(false, true, Some(_)) => FirewallStatus::Unknown,
(true, true, Some(_)) => FirewallStatus::Ok,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::mock::MockRuntime;
fn apply_result<const NUM: usize>(
detector: &mut Detector<MockRuntime>,
message4: bool,
message5: bool,
message7: Option<SocketAddr>,
) -> Option<FirewallStatus> {
(0..NUM).fold(None, |_, _x| {
detector.add_peer_test_result(message4, message5, message7)
})
}
#[test]
fn no_messages_received_returns_none() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
let result = detector.add_peer_test_result(false, false, None);
assert!(result.is_none());
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
}
#[test]
fn confirmation_threshold_required() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
assert_eq!(
detector.add_peer_test_result(true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
#[test]
fn mixed_results_reset_confirmation() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert!(detector.add_peer_test_result(false, true, None).is_none());
assert!(detector.add_peer_test_result(false, true, None).is_none());
assert_eq!(
detector.add_peer_test_result(false, true, None),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg4_only_firewalled() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(
apply_result::<3>(&mut detector, true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
#[test]
fn msg4_only_stays_symnat_if_currently_symnat() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
assert!(apply_result::<3>(&mut detector, true, false, None).is_none());
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
}
#[test]
fn msg5_only_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(
apply_result::<3>(&mut detector, false, true, None),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg5_only_unknown_if_currently_symnat() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
assert_eq!(
apply_result::<3>(&mut detector, false, true, None),
Some(FirewallStatus::Unknown)
);
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
}
#[test]
fn msg4_msg5_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(
apply_result::<3>(&mut detector, true, true, None),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg4_msg5_unknown_if_currently_symnat() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
assert_eq!(
apply_result::<3>(&mut detector, true, true, None),
Some(FirewallStatus::Unknown)
);
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
}
#[test]
fn msg4_msg7_ip_port_match_firewalled() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
let address = SocketAddr::new("1.2.3.4".parse().unwrap(), 8888);
detector.add_external_address(address);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(address)),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
#[test]
fn msg4_msg7_ip_match_port_mismatch_symnat() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
}
#[test]
fn msg4_msg7_ip_mismatch_port_match_firewalled() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("5.6.7.8".parse().unwrap(), 8888);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
#[test]
fn msg4_msg7_both_mismatch_symnat() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("5.6.7.8".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
}
#[test]
fn msg4_msg5_msg7_ip_port_match_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
let address = SocketAddr::new("1.2.3.4".parse().unwrap(), 8888);
detector.add_external_address(address);
assert_eq!(
apply_result::<3>(&mut detector, true, true, Some(address)),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg4_msg5_msg7_ip_match_port_mismatch_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, true, Some(msg7)),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg4_msg5_msg7_ip_mismatch_port_match_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("5.6.7.8".parse().unwrap(), 8888);
assert_eq!(
apply_result::<3>(&mut detector, true, true, Some(msg7)),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn msg4_msg5_msg7_both_mismatch_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("5.6.7.8".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, true, Some(msg7)),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn transition_firewalled_to_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(
apply_result::<3>(&mut detector, true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
assert_eq!(
apply_result::<3>(&mut detector, true, true, None),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn transition_ok_to_firewalled() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(
apply_result::<3>(&mut detector, true, true, None),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
assert_eq!(
apply_result::<3>(&mut detector, true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
#[test]
fn transition_symnat_to_ok() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
let msg7 = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(
apply_result::<3>(&mut detector, true, false, Some(msg7)),
Some(FirewallStatus::SymmetricNat)
);
assert_eq!(detector.firewall_status, FirewallStatus::SymmetricNat);
assert_eq!(
apply_result::<3>(&mut detector, true, true, Some(msg7)),
Some(FirewallStatus::Ok)
);
assert_eq!(detector.firewall_status, FirewallStatus::Ok);
}
#[test]
fn exactly_three_confirmations_needed() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.confirmation_count, 1);
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.confirmation_count, 2);
assert_eq!(
detector.add_peer_test_result(true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.confirmation_count, 0);
assert!(detector.pending_status.is_none());
}
#[test]
fn confirmation_resets_on_different_result() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.confirmation_count, 2);
assert_eq!(detector.pending_status, Some(FirewallStatus::Firewalled));
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
assert!(detector.add_peer_test_result(false, true, None).is_none());
assert_eq!(detector.confirmation_count, 1);
assert_eq!(detector.pending_status, Some(FirewallStatus::Ok));
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
}
#[test]
fn no_change_when_same_status_confirmed() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
assert_eq!(
apply_result::<3>(&mut detector, true, false, None),
Some(FirewallStatus::Firewalled)
);
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
for _ in 0..10 {
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert_eq!(detector.firewall_status, FirewallStatus::Firewalled);
}
}
#[test]
fn add_external_address_first_time() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
let address = SocketAddr::new("1.2.3.4".parse().unwrap(), 8889);
assert_eq!(detector.add_external_address(address), Some(address));
assert_eq!(detector.external_address, Some(address));
}
#[test]
fn alternating_results_never_confirm() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
for _ in 0..10 {
detector.add_peer_test_result(true, false, None);
detector.add_peer_test_result(false, true, None);
}
assert_eq!(detector.firewall_status, FirewallStatus::Unknown);
}
#[test]
fn same_status_doesnt_start_pending_process() {
let mut detector =
Detector::<MockRuntime>::new(false, MockRuntime::register_metrics(vec![], None));
detector.add_external_address(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888));
assert_eq!(
apply_result::<3>(&mut detector, true, false, None),
Some(FirewallStatus::Firewalled)
);
assert!(detector.pending_status.is_none());
assert_eq!(detector.confirmation_count, 0);
assert!(detector.add_peer_test_result(true, false, None).is_none());
assert!(detector.pending_status.is_none());
assert_eq!(detector.confirmation_count, 0);
assert!(detector
.add_peer_test_result(
true,
true,
Some(SocketAddr::new("1.2.3.4".parse().unwrap(), 8888))
)
.is_none());
assert_eq!(detector.pending_status, Some(FirewallStatus::Ok));
assert_eq!(detector.confirmation_count, 1);
}
}