diff --git a/ts_capabilityversion/src/lib.rs b/ts_capabilityversion/src/lib.rs index 454f296d..2ab2d69b 100644 --- a/ts_capabilityversion/src/lib.rs +++ b/ts_capabilityversion/src/lib.rs @@ -16,7 +16,7 @@ use core::fmt; /// /// Note: Prior to 2022-03-06, this value was known as the "`MapRequest` version", `mapVer`, or "map /// cap"; you'll still see that name used in comments throughout the Golang codebase. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct CapabilityVersion(u16); diff --git a/ts_control/Cargo.toml b/ts_control/Cargo.toml index a99edb54..48c27eeb 100644 --- a/ts_control/Cargo.toml +++ b/ts_control/Cargo.toml @@ -28,7 +28,7 @@ ts_transport_derp.workspace = true # Unconditionally required dependencies. bytes.workspace = true -chrono = { workspace = true, features = ["serde"] } +chrono = { workspace = true, features = ["clock", "serde"] } gethostname.workspace = true ipnet = { workspace = true, features = ["serde"] } lazy_static.workspace = true diff --git a/ts_control/src/lib.rs b/ts_control/src/lib.rs index 79b39cf6..4938be49 100644 --- a/ts_control/src/lib.rs +++ b/ts_control/src/lib.rs @@ -28,7 +28,9 @@ pub use config::{Config, DEFAULT_CONTROL_SERVER}; pub use control_dialer::{ControlDialer, TcpDialer, complete_connection}; pub use derp::{Map as DerpMap, Region as DerpRegion, convert_derp_map}; pub use dial_plan::{DialCandidate, DialMode, DialPlan}; -pub use node::{Id as NodeId, Node, StableId as StableNodeId, TailnetAddress}; +pub use node::{ + Id as NodeId, Node, NodeStatus, NodeUpdate, StableId as StableNodeId, TailnetAddress, +}; #[cfg(feature = "async_tokio")] pub use crate::tokio::{AsyncControlClient, FilterUpdate, PeerUpdate, StateUpdate}; diff --git a/ts_control/src/node.rs b/ts_control/src/node.rs index 3f67bf5a..0bbaf5dd 100644 --- a/ts_control/src/node.rs +++ b/ts_control/src/node.rs @@ -1,6 +1,11 @@ -use core::net::{IpAddr, SocketAddr}; +use alloc::collections::BTreeMap; +use core::{ + fmt, + net::{IpAddr, SocketAddr}, +}; use chrono::{DateTime, Utc}; +use ts_capabilityversion::CapabilityVersion; use ts_keys::{DiscoPublicKey, MachinePublicKey, NodePublicKey}; /// The unique id of a node. @@ -10,6 +15,100 @@ pub type Id = i64; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StableId(pub String); +/// Timestamp when an offline node was last connected to the control plane. Only applies to nodes +/// that are offline (disconnected from the control plane). +/// +/// Some timestamp values are only accurate to a resolution of ~10 minutes for privacy reasons, and +/// do not take into account differences between the control plane clock and local device clock. See +/// each variant for more information. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum NodeLastSeen { + /// The last time the node was connected to the control plane, as reported by the control plane + /// itself. + /// + /// The timestamp value may be rounded to the nearest 10-minute boundary before being reported + /// to this device by the control plane; in other words, many `Control` timestamps will be + /// aligned to 10-minute boundaries. This is a privacy-preserving measure implemented by the + /// control plane. Note that we have seen some non-rounded timestamps being reported from the + /// control plane, although it's unclear if this is a bug or by design. + /// + /// Note that the timestamp value was generated by the control plane. The control plane's + /// "clock" isn't synchronized with the local device's clock, so the timestamp value may be + /// much more than 5 minutes in the past/future when compared to the local device clock. + Control(DateTime), + /// The last time the node was connected to the control plane, as estimated by this device. + /// + /// For some types of node updates, we're only notified that the node is offline, not when it + /// was last seen by the control plane. When we receive one of these offline updates, we + /// estimate a last-seen timestamp using the local device clock. As the control plane "clock" + /// and the local device clock aren't synchronized, this estimate may differ much more than 5 + /// minutes in the past/future when compared to a fuzzed timestamp generated by the control + /// plane "clock". + Estimated(DateTime), +} + +impl fmt::Display for NodeLastSeen { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NodeLastSeen::Control(dt) => write!(f, "last seen (by control): {}", dt), + NodeLastSeen::Estimated(dt) => write!(f, "last seen (estimated): {}", dt), + } + } +} + +/// Whether a node is online (connected to the control plane) or offline (disconnected from the +/// control plane). +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub enum NodeStatus { + /// The node may be online or offline; the control plane hasn't informed us yet. + #[default] + Unknown, + /// The node is online (connected to the control plane). + Online, + /// The node is offline (disconnected from the control plane). + 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 { + /// Construct a new `NodeStatus` from a combination of online and last-seen timestamp. + /// + /// The netmap messages we get from the control plane have multiple ways to indicate a peer is + /// online and/or the timestamp the peer was last connected to the control plane. This method + /// wraps the sometimes-painful logic of creating a `NodeStatus` from the various combinations + /// of netmap field values. + pub fn new(online: Option, last_seen: Option>) -> Self { + match (online, last_seen) { + (Some(true), None) => Self::Online, + (Some(false), None) => { + // We intentionally don't fuzz the estimated timestamp value because the Go client + // code doesn't either. + // See: https://github.com/tailscale/tailscale/blob/ee0a03b140021541495b25bdb6642b589431758b/control/controlclient/map.go#L753-L764 + Self::Offline(NodeLastSeen::Estimated(Utc::now())) + } + (Some(false), Some(dt)) | (None, Some(dt)) => Self::Offline(NodeLastSeen::Control(dt)), + (None, None) => Self::Unknown, + (online, last_seen) => { + tracing::warn!( + ?online, + ?last_seen, + "unexpected combination of online/last_seen states" + ); + Self::Unknown + } + } + } +} + /// A node in a tailnet. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Node { @@ -17,13 +116,24 @@ pub struct Node { pub id: Id, /// The node's stable id. pub stable_id: StableId, - - /// This node's hostname. + /// The node's hostname. pub hostname: String, + /// The node's capability version. + pub capability_version: CapabilityVersion, + + /// Whether the node is connected to the control plane or not. If [`NodeStatus::Unknown`], the + /// control plane hasn't told us yet. + /// + /// This field does _not_ indicate whether the node is reachable or visible from this device. + pub status: NodeStatus, + /// The tailnet this node belongs to. pub tailnet: Option, + /// The node capabilities assigned to this node. + pub node_capabilities: BTreeMap>, + /// The tags assigned to this node. pub tags: Vec, @@ -39,6 +149,9 @@ pub struct Node { pub machine_key: Option, /// The node's [`DiscoPublicKey`], if known. pub disco_key: Option, + /// The signature of the node's public key with the Tailnet Lock signing key, if Tailnet Lock + /// is enabled and the signature is known. + pub tailnet_lock_key_signature: Option>, /// The routes this node accepts traffic for. pub accepted_routes: Vec, @@ -50,6 +163,67 @@ pub struct Node { } impl Node { + /// Apply the given partial update to this `Node`. Returns `true` if the update was successfully + /// applied; `false` otherwise. + #[tracing::instrument(skip_all, fields(id = self.stable_id.0, hostname = self.hostname))] + pub fn apply_update(&mut self, update: &NodeUpdate) -> bool { + if self.id != update.id { + tracing::warn!( + node_id = self.id, + update_id = update.id, + "ignoring node update with incorrect node ID" + ); + return false; + } + + 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(); + } + + true + } + /// The fully-qualified domain name of the node. /// /// This is a string of the form `$HOST.$TAILNET_DOMAIN.`. For tailnets controlled by @@ -124,10 +298,18 @@ impl From<&ts_control_serde::Node<'_>> for Node { 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() @@ -142,6 +324,7 @@ impl From<&ts_control_serde::Node<'_>> for Node { 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::::from(*s)), accepted_routes: value .allowed_ips @@ -159,3 +342,75 @@ impl From<&ts_control_serde::Node<'_>> for Node { } } } + +/// A partial update to a [`Node`] in a tailnet. +/// +/// Devices should reject any `NodeUpdate` that it doesn't have a corresponding [`Node`] for. This +/// type combines multiple different update/patch-style fields from netmap messages into a single +/// type, such as the +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] +pub struct NodeUpdate { + /// The node's id. + pub id: Id, + + /// Whether this node is online or offline (with last-seen timestamp), according to the control + /// plane. If [`NodeStatus::Unknown`], has not changed. + pub status: NodeStatus, + + /// The DERP region for this node. If `None`, has not changed. + pub derp_region: Option, + + /// The node's capability version. If `None`, has not changed. + pub cap: Option, + /// The node's capabilities (node caps, not peer caps). If `None`, has not changed. + pub cap_map: Option>>, + + /// The node's [`NodePublicKey`]. If `None`, has not changed. + pub node_key: Option, + /// The node key's expiration. If `None`, has not changed. + pub node_key_expiry: Option>, + + /// The node's [`DiscoPublicKey`]. If `None`, has not changed. + pub disco_key: Option, + + /// The node's key signature for Tailnet Lock. If `None`, has not changed. + pub tailnet_lock_key_signature: Option>, + + /// The underlay addresses this node is reachable on (`Endpoints` in Go). If `None`, has not + /// changed. + pub underlay_addresses: Option>, +} + +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::>(), + ) + }) + .collect() + }); + + Self { + id: value.node_id, + status: NodeStatus::new(value.online, value.last_seen), + derp_region: value + .derp_region + .map(|x| ts_transport_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(), + } + } +} diff --git a/ts_control/src/tokio/map_stream.rs b/ts_control/src/tokio/map_stream.rs index d83cbd78..d0cfd7a1 100644 --- a/ts_control/src/tokio/map_stream.rs +++ b/ts_control/src/tokio/map_stream.rs @@ -10,7 +10,7 @@ use ts_packetfilter as pf; use ts_packetfilter_state as pf_state; use url::Url; -use crate::{DialPlan, NodeId}; +use crate::{DialPlan, NodeId, NodeStatus}; #[derive(Debug, thiserror::Error, Clone, Copy, Eq, PartialEq)] pub enum MapStreamError { @@ -66,7 +66,9 @@ pub enum PeerUpdate { /// Delta update to the peer state. Delta { - /// Peers added to or changed in the state. + /// Peers with a few changed fields in the state. + patch: Vec, + /// Peers added to or completely changed in the state. upsert: Vec, /// Peer [`NodeId`]s removed from the state. remove: Vec, @@ -134,8 +136,53 @@ pub fn map_stream(reader: impl AsyncRead + Unpin) -> impl Stream = BTreeMap::new(); + for (id, seen) in map_response.peer_seen_change { + let status = if seen { + // Not online, and no timestamp provided by control, so we have to estimate. + NodeStatus::new(Some(false), None) + } else { + // We don't know whether the node is online or offline from this field. + NodeStatus::Unknown + }; + updates + .entry(id) + .and_modify(|u| u.status = status) + .or_insert(crate::NodeUpdate { + id, + status, + ..Default::default() + }); + } + + for (id, online) in map_response.online_change { + let status = NodeStatus::new(Some(online), None); + updates + .entry(id) + .and_modify(|u| u.status = status) + .or_insert(crate::NodeUpdate { + id, + status, + ..Default::default() + }); + } + + let mut patches = map_response + .peers_changed_patch + .unwrap_or_default() + .iter() + .map(|x| (x.node_id, crate::NodeUpdate::from(x))) + .collect(); + updates.append(&mut patches); + Some(PeerUpdate::Delta { + patch: updates.values().cloned().collect(), remove: map_response.peers_removed.unwrap_or_default(), upsert: map_response .peers_changed @@ -148,10 +195,6 @@ pub fn map_stream(reader: impl AsyncRead + Unpin) -> impl Stream { /// - Windows: "10.0.19044.1889" pub os_version: &'a str, - /// Indicates whether or not this Tailscale node is running inside a container. Detection is - /// best-effort only, and may not be accurate. + /// Indicates whether this Tailscale node is running inside a container. Detection is best- + /// effort only, and may not be accurate. pub container: Option, /// Represents the type of runtime environment that this Tailscale node is running in. #[serde(skip_serializing_if = "crate::util::is_default")] @@ -56,10 +56,10 @@ pub struct HostInfo<'a> { /// Disambiguates Tailscale nodes that run using `tsnet` (e.g. "k8s-operator", "golinks", etc). pub app: &'a str, - /// Indicates whether or not a desktop environment was detected. Used only for Linux devices. + /// Indicates whether a desktop environment was detected. Used only for Linux devices. pub desktop: Option, /// How this Tailscale node was packaged/delivered to the device (e.g. "choco", "appstore", - /// etc). Empty string if the packaging mechanism is unknown. + /// etc.) Empty string if the packaging mechanism is unknown. pub package: &'a str, /// Model of mobile phone for mobile devices (e.g. "Pixel 3a", "iPhone12,3"). pub device_model: &'a str, @@ -69,7 +69,7 @@ pub struct HostInfo<'a> { /// Hostname of this Tailscale node's host. pub hostname: Option<&'a str>, - /// Indicates whether or not this Tailscale node's host is blocking incoming connections. + /// Indicates whether this Tailscale node's host is blocking incoming connections. pub shields_up: bool, /// Indicates this Tailscale node exists in the netmap because it's owned by a shared-to user. pub sharee_node: bool, @@ -83,7 +83,7 @@ pub struct HostInfo<'a> { /// optimization, this is only sent if [`HostInfo::ingress_enabled`] is `false`, as /// [`HostInfo::ingress_enabled`] implies that this option is `true`. pub wire_ingress: bool, - /// Indicates whether or not this Tailscale node has any Tailscale Funnel endpoints enabled. + /// Indicates whether this Tailscale node has any Tailscale Funnel endpoints enabled. pub ingress_enabled: bool, /// Indicates that this Tailscale node has opted-in to remote updates triggered by the admin /// console. @@ -143,7 +143,8 @@ pub struct HostInfo<'a> { /// TPM device metadata, if available. pub tpm: Option>, - /// Reports whether the node state is stored encrypted on-disk. The actual mechanism is platform-specific: + /// Reports whether the node state is stored encrypted on-disk. The actual mechanism is + /// platform-specific: /// * Apple nodes use the Keychain /// * Linux and Windows nodes use the TPM /// * Android apps use `EncryptedSharedPreferences` diff --git a/ts_control_serde/src/lib.rs b/ts_control_serde/src/lib.rs index c45dd99f..21bddec9 100644 --- a/ts_control_serde/src/lib.rs +++ b/ts_control_serde/src/lib.rs @@ -36,7 +36,7 @@ pub use dns::{ }; pub use host_info::HostInfo; pub use net_info::{DerpLatencyMap, LinkType, NetInfo}; -pub use netmap::{Endpoint, EndpointType, MapRequest, MapResponse}; +pub use netmap::{Endpoint, EndpointType, MapRequest, MapResponse, PeerChange}; pub use node::{MarshaledSignature, Node, NodeId, StableNodeId}; pub use ping::{PingRequest, PingResponse, PingType}; pub use register::{RegisterAuth, RegisterRequest, RegisterResponse, SignatureType}; diff --git a/ts_control_serde/src/netmap.rs b/ts_control_serde/src/netmap.rs index e67fbf80..c2efa51f 100644 --- a/ts_control_serde/src/netmap.rs +++ b/ts_control_serde/src/netmap.rs @@ -313,7 +313,7 @@ pub struct MapResponse<'a> { /// These are applied after `peers*`, but in practice, the control server should only /// send these on their own, without the `peers*` fields also set. #[serde(borrow)] - pub peers_changed_patch: Vec>>, + pub peers_changed_patch: Option>>, /// How to update peers' [`last_seen`][crate::Node::last_seen] times. /// diff --git a/ts_control_serde/src/node.rs b/ts_control_serde/src/node.rs index 8a871678..51e0e0d7 100644 --- a/ts_control_serde/src/node.rs +++ b/ts_control_serde/src/node.rs @@ -70,7 +70,7 @@ pub struct Node<'a> { /// If populated, a signature of the Tailnet Key Authority (TKA) key authorizing the Tailscale /// node to join the Tailnet. #[serde(borrow)] - pub key_signature: MarshaledSignature<'a>, + pub key_signature: Option>, /// If populated, the public key of the Tailscale node's [`MachineKeyPair`][ts_keys::MachineKeyPair]. pub machine: Option, /// If populated, the public key of the Tailscale node's [`DiscoKeyPair`][ts_keys::DiscoKeyPair]. diff --git a/ts_nodecapability/src/lib.rs b/ts_nodecapability/src/lib.rs index ac60fd61..0790c779 100644 --- a/ts_nodecapability/src/lib.rs +++ b/ts_nodecapability/src/lib.rs @@ -3,7 +3,11 @@ extern crate alloc; -use alloc::{collections::BTreeMap, vec::Vec}; +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, + vec::Vec, +}; /// A map of node capabilities to their optional values. It is valid for a capability to have /// `None` as a value; such capabilities can be tested for by using the @@ -52,6 +56,12 @@ pub struct Values<'a>( pub Vec>, ); +impl From<&Values<'_>> for Vec { + fn from(value: &Values<'_>) -> Self { + value.0.iter().map(|v| v.to_string()).collect() + } +} + #[cfg(feature = "serde")] fn deserialize_nodecap<'a, 'de, D>( deserializer: D, diff --git a/ts_runtime/src/peer_tracker.rs b/ts_runtime/src/peer_tracker.rs index 8bc0b03d..43a19382 100644 --- a/ts_runtime/src/peer_tracker.rs +++ b/ts_runtime/src/peer_tracker.rs @@ -173,6 +173,8 @@ pub(crate) struct PeerState { #[allow(unused)] pub deletions: HashSet, #[allow(unused)] + pub patches: HashSet, + #[allow(unused)] pub upserts: HashSet, pub peers: Arc>, } @@ -191,6 +193,7 @@ impl Message> for PeerTracker { return; }; + let mut patches = HashSet::default(); let mut upserts = HashSet::default(); let mut deletions = HashSet::default(); @@ -212,9 +215,22 @@ impl Message> for PeerTracker { } } - ts_control::PeerUpdate::Delta { remove, upsert } => { + ts_control::PeerUpdate::Delta { + patch, + remove, + upsert, + } => { tracing::trace!("delta peer update"); + for peer in remove { + let node_key = self.id_to_nodekey.remove(peer); + + if let Some(node_key) = node_key { + self.peers.remove(&node_key); + deletions.insert(node_key); + } + } + for peer in upsert { self.id_to_nodekey.insert(peer.id, peer.node_key); self.peers.insert(peer.node_key, peer.clone()); @@ -222,18 +238,34 @@ impl Message> for PeerTracker { upserts.insert(peer.node_key); } - for peer in remove { - let node_key = self.id_to_nodekey.remove(peer); + for update in patch { + let node_key = match self.id_to_nodekey.get(&update.id) { + Some(key) => key, + None => { + tracing::warn!(node_id = update.id, "peer update for unknown node"); + continue; + } + }; + + if let Some(peer) = self.peers.get_mut(node_key) + && !peer.apply_update(update) + { + tracing::warn!(node_id = update.id, "failed to apply update to node"); + continue; + } - if let Some(node_key) = node_key { - self.peers.remove(&node_key); - deletions.insert(node_key); + patches.insert(*node_key); + + // If the peer's node key changed, we have to update the id_to_nodekey mapping. + if let Some(node_key) = update.node_key { + self.id_to_nodekey.insert(update.id, node_key); } } } } tracing::debug!( + n_patch = patches.len(), n_upsert = upserts.len(), n_delete = deletions.len(), peer_count = self.peers.len(), @@ -268,6 +300,7 @@ impl Message> for PeerTracker { if let Err(e) = self .env .publish(PeerState { + patches, upserts, deletions, peers: Arc::new(self.peers.clone()), @@ -343,7 +376,7 @@ mod test { use std::net::Ipv4Addr; use ipnet::Ipv4Net; - use ts_control::{StableNodeId, TailnetAddress}; + use ts_control::{NodeStatus, StableNodeId, TailnetAddress}; use super::*; @@ -353,9 +386,11 @@ mod test { node_key: Default::default(), id: 0, + status: NodeStatus::Unknown, stable_id: StableNodeId("".to_owned()), disco_key: Default::default(), machine_key: None, + tailnet_lock_key_signature: None, tailnet: None, hostname: "".to_owned(), tailnet_address: TailnetAddress { @@ -366,6 +401,8 @@ mod test { node_key_expiry: None, derp_region: None, tags: vec![], + capability_version: Default::default(), + node_capabilities: Default::default(), } } diff --git a/ts_transport_derp/src/lib.rs b/ts_transport_derp/src/lib.rs index d58fe004..f4c4a960 100644 --- a/ts_transport_derp/src/lib.rs +++ b/ts_transport_derp/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] use core::{ - fmt::Formatter, + fmt, net::{Ipv4Addr, Ipv6Addr}, num::NonZeroU32, }; @@ -18,9 +18,18 @@ pub use error::Error; /// A 24-byte nonce for symmetric encryption with ChaCha20Poly1305. #[repr(C)] -#[derive(Debug, Copy, Clone, PartialEq, KnownLayout, Immutable, IntoBytes, FromBytes)] +#[derive(Copy, Clone, PartialEq, KnownLayout, Immutable, IntoBytes, FromBytes)] pub struct Nonce(pub [u8; 24]); +impl fmt::Debug for Nonce { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in self.0.iter() { + write!(f, "{b:02x}")?; + } + Ok(()) + } +} + impl From> for Nonce { fn from(value: crypto_box::aead::Nonce) -> Self { Nonce(value.into()) @@ -37,8 +46,8 @@ impl From for crypto_box::aead::Nonce { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct RegionId(pub NonZeroU32); -impl core::fmt::Display for RegionId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl fmt::Display for RegionId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.get().fmt(f) } } @@ -100,7 +109,7 @@ impl ServerConnInfo { /// /// - The URL uses the `https` scheme /// - The URL's host resolves to IPv4 and IPv6 addresses via DNS - /// - The TLS common name is the url's host + /// - The TLS common name is the URL's host /// - The STUN port for the server is the default (3478) /// - The server doesn't support port 80 connections for captive portal detection pub fn default_from_url(url: &url::Url) -> Option {