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}