use alloc::collections::BTreeMap;
use core::{
fmt,
net::{IpAddr, SocketAddr},
};
use chrono::{DateTime, Utc};
use ts_capabilityversion::CapabilityVersion;
use ts_keys::{DiscoPublicKey, MachinePublicKey, NodePublicKey};
const LAST_SEEN_FORMAT: &str = "%F %T %Z";
pub type Id = i64;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct StableId(pub String);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct NodeLastSeen {
pub control: Option<DateTime<Utc>>,
pub estimated: DateTime<Utc>,
}
impl Default for NodeLastSeen {
fn default() -> Self {
Self {
control: None,
estimated: Utc::now(),
}
}
}
impl fmt::Display for NodeLastSeen {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"last seen: {} (by control: ",
self.estimated.format(LAST_SEEN_FORMAT)
)?;
match self.control {
None => write!(f, "unknown)"),
Some(dt) => write!(f, "{})", dt.format(LAST_SEEN_FORMAT)),
}
}
}
impl NodeLastSeen {
pub fn new(control: Option<DateTime<Utc>>) -> Self {
Self {
control,
..Default::default()
}
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)]
pub enum NodeStatus {
#[default]
Unknown,
Online,
Offline(NodeLastSeen),
}
impl fmt::Display for NodeStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NodeStatus::Unknown => write!(f, "unknown"),
NodeStatus::Online => write!(f, "online"),
NodeStatus::Offline(nls) => write!(f, "offline, {nls}"),
}
}
}
impl NodeStatus {
pub fn new(online: Option<bool>, last_seen: Option<DateTime<Utc>>) -> Self {
match (online, last_seen) {
(Some(true), None) => Self::Online,
(Some(false), None) => Self::Offline(NodeLastSeen::new(None)),
(Some(false), Some(dt)) | (None, Some(dt)) => {
Self::Offline(NodeLastSeen::new(Some(dt)))
}
(None, None) => Self::Unknown,
(online, last_seen) => {
tracing::warn!(
?online,
?last_seen,
"unexpected combination of online/last_seen states"
);
Self::Unknown
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Node {
pub id: Id,
pub stable_id: StableId,
pub hostname: String,
pub capability_version: CapabilityVersion,
pub status: NodeStatus,
pub tailnet: Option<String>,
pub node_capabilities: BTreeMap<String, Vec<String>>,
pub tags: Vec<String>,
pub tailnet_address: TailnetAddress,
pub node_key: NodePublicKey,
pub node_key_expiry: Option<DateTime<Utc>>,
pub machine_key: Option<MachinePublicKey>,
pub disco_key: Option<DiscoPublicKey>,
pub tailnet_lock_key_signature: Option<Vec<u8>>,
pub accepted_routes: Vec<ipnet::IpNet>,
pub underlay_addresses: Vec<SocketAddr>,
pub derp_region: Option<ts_derp::RegionId>,
}
impl Node {
#[tracing::instrument(skip_all, fields(id = self.stable_id.0, hostname = self.hostname))]
pub fn apply_update(&mut self, update: &NodeUpdate) {
assert_eq!(self.id, update.id, "node update ID != node ID");
if update.status != NodeStatus::Unknown {
tracing::debug!(old_status = %self.status, "peer {}", update.status);
self.status = update.status;
}
if let Some(cap) = update.cap {
tracing::debug!(old=?self.capability_version, new=?cap, "updating capability version");
self.capability_version = cap;
}
if let Some(cap_map) = update.cap_map.as_ref() {
tracing::debug!(old=?self.node_capabilities, new=?cap_map, "updating node capabilities");
self.node_capabilities = cap_map.clone();
}
if let Some(derp_region) = update.derp_region {
tracing::debug!(old=?self.derp_region, new=?derp_region, "updating derp home region");
self.derp_region = Some(derp_region);
}
if let Some(disco_key) = update.disco_key {
tracing::debug!(old=?self.disco_key, new=?disco_key, "updating disco key");
self.disco_key = Some(disco_key);
}
if let Some(node_key) = update.node_key {
tracing::debug!(old=?self.node_key, new=?node_key, "updating node key");
self.node_key = node_key;
}
if let Some(node_key_expiry) = update.node_key_expiry {
tracing::debug!(old=?self.node_key_expiry, new=?node_key_expiry, "updating node key expiry");
self.node_key_expiry = Some(node_key_expiry);
}
if let Some(tl_sig) = &update.tailnet_lock_key_signature {
tracing::debug!(old=?self.tailnet_lock_key_signature, new=?tl_sig, "updating tailnet lock key signature");
self.tailnet_lock_key_signature = Some(tl_sig.clone());
}
if let Some(underlay_addresses) = &update.underlay_addresses {
tracing::debug!(old=?self.underlay_addresses, new=?underlay_addresses, "updating underlay addresses");
self.underlay_addresses = underlay_addresses.clone();
}
}
pub fn fqdn(&self, trailing_dot: bool) -> String {
let dot = if trailing_dot { "." } else { "" };
match &self.tailnet {
Some(tailnet) => format!("{}.{tailnet}{dot}", self.hostname),
None => format!("{}{dot}", self.hostname),
}
}
pub fn fqdn_opt(&self, trailing_dot: bool) -> Option<String> {
let dot = if trailing_dot { "." } else { "" };
let tailnet = self.tailnet.as_deref()?;
Some(format!("{}.{tailnet}{dot}", self.hostname))
}
pub fn matches_name(&self, name: &str) -> bool {
let name = name.strip_suffix('.').unwrap_or(name);
let name = if let Some(tailnet) = &self.tailnet {
name.strip_suffix(tailnet.as_str())
.and_then(|name| name.strip_suffix('.'))
.unwrap_or(name)
} else {
name
};
name == self.hostname
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TailnetAddress {
pub ipv4: ipnet::Ipv4Net,
pub ipv6: ipnet::Ipv6Net,
}
impl TailnetAddress {
pub fn contains(&self, addr: IpAddr) -> bool {
match addr {
IpAddr::V4(a) => self.ipv4.addr() == a,
IpAddr::V6(a) => self.ipv6.addr() == a,
}
}
}
impl From<&ts_control_serde::Node<'_>> for Node {
fn from(value: &ts_control_serde::Node) -> Self {
let fqdn_without_trailing_dot = value.name.strip_suffix('.').unwrap_or(value.name);
let (hostname, tailnet) = match fqdn_without_trailing_dot.split_once('.') {
Some((hostname, tailnet)) => (hostname, Some(tailnet.to_owned())),
None => (fqdn_without_trailing_dot, None),
};
Self {
id: value.id,
stable_id: StableId(value.stable_id.0.to_string()),
hostname: hostname.to_owned(),
capability_version: value.cap,
status: NodeStatus::new(value.online, value.last_seen),
tailnet,
node_capabilities: value
.cap_map
.iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect(),
tags: value
.tags
.as_ref()
.map(|x| x.iter().map(|x| x.to_string()).collect())
.unwrap_or_default(),
tailnet_address: TailnetAddress {
ipv4: value.addresses.0,
ipv6: value.addresses.1,
},
node_key: value.key,
node_key_expiry: value.key_expiry,
machine_key: value.machine,
disco_key: value.disco_key,
tailnet_lock_key_signature: value.key_signature.as_ref().map(|s| Vec::<u8>::from(*s)),
accepted_routes: value
.allowed_ips
.clone()
.unwrap_or_else(|| vec![value.addresses.0.into(), value.addresses.1.into()]),
underlay_addresses: value.endpoints.clone(),
#[allow(deprecated)]
derp_region: value
.home_derp
.or(value.legacy_derp_string)
.or_else(|| value.host_info.net_info.as_ref()?.preferred_derp)
.map(|x| ts_derp::RegionId(x.into())),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct NodeUpdate {
pub id: Id,
pub status: NodeStatus,
pub derp_region: Option<ts_derp::RegionId>,
pub cap: Option<CapabilityVersion>,
pub cap_map: Option<BTreeMap<String, Vec<String>>>,
pub node_key: Option<NodePublicKey>,
pub node_key_expiry: Option<DateTime<Utc>>,
pub disco_key: Option<DiscoPublicKey>,
pub tailnet_lock_key_signature: Option<Vec<u8>>,
pub underlay_addresses: Option<Vec<SocketAddr>>,
}
impl From<&ts_control_serde::PeerChange<'_>> for NodeUpdate {
fn from(value: &ts_control_serde::PeerChange<'_>) -> Self {
let cap_map = value.cap_map.as_ref().map(|m| {
m.iter()
.map(|(name, values)| {
(
String::from(*name),
values
.0
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>(),
)
})
.collect()
});
Self {
id: value.node_id,
status: NodeStatus::new(value.online, value.last_seen),
derp_region: value.derp_region.map(|x| ts_derp::RegionId(x.into())),
cap: value.cap,
cap_map,
node_key: value.key,
node_key_expiry: value.key_expiry,
disco_key: value.disco_key,
tailnet_lock_key_signature: value.key_signature.map(|x| x.into()),
underlay_addresses: value.endpoints.clone(),
}
}
}