Skip to main content

ts_control_serde/
node.rs

1use alloc::{borrow::Cow, vec::Vec};
2use core::net::{IpAddr, SocketAddr};
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use ts_capabilityversion::CapabilityVersion;
7use ts_keys::{DiscoPublicKey, MachinePublicKey, NodePublicKey};
8
9use crate::{DnsResolver, derp_map::RegionId, host_info::HostInfo, user::UserId};
10
11/// A serialized (marshalled) node key signature. If valid, authorizes a specific Tailscale node to
12/// join a Tailnet protected with Tailnet Lock. The Tailnet Key Authority (TKA) for a Tailnet can
13/// verify if a signature is valid.
14///
15/// For more info, see `tka.NodeKeySignature` in the Golang codebase.
16pub type MarshaledSignature<'a> = &'a [u8];
17
18/// A unique integer ID for a Tailscale node.
19///
20/// It's global within a control plane URL (`tailscale up --login-server`) and is (as of
21/// 2025-01-06) never re-used even after a node is deleted.
22///
23/// To be nice, control plane servers should not use int64s that are too large to fit in a
24/// JavaScript number (see JavaScript's `Number.MAX_SAFE_INTEGER`). The Tailscale-hosted control
25/// plane stopped allocating large integers in March 2023, but nodes prior to that may have node
26/// IDs larger than `MAX_SAFE_INTEGER` (2^53 – 1).
27///
28/// [`NodeId`]s are not stable across control plane URLs. For more stable URLs, see [`StableNodeId`].
29pub type NodeId = i64;
30
31/// A string representation of a Tailscale node's [`NodeId`].
32///
33/// Different control plane servers should ideally have different [`StableNodeId`] suffixes for
34/// different sites or regions.
35///
36/// Being a string, it's safer to use in JavaScript without worrying about the size of the integer,
37/// as documented on [`NodeId`]. But in general, Tailscale APIs can accept either a [`NodeId`]
38/// integer or a [`StableNodeId`] string when referring to a node.
39#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
40pub struct StableNodeId<'a>(#[serde(borrow)] pub &'a str);
41
42/// A Tailscale device in a Tailnet.
43///
44/// The struct-level `#[serde_with::apply]` block makes **every** `Vec`/map field tolerate a wire
45/// `null` (Go marshals empty `omitempty` slices/maps as `null`; see
46/// [`crate::util::null_to_default`]). Applying it at the struct level — rather than annotating each
47/// field — means any `Vec`/map field added later is covered automatically, closing the recurring
48/// "forgot a field" gap that broke decoding against IPv6-off control planes. `Option<Vec<…>>` fields
49/// (e.g. `allowed_ips`, `tags`) are written as `Option<Vec<…>>`, matched by none of the rules below
50/// (the `apply` macro matches the type **exactly as written**), so they keep their `null` → `None`
51/// semantics untouched. The path-qualified `ts_nodecapability::Map` rule is required because the
52/// `cap_map` field is written with that full path — a bare `Map` token would not match it.
53#[serde_with::apply(
54    Vec      => #[serde(default, deserialize_with = "crate::util::null_to_default")],
55    ts_nodecapability::Map => #[serde(default, deserialize_with = "crate::util::null_to_default")],
56)]
57#[derive(Clone, Debug, Default, Deserialize, Serialize)]
58#[serde(rename_all = "PascalCase", default)]
59pub struct Node<'a> {
60    /// A unique integer ID for the Tailscale node.
61    #[serde(rename = "ID")]
62    pub id: NodeId,
63    /// A string representation of the Tailscale node's [`Node::id`] field.
64    #[serde(rename = "StableID", borrow)]
65    pub stable_id: StableNodeId<'a>,
66
67    /// The fully-qualified domain name (FQDN) of this node, as well as the MagicDNS name for the
68    /// node. Ends with a trailing dot, e.g. "host.tail-scale.ts.net."
69    #[serde(borrow)]
70    pub name: Cow<'a, str>,
71
72    /// Unique ID of the [`User`][crate::User] who created the node.
73    ///
74    /// If ACL tags are in use for the node, this field doesn't reflect the ACL identity that the
75    /// node is running as.
76    pub user: UserId,
77    /// Unique ID of the user who shared this node, if non-zero and different from [`Node::user`].
78    pub sharer: UserId,
79
80    /// If populated, the public key of the Tailscale node's [`NodeKeyPair`][ts_keys::NodeKeyPair].
81    pub key: NodePublicKey,
82    /// The date and time that the Tailscale node's [`NodeKeyPair`][ts_keys::NodeKeyPair] will expire.
83    pub key_expiry: Option<DateTime<Utc>>,
84    /// If populated, a signature of the Tailnet Key Authority (TKA) key authorizing the Tailscale
85    /// node to join the Tailnet.
86    #[serde(borrow)]
87    pub key_signature: MarshaledSignature<'a>,
88    /// If populated, the public key of the Tailscale node's [`MachineKeyPair`][ts_keys::MachineKeyPair].
89    pub machine: Option<MachinePublicKey>,
90    /// If populated, the public key of the Tailscale node's [`DiscoKeyPair`][ts_keys::DiscoKeyPair].
91    pub disco_key: Option<DiscoPublicKey>,
92
93    /// The IP addresses (CIDR prefixes) assigned to this node in the tailnet (Go `tailcfg.Node`'s
94    /// `Addresses []netip.Prefix`).
95    ///
96    /// A **variable-length** list, NOT a fixed `(v4, v6)` pair: a node on an IPv4-only tailnet
97    /// (e.g. an IPv6-off control plane / Headscale) is assigned **only** an IPv4 prefix, so this
98    /// has length 1. Modeling it as a 2-tuple broke deserialization against such control planes
99    /// ("invalid length 1, expected a tuple of size 2"). The domain [`Node`] picks the first IPv4
100    /// and (optionally) the first IPv6 prefix out of this list.
101    ///
102    /// `null` tolerance is supplied by the struct-level `#[serde_with::apply]` block.
103    pub addresses: Vec<ipnet::IpNet>,
104    /// IP ranges to route to this node.
105    ///
106    /// As of [`CapabilityVersion::V112`], this may be null/undefined on the wire to indicate the
107    /// value is the same as [`Node::addresses`]. Once deserialized, it must always be populated,
108    /// even if those values are identical to [`Node::addresses`].
109    #[serde(rename = "AllowedIPs")]
110    pub allowed_ips: Option<Vec<ipnet::IpNet>>,
111    /// IP addresses/ports that this node can be reached directly on.
112    ///
113    /// Examples include public IP addresses/ports discovered via disco/STUN, or LAN-local IP
114    /// addresses/ports.
115    pub endpoints: Vec<SocketAddr>,
116
117    /// Deprecated. This node's home DERP region ID, but shoved into an IP:port string for legacy
118    /// reasons. The IP address is always `127.3.3.40` (a loopback address (127) followed by the
119    /// number keys over the letters DERP on a QWERTY keyboard (`3.3.40`)). The "port number" is
120    /// the home DERP region ID.
121    ///
122    /// The [`Node::home_derp`] field has replaced this since capability version 111, but old
123    /// servers might still send this field (see tailscale/tailscale#14636). Do not use this field
124    /// in code other than to upgrade/canonicalize the value to use [`Node::home_derp`] if a
125    /// `"LegacyDERPString"` field arrives on the wire.
126    #[serde(rename = "DERP", with = "legacy_derp_string")]
127    #[deprecated = "use Node::home_derp field instead"]
128    pub legacy_derp_string: Option<RegionId>,
129
130    /// Unique ID of this node's home DERP region.
131    ///
132    /// May be zero if not yet known, but will ideally always be non-zero for normal connectivity;
133    /// as DERP is used to discover direct connections, a home DERP region ID of zero prevents
134    /// direct connection types from being discovered until its home DERP region ID is populated.
135    ///
136    /// Preferred over the [`Node::legacy_derp_string`] field and supported by clients as of
137    /// [`CapabilityVersion`] 111.
138    #[serde(rename = "HomeDERP", deserialize_with = "crate::util::derp_region_id")]
139    pub home_derp: Option<RegionId>,
140
141    /// A summary of the host that a Tailscale node is running on. Includes information about the
142    /// version of Tailscale running on the host, the operating system, running services, and
143    /// various diagnostic/logging and configuration values.
144    ///
145    /// Wire key `Hostinfo` — Go names the field `Hostinfo` (lowercase `i`, tailcfg.go:406), so serde
146    /// `PascalCase` (`HostInfo`) is wrong and a strict Go decoder drops it.
147    #[serde(rename = "Hostinfo")]
148    #[serde(borrow)]
149    pub host_info: HostInfo<'a>,
150    /// The date/time this Tailscale node was created (added to the Tailnet for the first time).
151    pub created: DateTime<Utc>,
152    /// The node's [`CapabilityVersion`]; old servers may not send this value across the wire.
153    pub cap: CapabilityVersion,
154
155    /// The list of ACL tags applied to this node. Tags take the form of `tag:<value>` where
156    /// `<value>` starts with a letter and only contains alphanumerics and dashes (`-`).
157    ///
158    /// Some valid tag examples:
159    /// - `tag:prod`
160    /// - `tag:database`
161    /// - `tag:lab-1`
162    #[serde(borrow)]
163    pub tags: Option<Vec<&'a str>>,
164
165    /// The routes from [`Node::allowed_ips`] that this node is currently the primary subnet router
166    /// for, as determined by the control plane. It does not include the self address values from
167    /// [`Node::addresses`] that are in [`Node::allowed_ips`].
168    pub primary_routes: Vec<ipnet::IpNet>,
169
170    /// When the node was last online. Only updated when [`Node::online`] is `false`. It is
171    /// `None` if the current node doesn't have permission to know, or the node has never been
172    /// online.
173    pub last_seen: Option<DateTime<Utc>>,
174
175    /// Whether the node is currently connected to the control plane. A value of `None` means:
176    /// 1. The online status of the node is unknown
177    /// 2. The current node doesn't have permission to know whether this node is online
178    /// 3. The node has never been online
179    pub online: Option<bool>,
180
181    /// Whether or not the Tailscale node is authorized to be part of the Tailnet.
182    pub machine_authorized: bool,
183
184    /// Deprecated. Capabilities of this node.
185    ///
186    /// They're free-form strings, but should be in the form of URLs/URIs
187    /// such as:
188    /// - `https://tailscale.com/cap/is-admin`
189    /// - `https://tailscale.com/cap/file-sharing`
190    ///
191    /// Replaced by the [`Node::cap_map`] field since capability version 89; use that field instead
192    /// (see [tailscale/tailscale#11508](https://github.com/tailscale/tailscale/issues/11508)).
193    #[deprecated = "use Node::cap_map instead"]
194    #[serde(borrow)]
195    pub capabilities: Vec<ts_nodecapability::NodeCap<'a>>,
196
197    /// Map of capabilities to their optional argument/data values.
198    ///
199    /// It is valid for a capability to not have any argument/data values. These type of
200    /// capabilities indicate that a node has a capability, but there is no additional data
201    /// associated with it. These were previously represented by the `capabilities` field,
202    /// but can now be represented by an entry in [`Node::cap_map`] with an empty value.
203    ///
204    /// See [`NodeCap`][ts_nodecapability::NodeCap] for more information on keys.
205    ///
206    /// Metadata about nodes can be transmitted in 3 ways:
207    /// 1. [`MapResponse::node::cap_map`][Node::cap_map] describes attributes that affect behavior
208    ///    for this node, such as which features have been enabled through the admin panel and any
209    ///    associated configuration details.
210    /// 2. [`MapResponse::packet_filters`][crate::MapResponse::packet_filters] describes
211    ///    access (both IP- and application-based) that should be granted to peers.
212    /// 3. [`MapResponse::peers::cap_map`][Node::cap_map] describes attributes regarding a peer node, such as
213    ///    which features the peer supports or if that peer is preferred for a particular task vs
214    ///    other peers that could also be chosen.
215    #[serde(borrow)]
216    pub cap_map: ts_nodecapability::Map<'a>,
217
218    /// Indicates this node is not signed nor subject to Tailnet Key Authority (TKA) restrictions.
219    /// However, in exchange for that privilege, it does not get network access.It can only access
220    /// this node's peerapi, which may not let it do anything. It is the Tailscale client's job to
221    /// double-check the [`MapResponse::packet_filter`][crate::MapResponse::packet_filter] field to
222    /// verify that its [`Node::allowed_ips`] will not be accepted by the packet filter.
223    #[serde(rename = "UnsignedPeerAPIOnly")]
224    pub unsigned_peer_api_only: bool,
225
226    /// The per-node logtail ID used for data plane audit logging.
227    #[serde(rename = "DataPlaneAuditLogID", borrow)]
228    pub data_plane_audit_log_id: &'a str,
229
230    /// Whether or not this node's key has expired.
231    ///
232    /// Control may send this; clients are only allowed to set this from `false` to `true`. On the
233    /// client, this is calculated client-side based on a timestamp sent from control to avoid
234    /// clock skew issues.
235    pub expired: bool,
236
237    /// The IPv4 address that this peer knows the current node as. It may be `None` if the peer
238    /// knows the current node by its native IPv4 address.
239    ///
240    /// This field is only populated in [`MapResponse::peers`][crate::MapResponse::peers], and will not be populated for the
241    /// current node. If set, it should be used to masquerade traffic originating from the current
242    /// node to this peer. The masquerade address is only relevant for this peer and not for other
243    /// peers. This only applies to traffic originating from the current node to the peer or any of
244    /// its subnets. Traffic originating from subnet routes will not be masqueraded (e.g. in case
245    /// of `--snat-subnet-routes`).
246    pub self_node_v4_masq_addr_for_this_peer: Option<IpAddr>,
247    /// The IPv6 address that this peer knows the current node as. It may be `None` if the peer
248    /// knows the current node by its native IPv6 address.
249    ///
250    /// This field is only populated in [`MapResponse::peers`][crate::MapResponse::peers], and will not be populated for the
251    /// current node. If set, it should be used to masquerade traffic originating from the current
252    /// node to this peer. The masquerade address is only relevant for this peer and not for other
253    /// peers. This only applies to traffic originating from the current node to the peer or any of
254    /// its subnets. Traffic originating from subnet routes will not be masqueraded (e.g. in case
255    /// of `--snat-subnet-routes`).
256    pub self_node_v6_masq_addr_for_this_peer: Option<IpAddr>,
257
258    /// Indicates that this is a non-Tailscale WireGuard peer.
259    ///
260    /// WireGuard-only peers are not expected to speak Disco or DERP, and must have valid values in
261    /// [`Node::endpoints`] to be reachable.
262    #[serde(rename = "IsWireGuardOnly")]
263    pub is_wireguard_only: bool,
264
265    /// Indicates that this node is jailed and should not be allowed initiate connections, but
266    /// should be allowed to accept inbound connections.
267    pub is_jailed: bool,
268
269    /// The list of DNS servers that should be used when this node is WireGuard-only and being used
270    /// as an exit node.
271    #[serde(rename = "ExitNodeDNSResolvers", borrow)]
272    pub exit_node_dns_resolvers: Vec<DnsResolver<'a>>,
273}
274
275pub mod legacy_derp_string {
276    use core::num::NonZeroU32;
277
278    use serde::{Deserialize, Serialize};
279
280    use crate::DerpRegionId;
281
282    const PREFIX: &str = "127.3.3.40:";
283
284    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DerpRegionId>, D::Error>
285    where
286        D: serde::Deserializer<'de>,
287    {
288        let s = <&'de str>::deserialize(deserializer)?;
289        if !s.starts_with(PREFIX) {
290            return Ok(None);
291        }
292
293        let Some((_pfx, port)) = s.split_at_checked(PREFIX.len()) else {
294            return Ok(None);
295        };
296
297        let port = match port.parse::<u16>() {
298            Ok(port) => port,
299            Err(e) => return Err(serde::de::Error::custom(e)),
300        };
301
302        let Some(region) = NonZeroU32::new(port as _) else {
303            return Ok(None);
304        };
305
306        Ok(Some(DerpRegionId::from(region)))
307    }
308
309    pub fn serialize<S>(val: &Option<DerpRegionId>, s: S) -> Result<S::Ok, S::Error>
310    where
311        S: serde::Serializer,
312    {
313        match val {
314            &Some(x) => {
315                let val: u32 = x.into();
316                alloc::format!("{PREFIX}{val}").serialize(s)
317            }
318            None => "".serialize(s),
319        }
320    }
321}