1- use core:: net:: { IpAddr , SocketAddr } ;
1+ use alloc:: collections:: BTreeMap ;
2+ use core:: {
3+ fmt,
4+ net:: { IpAddr , SocketAddr } ,
5+ } ;
26
37use chrono:: { DateTime , Utc } ;
8+ use ts_capabilityversion:: CapabilityVersion ;
49use ts_keys:: { DiscoPublicKey , MachinePublicKey , NodePublicKey } ;
510
611/// The unique id of a node.
@@ -10,20 +15,125 @@ pub type Id = i64;
1015#[ derive( Debug , Clone , PartialEq , Eq , Hash ) ]
1116pub struct StableId ( pub String ) ;
1217
18+ /// Timestamp when an offline node was last connected to the control plane. Only applies to nodes
19+ /// that are offline (disconnected from the control plane).
20+ ///
21+ /// Some timestamp values are only accurate to a resolution of ~10 minutes for privacy reasons, and
22+ /// do not take into account differences between the control plane clock and local device clock. See
23+ /// each variant for more information.
24+ #[ derive( Debug , Copy , Clone , PartialEq , Eq , Hash ) ]
25+ pub enum NodeLastSeen {
26+ /// The last time the node was connected to the control plane, as reported by the control plane
27+ /// itself.
28+ ///
29+ /// The timestamp value may be rounded to the nearest 10-minute boundary before being reported
30+ /// to this device by the control plane; in other words, many `Control` timestamps will be
31+ /// aligned to 10-minute boundaries. This is a privacy-preserving measure implemented by the
32+ /// control plane. Note that we have seen some non-rounded timestamps being reported from the
33+ /// control plane, although it's unclear if this is a bug or by design.
34+ ///
35+ /// Note that the timestamp value was generated by the control plane. The control plane's
36+ /// "clock" isn't synchronized with the local device's clock, so the timestamp value may be
37+ /// much more than 5 minutes in the past/future when compared to the local device clock.
38+ Control ( DateTime < Utc > ) ,
39+ /// The last time the node was connected to the control plane, as estimated by this device.
40+ ///
41+ /// For some types of node updates, we're only notified that the node is offline, not when it
42+ /// was last seen by the control plane. When we receive one of these offline updates, we
43+ /// estimate a last-seen timestamp using the local device clock. As the control plane "clock"
44+ /// and the local device clock aren't synchronized, this estimate may differ much more than 5
45+ /// minutes in the past/future when compared to a fuzzed timestamp generated by the control
46+ /// plane "clock".
47+ Estimated ( DateTime < Utc > ) ,
48+ }
49+
50+ impl fmt:: Display for NodeLastSeen {
51+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
52+ match self {
53+ NodeLastSeen :: Control ( dt) => write ! ( f, "last seen (by control): {}" , dt) ,
54+ NodeLastSeen :: Estimated ( dt) => write ! ( f, "last seen (estimated): {}" , dt) ,
55+ }
56+ }
57+ }
58+
59+ /// Whether a node is online (connected to the control plane) or offline (disconnected from the
60+ /// control plane).
61+ #[ derive( Debug , Default , Copy , Clone , PartialEq , Eq , Hash ) ]
62+ pub enum NodeStatus {
63+ /// The node may be online or offline; the control plane hasn't informed us yet.
64+ #[ default]
65+ Unknown ,
66+ /// The node is online (connected to the control plane).
67+ Online ,
68+ /// The node is offline (disconnected from the control plane).
69+ Offline ( NodeLastSeen ) ,
70+ }
71+
72+ impl fmt:: Display for NodeStatus {
73+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
74+ match self {
75+ NodeStatus :: Unknown => write ! ( f, "unknown" ) ,
76+ NodeStatus :: Online => write ! ( f, "online" ) ,
77+ NodeStatus :: Offline ( nls) => write ! ( f, "offline, {nls}" ) ,
78+ }
79+ }
80+ }
81+
82+ impl NodeStatus {
83+ /// Construct a new `NodeStatus` from a combination of online and last-seen timestamp.
84+ ///
85+ /// The netmap messages we get from the control plane have multiple ways to indicate a peer is
86+ /// online and/or the timestamp the peer was last connected to the control plane. This method
87+ /// wraps the sometimes-painful logic of creating a `NodeStatus` from the various combinations
88+ /// of netmap field values.
89+ pub fn new ( online : Option < bool > , last_seen : Option < DateTime < Utc > > ) -> Self {
90+ match ( online, last_seen) {
91+ ( Some ( true ) , None ) => Self :: Online ,
92+ ( Some ( false ) , None ) => {
93+ // We intentionally don't fuzz the estimated timestamp value because the Go client
94+ // code doesn't either.
95+ // See: https://github.com/tailscale/tailscale/blob/ee0a03b140021541495b25bdb6642b589431758b/control/controlclient/map.go#L753-L764
96+ Self :: Offline ( NodeLastSeen :: Estimated ( Utc :: now ( ) ) )
97+ }
98+ ( Some ( false ) , Some ( dt) ) | ( None , Some ( dt) ) => Self :: Offline ( NodeLastSeen :: Control ( dt) ) ,
99+ ( None , None ) => Self :: Unknown ,
100+ ( online, last_seen) => {
101+ tracing:: warn!(
102+ ?online,
103+ ?last_seen,
104+ "unexpected combination of online/last_seen states"
105+ ) ;
106+ Self :: Unknown
107+ }
108+ }
109+ }
110+ }
111+
13112/// A node in a tailnet.
14113#[ derive( Debug , Clone , PartialEq , Eq , Hash ) ]
15114pub struct Node {
16115 /// The node's id.
17116 pub id : Id ,
18117 /// The node's stable id.
19118 pub stable_id : StableId ,
20-
21- /// This node's hostname.
119+ /// The node's hostname.
22120 pub hostname : String ,
23121
122+ /// The node's capability version.
123+ pub capability_version : CapabilityVersion ,
124+
125+ /// Whether the node is connected to the control plane or not. If [`NodeStatus::Unknown`], the
126+ /// control plane hasn't told us yet.
127+ ///
128+ /// This field does _not_ indicate whether the node is reachable or visible from this device.
129+ pub status : NodeStatus ,
130+
24131 /// The tailnet this node belongs to.
25132 pub tailnet : Option < String > ,
26133
134+ /// The node capabilities assigned to this node.
135+ pub node_capabilities : BTreeMap < String , Vec < String > > ,
136+
27137 /// The tags assigned to this node.
28138 pub tags : Vec < String > ,
29139
@@ -39,6 +149,9 @@ pub struct Node {
39149 pub machine_key : Option < MachinePublicKey > ,
40150 /// The node's [`DiscoPublicKey`], if known.
41151 pub disco_key : Option < DiscoPublicKey > ,
152+ /// The signature of the node's public key with the Tailnet Lock signing key, if Tailnet Lock
153+ /// is enabled and the signature is known.
154+ pub tailnet_lock_key_signature : Option < Vec < u8 > > ,
42155
43156 /// The routes this node accepts traffic for.
44157 pub accepted_routes : Vec < ipnet:: IpNet > ,
@@ -50,6 +163,67 @@ pub struct Node {
50163}
51164
52165impl Node {
166+ /// Apply the given partial update to this `Node`. Returns `true` if the update was successfully
167+ /// applied; `false` otherwise.
168+ #[ tracing:: instrument( skip_all, fields( id = self . stable_id. 0 , hostname = self . hostname) ) ]
169+ pub fn apply_update ( & mut self , update : & NodeUpdate ) -> bool {
170+ if self . id != update. id {
171+ tracing:: warn!(
172+ node_id = self . id,
173+ update_id = update. id,
174+ "ignoring node update with incorrect node ID"
175+ ) ;
176+ return false ;
177+ }
178+
179+ if update. status != NodeStatus :: Unknown {
180+ tracing:: debug!( old_status = %self . status, "peer {}" , update. status) ;
181+ self . status = update. status ;
182+ }
183+
184+ if let Some ( cap) = update. cap {
185+ tracing:: debug!( old=?self . capability_version, new=?cap, "updating capability version" ) ;
186+ self . capability_version = cap;
187+ }
188+
189+ if let Some ( cap_map) = update. cap_map . as_ref ( ) {
190+ tracing:: debug!( old=?self . node_capabilities, new=?cap_map, "updating node capabilities" ) ;
191+ self . node_capabilities = cap_map. clone ( ) ;
192+ }
193+
194+ if let Some ( derp_region) = update. derp_region {
195+ tracing:: debug!( old=?self . derp_region, new=?derp_region, "updating derp home region" ) ;
196+ self . derp_region = Some ( derp_region) ;
197+ }
198+
199+ if let Some ( disco_key) = update. disco_key {
200+ tracing:: debug!( old=?self . disco_key, new=?disco_key, "updating disco key" ) ;
201+ self . disco_key = Some ( disco_key) ;
202+ }
203+
204+ if let Some ( node_key) = update. node_key {
205+ tracing:: debug!( old=?self . node_key, new=?node_key, "updating node key" ) ;
206+ self . node_key = node_key;
207+ }
208+
209+ if let Some ( node_key_expiry) = update. node_key_expiry {
210+ tracing:: debug!( old=?self . node_key_expiry, new=?node_key_expiry, "updating node key expiry" ) ;
211+ self . node_key_expiry = Some ( node_key_expiry) ;
212+ }
213+
214+ if let Some ( tl_sig) = & update. tailnet_lock_key_signature {
215+ tracing:: debug!( old=?self . tailnet_lock_key_signature, new=?tl_sig, "updating tailnet lock key signature" ) ;
216+ self . tailnet_lock_key_signature = Some ( tl_sig. clone ( ) ) ;
217+ }
218+
219+ if let Some ( underlay_addresses) = & update. underlay_addresses {
220+ tracing:: debug!( old=?self . underlay_addresses, new=?underlay_addresses, "updating underlay addresses" ) ;
221+ self . underlay_addresses = underlay_addresses. clone ( ) ;
222+ }
223+
224+ true
225+ }
226+
53227 /// The fully-qualified domain name of the node.
54228 ///
55229 /// 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 {
124298 Self {
125299 id : value. id ,
126300 stable_id : StableId ( value. stable_id . 0 . to_string ( ) ) ,
127-
128301 hostname : hostname. to_owned ( ) ,
302+
303+ capability_version : value. cap ,
304+
305+ status : NodeStatus :: new ( value. online , value. last_seen ) ,
129306 tailnet,
130307
308+ node_capabilities : value
309+ . cap_map
310+ . iter ( )
311+ . map ( |( k, v) | ( k. to_string ( ) , v. into ( ) ) )
312+ . collect ( ) ,
131313 tags : value
132314 . tags
133315 . as_ref ( )
@@ -142,6 +324,7 @@ impl From<&ts_control_serde::Node<'_>> for Node {
142324 node_key_expiry : value. key_expiry ,
143325 machine_key : value. machine ,
144326 disco_key : value. disco_key ,
327+ tailnet_lock_key_signature : value. key_signature . as_ref ( ) . map ( |s| Vec :: < u8 > :: from ( * s) ) ,
145328
146329 accepted_routes : value
147330 . allowed_ips
@@ -159,3 +342,75 @@ impl From<&ts_control_serde::Node<'_>> for Node {
159342 }
160343 }
161344}
345+
346+ /// A partial update to a [`Node`] in a tailnet.
347+ ///
348+ /// Devices should reject any `NodeUpdate` that it doesn't have a corresponding [`Node`] for. This
349+ /// type combines multiple different update/patch-style fields from netmap messages into a single
350+ /// type, such as the
351+ #[ derive( Debug , Default , Clone , PartialEq , Eq , Hash ) ]
352+ pub struct NodeUpdate {
353+ /// The node's id.
354+ pub id : Id ,
355+
356+ /// Whether this node is online or offline (with last-seen timestamp), according to the control
357+ /// plane. If [`NodeStatus::Unknown`], has not changed.
358+ pub status : NodeStatus ,
359+
360+ /// The DERP region for this node. If `None`, has not changed.
361+ pub derp_region : Option < ts_transport_derp:: RegionId > ,
362+
363+ /// The node's capability version. If `None`, has not changed.
364+ pub cap : Option < CapabilityVersion > ,
365+ /// The node's capabilities (node caps, not peer caps). If `None`, has not changed.
366+ pub cap_map : Option < BTreeMap < String , Vec < String > > > ,
367+
368+ /// The node's [`NodePublicKey`]. If `None`, has not changed.
369+ pub node_key : Option < NodePublicKey > ,
370+ /// The node key's expiration. If `None`, has not changed.
371+ pub node_key_expiry : Option < DateTime < Utc > > ,
372+
373+ /// The node's [`DiscoPublicKey`]. If `None`, has not changed.
374+ pub disco_key : Option < DiscoPublicKey > ,
375+
376+ /// The node's key signature for Tailnet Lock. If `None`, has not changed.
377+ pub tailnet_lock_key_signature : Option < Vec < u8 > > ,
378+
379+ /// The underlay addresses this node is reachable on (`Endpoints` in Go). If `None`, has not
380+ /// changed.
381+ pub underlay_addresses : Option < Vec < SocketAddr > > ,
382+ }
383+
384+ impl From < & ts_control_serde:: PeerChange < ' _ > > for NodeUpdate {
385+ fn from ( value : & ts_control_serde:: PeerChange < ' _ > ) -> Self {
386+ let cap_map = value. cap_map . as_ref ( ) . map ( |m| {
387+ m. iter ( )
388+ . map ( |( name, values) | {
389+ (
390+ String :: from ( * name) ,
391+ values
392+ . 0
393+ . iter ( )
394+ . map ( |v| v. to_string ( ) )
395+ . collect :: < Vec < String > > ( ) ,
396+ )
397+ } )
398+ . collect ( )
399+ } ) ;
400+
401+ Self {
402+ id : value. node_id ,
403+ status : NodeStatus :: new ( value. online , value. last_seen ) ,
404+ derp_region : value
405+ . derp_region
406+ . map ( |x| ts_transport_derp:: RegionId ( x. into ( ) ) ) ,
407+ cap : value. cap ,
408+ cap_map,
409+ node_key : value. key ,
410+ node_key_expiry : value. key_expiry ,
411+ disco_key : value. disco_key ,
412+ tailnet_lock_key_signature : value. key_signature . map ( |x| x. into ( ) ) ,
413+ underlay_addresses : value. endpoints . clone ( ) ,
414+ }
415+ }
416+ }
0 commit comments