use alloc::{collections::BTreeMap, vec::Vec};
use core::net::SocketAddr;
use chrono::{DateTime, TimeDelta, Utc};
use ts_derp::RegionId;
use ts_keys::NodePublicKey;
use crate::{Node, node::StableId};
pub const FLAG_EXPIRED_PEERS_EPOCH_UNIX: i64 = 1_673_373_066;
#[must_use]
pub fn flag_expired_peers_epoch() -> DateTime<Utc> {
DateTime::from_timestamp(FLAG_EXPIRED_PEERS_EPOCH_UNIX, 0)
.expect("FLAG_EXPIRED_PEERS_EPOCH_UNIX is a representable timestamp")
}
pub const MIN_CLOCK_DELTA_SECS: i64 = 60;
pub const EXPIRY_TIMER_SLACK_SECS: i64 = 10;
pub const CLOCK_SKEW_EXPIRY_FLOOR_SECS: i64 = 30;
pub const PEER_KEY_EXPIRED: &str = "peer's node key has expired";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlaggedPeer {
pub peer: Node,
pub first_transition: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PristinePeer {
node_key: NodePublicKey,
underlay_addresses: Vec<SocketAddr>,
derp_region: Option<RegionId>,
}
#[derive(Debug, Default, Clone)]
pub struct ExpiryManager {
previously_expired: BTreeMap<StableId, PristinePeer>,
clock_delta: TimeDelta,
}
impl ExpiryManager {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn on_control_time(
&mut self,
control_time: DateTime<Utc>,
local_now: DateTime<Utc>,
) -> TimeDelta {
let delta = control_time - local_now;
self.clock_delta = if delta.abs() > TimeDelta::seconds(MIN_CLOCK_DELTA_SECS) {
delta
} else {
TimeDelta::zero()
};
self.clock_delta
}
#[must_use]
pub fn clock_delta(&self) -> TimeDelta {
self.clock_delta
}
#[must_use]
pub fn control_now(&self, local_now: DateTime<Utc>) -> DateTime<Utc> {
local_now + self.clock_delta
}
fn usable_control_now(&self, local_now: DateTime<Utc>) -> Option<DateTime<Utc>> {
let control_now = self.control_now(local_now);
(control_now >= flag_expired_peers_epoch()).then_some(control_now)
}
pub fn flag_expired_peer(
&mut self,
peer: &Node,
local_now: DateTime<Utc>,
) -> Option<FlaggedPeer> {
let control_now = self.usable_control_now(local_now)?;
if peer
.node_key_expiry
.is_none_or(|expiry| expiry > control_now)
{
let pristine = self.previously_expired.remove(&peer.stable_id)?;
if peer.node_key != ts_keys::node_public_with_bad_old_prefix(pristine.node_key) {
return None;
}
let mut peer = peer.clone();
peer.node_key = pristine.node_key;
peer.expired = false;
if peer.underlay_addresses.is_empty() {
peer.underlay_addresses = pristine.underlay_addresses;
}
if peer.derp_region.is_none() {
peer.derp_region = pristine.derp_region;
}
return Some(FlaggedPeer {
peer,
first_transition: true,
});
}
if peer.expired {
return None;
}
let first_transition = self
.previously_expired
.insert(
peer.stable_id.clone(),
PristinePeer {
node_key: peer.node_key,
underlay_addresses: peer.underlay_addresses.clone(),
derp_region: peer.derp_region,
},
)
.is_none();
let mut peer = peer.clone();
peer.expired = true;
peer.underlay_addresses.clear();
peer.derp_region = None;
peer.node_key = ts_keys::node_public_with_bad_old_prefix(peer.node_key);
Some(FlaggedPeer {
peer,
first_transition,
})
}
#[must_use]
pub fn next_peer_expiry<'a>(
&self,
peers: impl IntoIterator<Item = &'a Node>,
self_node: Option<&Node>,
local_now: DateTime<Utc>,
) -> Option<DateTime<Utc>> {
let control_now = self.usable_control_now(local_now)?;
let mut next: Option<DateTime<Utc>> = None;
let mut consider = |node: &Node| {
let Some(expiry) = node.node_key_expiry else {
return; };
if node.expired || expiry < control_now {
return;
}
if next.is_none_or(|soonest| expiry < soonest) {
next = Some(expiry);
}
};
for peer in peers {
consider(peer);
}
if let Some(self_node) = self_node {
consider(self_node);
}
let next = next?;
if next < local_now {
return Some(local_now + TimeDelta::seconds(CLOCK_SKEW_EXPIRY_FLOOR_SECS));
}
Some(next)
}
}
#[cfg(test)]
mod tests {
use alloc::{string::ToString, vec, vec::Vec};
use chrono::{DateTime, TimeDelta, Utc};
use ts_keys::NodePublicKey;
use super::{
CLOCK_SKEW_EXPIRY_FLOOR_SECS, ExpiryManager, MIN_CLOCK_DELTA_SECS, flag_expired_peers_epoch,
};
use crate::{
Node,
node::{StableId, tests::test_node},
};
fn now() -> DateTime<Utc> {
DateTime::from_timestamp(1_800_000_000, 0).unwrap()
}
fn peer(stable_id: &str, key: u8, expiry: Option<DateTime<Utc>>) -> Node {
let mut node = test_node();
node.stable_id = StableId(stable_id.to_string());
node.node_key = NodePublicKey::from([key; 32]);
node.node_key_expiry = expiry;
node.underlay_addresses = vec!["192.0.2.7:41641".parse().unwrap()];
node.derp_region = Some(ts_derp::RegionId(core::num::NonZeroU32::new(2).unwrap()));
node
}
#[test]
fn flags_a_peer_whose_expiry_has_passed() {
let mut em = ExpiryManager::new();
let original_key = NodePublicKey::from([7u8; 32]);
let p = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
let flagged = em.flag_expired_peer(&p, now()).expect("peer is flagged");
assert!(
flagged.first_transition,
"the first pass is the log-worthy one"
);
let p = flagged.peer;
assert!(p.expired);
assert!(p.underlay_addresses.is_empty());
assert_eq!(p.derp_region, None);
assert_eq!(
p.node_key,
ts_keys::node_public_with_bad_old_prefix(original_key)
);
assert_eq!(p.stable_id, StableId("nOdE1".to_string()));
}
#[test]
fn does_not_reflag_an_already_expired_peer() {
let mut em = ExpiryManager::new();
let p = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
let flagged = em
.flag_expired_peer(&p, now())
.expect("peer is flagged")
.peer;
assert_eq!(em.flag_expired_peer(&flagged, now()), None);
let restated = em
.flag_expired_peer(&p, now())
.expect("a restated expired peer is rewritten again");
assert!(restated.peer.expired);
assert!(
!restated.first_transition,
"the log line appears once per episode, not once per netmap"
);
}
#[test]
fn control_sent_expired_is_left_alone() {
let mut em = ExpiryManager::new();
let mut p = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
p.expired = true;
assert_eq!(em.flag_expired_peer(&p, now()), None);
}
#[test]
fn a_peer_with_no_expiry_is_never_flagged() {
let mut em = ExpiryManager::new();
let p = peer("tAgGeD", 7, None);
assert_eq!(em.flag_expired_peer(&p, now()), None);
assert_eq!(
em.flag_expired_peer(&p, now() + TimeDelta::days(3650)),
None
);
assert!(!p.expired);
assert_eq!(em.next_peer_expiry([&p], None, now()), None);
}
#[test]
fn a_future_expiry_is_not_flagged() {
let mut em = ExpiryManager::new();
let p = peer("nOdE1", 7, Some(now() + TimeDelta::hours(1)));
assert_eq!(em.flag_expired_peer(&p, now()), None);
}
#[test]
fn extending_the_expiry_restores_the_pristine_key() {
let mut em = ExpiryManager::new();
let original_key = NodePublicKey::from([7u8; 32]);
let p = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
let mut p = em
.flag_expired_peer(&p, now())
.expect("peer is flagged")
.peer;
assert_ne!(p.node_key, original_key);
p.node_key_expiry = Some(now() + TimeDelta::days(30));
let p = em
.flag_expired_peer(&p, now())
.expect("peer is un-flagged")
.peer;
assert!(!p.expired);
assert_eq!(p.node_key, original_key);
}
#[test]
fn extending_the_expiry_restores_the_routing_fields() {
let mut em = ExpiryManager::new();
let live = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
let flagged = em
.flag_expired_peer(&live, now())
.expect("peer is flagged")
.peer;
assert!(flagged.underlay_addresses.is_empty());
assert_eq!(flagged.derp_region, None);
let mut extended = flagged;
extended.node_key_expiry = Some(now() + TimeDelta::days(30));
let recovered = em
.flag_expired_peer(&extended, now())
.expect("peer is un-flagged")
.peer;
assert!(!recovered.expired);
assert_eq!(
recovered.underlay_addresses, live.underlay_addresses,
"the peer's direct candidates come back with it"
);
assert_eq!(
recovered.derp_region, live.derp_region,
"and so does its home DERP route"
);
}
#[test]
fn a_restated_route_beats_the_memo() {
let mut em = ExpiryManager::new();
let live = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
let flagged = em
.flag_expired_peer(&live, now())
.expect("peer is flagged")
.peer;
let fresh_endpoint: core::net::SocketAddr = "198.51.100.4:41641".parse().unwrap();
let fresh_derp = ts_derp::RegionId(core::num::NonZeroU32::new(9).unwrap());
let mut extended = flagged;
extended.node_key_expiry = Some(now() + TimeDelta::days(30));
extended.underlay_addresses = vec![fresh_endpoint];
extended.derp_region = Some(fresh_derp);
let recovered = em
.flag_expired_peer(&extended, now())
.expect("peer is un-flagged")
.peer;
assert_eq!(recovered.underlay_addresses, vec![fresh_endpoint]);
assert_eq!(recovered.derp_region, Some(fresh_derp));
}
#[test]
fn key_expiry_exactly_at_control_now_is_expired() {
let mut em = ExpiryManager::new();
let p = peer("nOdE1", 7, Some(now()));
let flagged = em
.flag_expired_peer(&p, now())
.expect("expiry == control's now is expired, matching Go's `After`");
assert!(flagged.peer.expired);
let em = ExpiryManager::new();
assert_eq!(em.next_peer_expiry([&p], None, now()), Some(now()));
assert_eq!(
em.next_peer_expiry([&flagged.peer], None, now()),
None,
"the flagged peer is skipped, so no timer is armed for an event already handled"
);
}
#[test]
fn a_vast_backwards_clock_jump_flags_nothing() {
let mut em = ExpiryManager::new();
let control_time = DateTime::from_timestamp(1_000_000_000, 0).unwrap();
em.on_control_time(control_time, now());
assert!(em.control_now(now()) < flag_expired_peers_epoch());
let p = peer("nOdE1", 7, Some(now() - TimeDelta::hours(1)));
assert_eq!(em.flag_expired_peer(&p, now()), None);
let future = peer("nOdE2", 8, Some(now() + TimeDelta::hours(1)));
assert_eq!(em.next_peer_expiry([&future], None, now()), None);
}
#[test]
fn a_small_control_time_offset_is_ignored() {
let mut em = ExpiryManager::new();
let delta = em.on_control_time(now() + TimeDelta::seconds(MIN_CLOCK_DELTA_SECS), now());
assert_eq!(delta, TimeDelta::zero());
assert_eq!(em.control_now(now()), now());
}
#[test]
fn a_large_control_time_offset_shifts_every_comparison() {
let mut em = ExpiryManager::new();
let skew = TimeDelta::hours(2);
assert_eq!(em.on_control_time(now() + skew, now()), skew);
let p = peer("nOdE1", 7, Some(now() + TimeDelta::hours(1)));
let p = em
.flag_expired_peer(&p, now())
.expect("expired against control's clock");
assert!(p.peer.expired);
}
#[test]
fn a_corrected_control_time_clears_the_delta() {
let mut em = ExpiryManager::new();
em.on_control_time(now() + TimeDelta::hours(2), now());
assert_ne!(em.clock_delta(), TimeDelta::zero());
em.on_control_time(now(), now());
assert_eq!(em.clock_delta(), TimeDelta::zero());
}
#[test]
fn next_peer_expiry_picks_the_soonest_future_expiry() {
let em = ExpiryManager::new();
let soon = now() + TimeDelta::minutes(5);
let peers: Vec<Node> = vec![
peer("a", 1, Some(now() + TimeDelta::hours(4))),
peer("b", 2, Some(soon)),
peer("c", 3, None),
];
assert_eq!(em.next_peer_expiry(peers.iter(), None, now()), Some(soon));
}
#[test]
fn next_peer_expiry_skips_expired_and_past_peers() {
let em = ExpiryManager::new();
let mut flagged = peer("a", 1, Some(now() - TimeDelta::hours(1)));
flagged.expired = true;
let stale = peer("b", 2, Some(now() - TimeDelta::minutes(1)));
assert_eq!(
em.next_peer_expiry([&flagged, &stale], None, now()),
None,
"no future event: the answer must not be a time in the past"
);
}
#[test]
fn next_peer_expiry_folds_in_the_self_node() {
let em = ExpiryManager::new();
let self_expiry = now() + TimeDelta::minutes(2);
let self_node = peer("self", 9, Some(self_expiry));
let p = peer("a", 1, Some(now() + TimeDelta::hours(4)));
assert_eq!(
em.next_peer_expiry([&p], Some(&self_node), now()),
Some(self_expiry)
);
let expired_self = peer("self", 9, Some(now() - TimeDelta::minutes(2)));
assert_eq!(
em.next_peer_expiry([&p], Some(&expired_self), now()),
p.node_key_expiry
);
}
#[test]
fn next_peer_expiry_floors_a_clock_skewed_answer() {
let mut em = ExpiryManager::new();
em.on_control_time(now() - TimeDelta::hours(2), now());
let p = peer("a", 1, Some(now() - TimeDelta::minutes(90)));
assert_eq!(
em.next_peer_expiry([&p], None, now()),
Some(now() + TimeDelta::seconds(CLOCK_SKEW_EXPIRY_FLOOR_SECS))
);
}
}