ts_control_serde/netmap.rs
1use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec};
2use core::net::SocketAddr;
3
4use chrono::{DateTime, Utc};
5use serde::Deserialize;
6use ts_capabilityversion::CapabilityVersion;
7use ts_keys::{DiscoPublicKey, NodePublicKey};
8
9use crate::{
10 DerpRegionId, DnsConfig, MarshaledSignature,
11 client_version::ClientVersion,
12 debug::Debug,
13 derp_map::DerpMap,
14 dial_plan::ControlDialPlan,
15 host_info::HostInfo,
16 node::{Node, NodeId},
17 ping::PingRequest,
18 ssh_policy::SSHPolicy,
19 tka_info::TkaInfo,
20 user::UserProfile,
21};
22
23/// Sent by a Tailscale node to the control server to either update the control plane about its
24/// current state, or to start a long-poll of network map updates. Includes a copy of the node's
25/// current set of WireGuard endpoints and general host information.
26///
27/// The request is JSON-encoded and sent to the control server via an HTTP POST to
28/// `https://<control-server>/machine/map`.
29#[serde_with::apply(
30 bool => #[serde(skip_serializing_if = "crate::util::is_default")],
31 &str => #[serde(borrow)] #[serde(skip_serializing_if = "str::is_empty")],
32 Option => #[serde(skip_serializing_if = "Option::is_none")],
33 Vec => #[serde(skip_serializing_if = "Vec::is_empty")],
34 _ => #[serde(default)],
35)]
36#[derive(Debug, Default, Clone, serde::Deserialize, serde::Serialize)]
37#[serde(rename_all = "PascalCase")]
38pub struct MapRequest<'a> {
39 /// The capability version of this Tailscale node. Incremented whenever the client code
40 /// (in any client, Go/Rust/etc) changes enough that we want to signal to the control server
41 /// that we're capable of something different.
42 ///
43 /// See the [`CapabilityVersion`] enum for info the changes introduced with each version.
44 pub version: CapabilityVersion,
45
46 /// Either "zstd" to receive [`MapResponse`]s compressed with `zstd`, or "" to receive
47 /// [`MapResponse`]s with no compression.
48 pub compress: &'a str,
49 /// Whether the control server should periodically send application-level keep-alives back to
50 /// this Tailscale node.
51 pub keep_alive: bool,
52
53 /// The public key of this Tailscale node.
54 pub node_key: NodePublicKey,
55 /// The public key this Tailscale node will use with the Disco protocol to establish direct
56 /// connections with peer nodes in the Tailnet.
57 pub disco_key: DiscoPublicKey,
58
59 /// If populated, the public key of the node's hardware-backed identity attestation key.
60 pub hardware_attestation_key: Option<Vec<u8>>,
61 /// If populated, the signature of "$UNIX_TIMESTAMP|$NODE_PUBLIC_KEY" as signed by the
62 /// hardware attestation key.
63 pub hardware_attestation_key_signature: Option<Vec<u8>>,
64 /// If populated, the time at which the [`MapRequest::hardware_attestation_key_signature`] was
65 /// created.
66 #[serde_as(as = "serde_with::TimestampSeconds<i64>")]
67 pub hardware_attestation_key_signature_timestamp: Option<DateTime<Utc>>,
68
69 /// Whether or not this Tailscale node wants to receive multiple [`MapResponse`]s over the same
70 /// HTTP connection, referred to as "long-polling" or a "map poll".
71 ///
72 /// If `false`, the control server will send a single [`MapResponse`] and then close the
73 /// connection. If `true` and [`MapRequest::version`] >= 68, the server will treat this as a
74 /// read-only request and ignore [`MapRequest::host_info`] and any other fields that might be
75 /// set.
76 pub stream: bool,
77
78 /// Current information about this Tailscale node's host. Although it is always included in a
79 /// [`MapRequest`], a control server may choose to ignore it when [`MapRequest::stream`] is
80 /// `true` and [`MapRequest::version`] >= 68.
81 #[serde(borrow)]
82 pub host_info: Option<HostInfo<'a>>,
83
84 /// If non-empty, indicates a request to reattach to a previous map session after a previous
85 /// map session was interrupted for whatever reason. Its value is an opaque string.
86 ///
87 /// When set, the Tailscale node must also send [`MapRequest::map_session_seq`] to specify the
88 /// last processed message in that prior session. The control server may choose to ignore the
89 /// request for any reason and start a new map session. This is only applicable when
90 /// [`MapRequest::stream`] is `true`.
91 pub map_session_handle: &'a str,
92 /// The sequence number in the map session (identified by [`MapRequest::map_session_handle`]
93 /// that was most recently processed by this Tailscale node. It is only applicable when
94 /// [`MapRequest::map_session_handle`] is specified. If the control server chooses to honor the
95 /// [`MapRequest::map_session_handle`] request, only sequence numbers greater than this value
96 /// will be returned.
97 #[serde(skip_serializing_if = "crate::util::is_default")]
98 pub map_session_seq: i64,
99
100 /// The client's magicsock UDP ip:port endpoints (IPv4 or IPv6).
101 ///
102 /// These can be ignored if `stream` is true and `version` >= 68.
103 #[serde(flatten, with = "endpoint_serde")]
104 pub endpoints: Vec<Endpoint>,
105
106 /// Describes the hash of the latest AUM applied to the local Tailnet Key Authority, if one is
107 /// operating.
108 #[serde(rename = "TKAHead")]
109 pub tka_head: &'a str,
110
111 /// Deprecated. In the past, was set by Tailscale nodes when they wanted to fetch the full
112 /// [`MapResponse`] from the control server without updating their [`MapRequest::endpoints`].
113 /// The intended use was for clients to discover the DERP map at start-up before their first
114 /// real endpoint update.
115 ///
116 /// This value must always be omitted or set to `false` as of [`MapRequest::version`] >= 68.
117 #[deprecated = "do not use; must always be omitted/false"]
118 pub read_only: Option<bool>,
119
120 /// Whether the Tailscale node is okay with the [`MapResponse::peers`] list being omitted in the
121 /// [`MapResponse`]. If `true`, the behavior of the control server varies based on the
122 /// [`MapRequest::stream`] and [`MapRequest::read_only`] flags:
123 ///
124 /// - If [`MapRequest::omit_peers`] is `true`, [`MapRequest::stream`] is `false`, and
125 /// [`MapRequest::read_only`] is `false`: the control server will let Tailscale nodes update
126 /// their endpoints without breaking existing long-polling connections. In this case, the
127 /// server can omit the entire response; the Tailscale node only needs to check the HTTP
128 /// response status code.
129 /// - If [`MapRequest::omit_peers`] is `true`, [`MapRequest::stream`] is `false`, and
130 /// [`MapRequest::read_only`] is `true`: the control server includes all fields in the
131 /// [`MapResponse`], as if the Tailscale node is fetching data from the control server for
132 /// the first time.
133 pub omit_peers: bool,
134
135 /// A list of strings specifying debugging and development features to enable in handling this
136 /// [`MapRequest`]. The values are deliberately unspecified, as they get added and removed all
137 /// the time during development, and offer no compatibility promise. To roll out semantic
138 /// changes, bump the [`CapabilityVersion`] instead.
139 ///
140 /// Current valid values are:
141 /// - `"warn-ip-forwarding-off"`: node is trying to be a subnet router, but their IP forwarding
142 /// is broken.
143 /// - `"warn-router-unhealthy"`: node's subnet router implementation is having problems.
144 pub debug_flags: Vec<&'a str>,
145
146 /// If non-empty, an opaque string sent by the Tailscale node that identifies this specific
147 /// connection to the control server. The server may choose to use this handle to identify
148 /// the connection for debugging or testing purposes. It has no semantic meaning.
149 pub connection_handle_for_test: &'a str,
150}
151
152/// An endpoint (address + port) on which a peer can be reached.
153#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
154pub struct Endpoint {
155 /// The address of this endpoint.
156 pub endpoint: SocketAddr,
157
158 /// The type of this endpoint.
159 pub ty: EndpointType,
160}
161
162/// Distinguishes different sources of [`MapRequest::endpoints`] values.
163#[derive(
164 Debug, Copy, Clone, PartialEq, Eq, serde_repr::Serialize_repr, serde_repr::Deserialize_repr,
165)]
166#[repr(isize)]
167pub enum EndpointType {
168 /// Unknown endpoint type.
169 Unknown = 0,
170
171 /// Endpoint is a LAN address.
172 Local = 1,
173
174 /// Endpoint is a STUN address.
175 Stun = 2,
176
177 /// Endpoint is a router port-mapping.
178 PortMapped = 3,
179
180 /// Hard NAT: STUNed IPv4 with local fixed port.
181 Stun4LocalPort = 4,
182
183 /// Explicitly configured (routing to be done by client).
184 ExplicitConf = 5,
185}
186
187mod endpoint_serde {
188 use core::net::SocketAddr;
189
190 use serde::{Deserialize, Serialize};
191
192 use super::*;
193
194 #[derive(serde::Serialize, serde::Deserialize)]
195 #[serde(rename_all = "PascalCase")]
196 struct EndpointSerde {
197 pub endpoints: Vec<SocketAddr>,
198 pub endpoint_types: Vec<EndpointType>,
199 }
200
201 pub fn deserialize<'de, D>(de: D) -> Result<Vec<Endpoint>, D::Error>
202 where
203 D: serde::Deserializer<'de>,
204 {
205 let result = EndpointSerde::deserialize(de)?;
206
207 let eps = result
208 .endpoints
209 .into_iter()
210 .zip(result.endpoint_types)
211 .map(|(endpoint, ty)| Endpoint { endpoint, ty })
212 .collect();
213
214 Ok(eps)
215 }
216
217 pub fn serialize<S>(t: &[Endpoint], s: S) -> Result<S::Ok, S::Error>
218 where
219 S: serde::Serializer,
220 {
221 let tys = t.iter().map(|x| x.ty).collect();
222 let addrs = t.iter().map(|x| x.endpoint).collect();
223
224 EndpointSerde {
225 endpoint_types: tys,
226 endpoints: addrs,
227 }
228 .serialize(s)
229 }
230}
231
232/// The response to a [`MapRequest`]. It describes the state of the local Tailscale node, the peer
233/// nodes in the Tailnet, the DNS configuration, the packet filter, and more. A [`MapRequest`],
234/// depending on its parameters, may result in the control plane coordination server sending 0, 1,
235/// or a stream of multiple [`MapResponse`] values.
236///
237/// When a node sends a [`MapRequest`] to the control server with the [`MapRequest::stream`] flag
238/// set to `true`, the server will respond with a stream of [`MapResponse`]s. The long-lived HTTP
239/// transaction delivering the stream is called a "map poll". In a map poll, the first
240/// [`MapResponse`] will be complete; subsequent [`MapResponse`]s will be incremental updates with
241/// only changed information.
242///
243/// In general, fields omitted in the [`MapResponse`] JSON (or `None` in the deserialized struct
244/// instance) indicate the field's value is unchanged from the previous value. However, several
245/// older slice-like fields have different semantics; this is noted in the doc comments for the
246/// relevant fields. For background, see the [doc comment for `MapResponse`] in the Go client.
247///
248/// [doc comment for MapResponse]: <https://github.com/tailscale/tailscale/blob/e2233b794247bf20d022d0ebefa99ad39bbad591/tailcfg/tailcfg.go#L1927-L1936>
249///
250/// The struct-level `#[serde_with::apply]` block makes every bare `Vec`/map field tolerate a wire
251/// `null` (Go marshals empty `omitempty` slices/maps as `null`; see [`crate::util::null_to_default`])
252/// and auto-covers any such field added later. This is deliberately scoped to **non-`Option`**
253/// `Vec`/map fields: the delta-encoded fields whose `null`/absence means "unchanged from the prior
254/// poll" (`peers`, `peers_changed`, `peers_removed`, `packet_filter` singular, etc.) are all
255/// `Option<…>` — matched by none of the rules below (the `apply` macro matches the type **exactly
256/// as written**, path qualifier and all), so they are left completely untouched and keep their
257/// "unchanged" semantics. Note the path-qualified `ts_packetfilter_serde::Map` rule: a bare `Map`
258/// token would NOT match it (the field is written with its full path), which is why each alias
259/// spelling that appears on the struct needs its own rule.
260#[serde_with::apply(
261 Vec => #[serde(default, deserialize_with = "crate::util::null_to_default")],
262 BTreeMap => #[serde(default, deserialize_with = "crate::util::null_to_default")],
263 ts_packetfilter_serde::Map => #[serde(default, deserialize_with = "crate::util::null_to_default")],
264)]
265#[derive(Default, Debug, Clone, Deserialize)]
266#[serde(rename_all = "PascalCase", default)]
267pub struct MapResponse<'a> {
268 /// Optionally specifies a unique opaque handle for this stateful [`MapResponse`] session.
269 /// Servers may choose not to send it, and it's only sent on the first [`MapResponse`] in a
270 /// stream. The client can determine whether it's reattaching to a prior stream by seeing
271 /// whether this value matches the requested [`MapResponse::map_session_handle`].
272 #[serde(borrow)]
273 pub map_session_handle: &'a str,
274 /// Sequence number within a named map session (a response where the first message contains a
275 /// [`MapResponse::map_session_handle`]). The sequence number may be omitted on responses that
276 /// don't change the state of the stream, such as KeepAlive or certain types of PingRequests.
277 /// This is the value to be sent in [`MapRequest::map_session_seq`] to resume after this
278 /// message.
279 pub seq: i64,
280 /// If set, represents an empty message just to keep the connection alive. When `true`, all
281 /// other fields except [`MapResponse::ping_request`], [`MapResponse::control_time`], and
282 /// [`MapResponse::pop_browser_url`] are ignored.
283 pub keep_alive: Option<bool>,
284 /// If non-`None`, a request to the client to prove it's still there by sending an HTTP
285 /// request to the provided URL. No auth headers are necessary. [`MapResponse::ping_request`]
286 /// may be sent on any [`MapResponse`] (ones with [`MapResponse::keep_alive`] set to either
287 /// `true` or `false`).
288 pub ping_request: Option<PingRequest>,
289 /// If non-`None`, a URL for the client to open to complete an action. The client should
290 /// debounce identical URLs and only open it once for the same URL.
291 ///
292 /// A `Cow` because a URL legitimately carries `&` in its query string, which Go's `json.Marshal`
293 /// escapes to `&` by default — a borrowed `&str` cannot decode that escaped form and would
294 /// fail the whole `MapResponse` decode.
295 #[serde(borrow)]
296 pub pop_browser_url: Option<Cow<'a, str>>,
297
298 /// Describes the Tailscale node making the map request (ie, the "self" node). Starting with
299 /// capability version 18, a value of `None` means unchanged.
300 pub node: Option<Node<'a>>,
301
302 /// Describes the set of available DERP regions and servers. If `None`, the set of servers is
303 /// unchanged from the last set sent from the control plane to this client.
304 #[serde(rename = "DERPMap")]
305 pub derp_map: Option<DerpMap<'a>>,
306
307 /// The complete list of peer Tailscale nodes in the same Tailnet as this node. This field will
308 /// always be populated in the first [`MapResponse`] in a long-polled stream sent to this node.
309 /// Subsequent [`MapResponse`]s in the stream will usually provide delta-encoded updates on
310 /// nodes that have been added, removed, or changed since the previous [`MapResponse`] via the
311 /// [`MapResponse::peers_changed`] and [`MapResponse::peers_removed`] fields.
312 ///
313 /// If this field is populated, it takes precedence over the other two fields; in other words,
314 /// if [`MapResponse::peers`] is populated, you must ignore both the
315 /// [`MapResponse::peers_changed`] and [`MapResponse::peers_removed`] fields and use only the
316 /// values in this field.
317 ///
318 /// This list will always be sorted by [`Node::id`] in ascending order.
319 pub peers: Option<Vec<Node<'a>>>,
320 /// The Tailscale nodes in the Tailnet that have changed or been added since the last
321 /// [`MapResponse`] sent to this node. Do not use this field if [`MapResponse::peers`] is
322 /// populated.
323 ///
324 /// This list will always be sorted by [`Node::id`] in ascending order.
325 pub peers_changed: Option<Vec<Node<'a>>>,
326 /// IDs of Tailscale nodes that are no longer in the peer list for the Tailnet.
327 pub peers_removed: Option<Vec<NodeId>>,
328
329 /// If present, the indicated nodes have changed.
330 ///
331 /// This is a lighter version of `peers_changed` that only supports certain types of
332 /// updates.
333 ///
334 /// These are applied after `peers*`, but in practice, the control server should only
335 /// send these on their own, without the `peers*` fields also set.
336 #[serde(borrow)]
337 pub peers_changed_patch: Vec<Option<PeerChange<'a>>>,
338
339 /// How to update peers' [`last_seen`][crate::Node::last_seen] times.
340 ///
341 /// If the value for a peer is false, the peer is gone. If true, update `last_seen` to
342 /// now.
343 pub peer_seen_change: BTreeMap<NodeId, bool>,
344
345 /// Updates to peers' [`online`][crate::Node::online] states.
346 pub online_change: BTreeMap<NodeId, bool>,
347
348 /// The DNS settings for the client to use.
349 ///
350 /// A `None` value means no change.
351 #[serde(borrow, rename = "DNSConfig")]
352 pub dns_config: Option<DnsConfig<'a>>,
353
354 /// The name of the network that this node is in. It's either of the form:
355 /// - "example.com" (for user foo@example.com, for multi-user networks)
356 /// - "foo@gmail.com" (for siloed users on shared email providers)
357 ///
358 /// Do not depend on the exact format of this field; more forms will be added in the future. If
359 /// empty, the value is unchanged.
360 #[serde(borrow)]
361 pub domain: Cow<'a, str>,
362
363 /// Indicates whether this node's tailnet has requested that info about services be included in
364 /// [`Node::host_info`]. If `None`, the most recent non-empty MapResponse value in the HTTP
365 /// response stream is used.
366 pub collect_services: Option<bool>,
367
368 /// `packet_filter` are the firewall rules.
369 ///
370 /// For [`MapRequest::version`] >= 6, a `None` value means the most
371 /// previously streamed non-`None` [`MapResponse::packet_filter`] within
372 /// the same HTTP response. A present (`Some`) but empty list always means
373 /// no `packet_filter` (that is, to block everything).
374 ///
375 /// See [`packet_filters`][MapResponse::packet_filters] for the newer way to send
376 /// `packet_filter` updates.
377 #[serde(borrow)]
378 pub packet_filter: Option<ts_packetfilter_serde::Ruleset<'a>>,
379
380 /// `packet_filters` encodes incremental packet filter updates to the client
381 /// without having to send the entire packet filter on any changes as
382 /// required by the older `packet_filter` (singular) field above. The map keys
383 /// are server-assigned arbitrary strings. The map values are the new rules
384 /// for that key, or nil to delete it. The client then concatenates all the
385 /// rules together to generate the final packet filter. Because the
386 /// [`FilterRule`][ts_packetfilter_serde::FilterRule]s can only match or not match, the
387 /// ordering of filter rules doesn't matter.
388 ///
389 /// If the server sends a non-nil [`packet_filter`][MapResponse::packet_filter]
390 /// (above), that is equivalent to a named packet filter with the key "base". It is
391 /// valid for the server to send both `packet_filter` and `packet_filters` in the same
392 /// MapResponse or alternate between them within a session. `packet_filter` is applied
393 /// first (if set), and then `packet_filters`.
394 ///
395 /// As a special case, the map key "*" with a value of `None` means to clear all
396 /// prior named packet filters (including any implicit "base") before
397 /// processing the other map entries.
398 #[serde(borrow)]
399 pub packet_filters: ts_packetfilter_serde::Map<'a>,
400
401 // --------------------------------------------------------------------------------------------
402 /// The [`UserProfile`]s associated with Tailscale nodes in the Tailnet. As of
403 /// [`CapabilityVersion::V5`], contains only new or updated profiles.
404 pub user_profiles: Vec<UserProfile<'a>>,
405
406 // --------------------------------------------------------------------------------------------
407 /// Sets the health state of the node from the control plane's perspective (Go capver 24).
408 ///
409 /// In Go, a `nil` slice means "no change from the previous `MapResponse`", a non-`nil`
410 /// zero-length slice restores health to good (no known problems), and a non-empty slice is the
411 /// list of problems the control plane sees. Either this or
412 /// [`display_messages`][MapResponse::display_messages] is set, but not both.
413 ///
414 /// This fork decodes the wire value into a `Vec` (the struct-level `apply` block tolerates a
415 /// wire `null`, mapping it to an empty `Vec`); it does not currently distinguish "no change"
416 /// (`nil`) from "all good" (empty) downstream — the field is carried so health warnings are no
417 /// longer silently dropped.
418 pub health: Vec<&'a str>,
419
420 /// Structured health/display messages from the control plane (Go capver 117).
421 ///
422 /// The map keys are opaque `DisplayMessageID` strings; a value of `None` (Go `nil`, JSON
423 /// `null`) deletes that id. Go treats a populated map as a PATCH: new entries are added, `null`
424 /// values delete, and existing entries with new values are updated. As a special case, the key
425 /// `"*"` with a `None` value clears all prior display messages before the other entries are
426 /// processed. A `nil`/absent map (and, in Go, an empty map) means no change.
427 ///
428 /// Either this or [`health`][MapResponse::health] is set, but not both.
429 ///
430 /// This fork decodes-and-carries the map (the struct-level `apply` block tolerates a wire
431 /// `null`); the PATCH/`"*"`-clear/`null`-delete semantics are not yet applied downstream (see
432 /// the map-stream consumer). Decoding it here is what stops control-pushed display messages
433 /// from being silently dropped. TODO: wire the patch semantics into the map stream.
434 pub display_messages: BTreeMap<&'a str, Option<DisplayMessage<'a>>>,
435
436 /// If non-`None`, updates the SSH policy for how incoming SSH connections should be handled.
437 /// A `None` value means no change from the previous value.
438 #[serde(default, rename = "SSHPolicy")]
439 pub ssh_policy: Option<SSHPolicy<'a>>,
440
441 // --------------------------------------------------------------------------------------------
442 /// The current timestamp according to the control server; otherwise, `None`.
443 pub control_time: Option<DateTime<Utc>>,
444
445 /// Encodes the control plane's view of Tailnet Key Authority (TKA) state.
446 ///
447 /// If populated for an initial [`MapResponse`] (not a delta update), the control plane
448 /// believes TKA should be enabled for this node. If `None` in an initial [`MapResponse`], the
449 /// control plane believes TKA should be disabled for this node.
450 ///
451 /// If `None` in subsequent [`MapResponse`] updates in a long-polling map stream (i.e., delta
452 /// updates), there are no changes to TKA state since the previous value.
453 #[serde(rename = "TKAInfo")]
454 pub tka_info: Option<TkaInfo<'a>>,
455
456 /// If populated, the per-tailnet log ID to be used when writing data plane audit logs.
457 #[serde(rename = "DomainDataPlaneAuditLogID")]
458 pub domain_data_plane_audit_log_id: Option<&'a str>,
459
460 /// Deprecated. If populated, contains debug settings from the control server that this
461 /// Tailscale node should set.
462 #[deprecated = "use Node::capabilities or c2n requests instead"]
463 pub debug: Option<Debug>,
464
465 /// If populated, tells this Tailscale node how to connect to the control server. If `None`,
466 /// the node should use DNS to look up the IP address of the control server.
467 ///
468 /// Used to maintain connection if the node's network state changes after the initial
469 /// connection, or if the control server pushes other changes to the node (such as DNS config
470 /// updates) that break connectivity.
471 pub control_dial_plan: Option<ControlDialPlan<'a>>,
472
473 /// If populated, describes the latest Tailscale version that's available for download for this
474 /// node's platform and package type. If `None`, the latest version hasn't changed since the
475 /// previous value.
476 pub client_version: Option<ClientVersion<'a>>,
477
478 /// The default node auto-update setting for this tailnet. The node is free to opt-in or out
479 /// locally regardless of this value. This value is only used on first [`MapResponse`] from
480 /// control; the auto-update setting doesn't change if the tailnet admin flips the default
481 /// after the node registered.
482 pub default_auto_update: Option<bool>,
483}
484
485/// A structured health/display message pushed by the control plane (Go `tailcfg.DisplayMessage`,
486/// capver 117), surfaced to the user as a warning/notice about node or tailnet state.
487///
488/// `#[serde(default)]` makes every field optional (Go marshals `Title`/`Text`/`Severity` with no
489/// `omitempty`, but a tolerant decode shouldn't fail on a sparse message), and there is
490/// deliberately no `deny_unknown_fields`: Go adds fields to this struct over time, and an unknown
491/// field must not fail the whole netmap decode.
492#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
493#[serde(rename_all = "PascalCase", default)]
494pub struct DisplayMessage<'a> {
495 /// Short, human-readable title summarizing the message.
496 ///
497 /// `Cow` because admin/control-authored prose can contain a JSON escape (a literal `"`, `\`, or
498 /// — since Go's `json.Marshal` escapes `&`/`<`/`>` by default — an `&`); a borrowed `&str`
499 /// cannot decode an escaped string and would fail the whole `MapResponse` decode.
500 #[serde(borrow)]
501 pub title: Cow<'a, str>,
502 /// Longer, human-readable body text describing the message. `Cow` for the same escape-tolerance
503 /// reason as [`title`][Self::title] (and body prose is the most likely to contain a newline).
504 #[serde(borrow)]
505 pub text: Cow<'a, str>,
506 /// Severity of the message. Go's `DisplayMessageSeverity` is a string with known values
507 /// `"high"`, `"medium"`, and `"low"`; this is kept as an open `Cow<'a, str>` (rather than a
508 /// closed enum) so an unrecognized future severity decodes rather than failing the netmap.
509 #[serde(borrow)]
510 pub severity: Cow<'a, str>,
511 /// Whether the condition this message describes impacts the node's connectivity.
512 pub impacts_connectivity: bool,
513 /// Optional primary call-to-action (e.g. a "learn more"/"fix it" link) for this message.
514 #[serde(borrow)]
515 pub primary_action: Option<DisplayMessageAction<'a>>,
516}
517
518/// A call-to-action attached to a [`DisplayMessage`] (Go `tailcfg.DisplayMessageAction`): a label
519/// and a URL the client may surface as a button/link.
520#[derive(Default, Debug, Clone, PartialEq, Eq, Deserialize)]
521#[serde(rename_all = "PascalCase", default)]
522pub struct DisplayMessageAction<'a> {
523 /// The URL to open when the user activates the action. `Cow` because a URL's query string
524 /// carries `&` (which Go's `json.Marshal` escapes to `&`), which a borrowed `&str` cannot
525 /// decode.
526 #[serde(borrow, rename = "URL")]
527 pub url: Cow<'a, str>,
528 /// The human-readable label for the action (e.g. button text). `Cow` for escape tolerance.
529 #[serde(borrow)]
530 pub label: Cow<'a, str>,
531}
532
533/// An update to a node.
534#[derive(Default, Debug, Clone, serde::Deserialize)]
535#[serde(rename_all = "PascalCase", default)]
536pub struct PeerChange<'a> {
537 /// The ID of the node being mutated.
538 ///
539 /// If not known in the current netmap, this change should be ignored.
540 #[serde(rename = "NodeID")]
541 pub node_id: NodeId,
542
543 /// If present, the node's home derp region is updated to the new value.
544 #[serde(
545 rename = "DERPRegion",
546 deserialize_with = "crate::util::derp_region_id"
547 )]
548 pub derp_region: Option<DerpRegionId>,
549
550 /// If present, the node's capability version is the new value.
551 pub cap: Option<CapabilityVersion>,
552
553 /// If present, the node's capability map has changed.
554 #[serde(borrow)]
555 pub cap_map: Option<ts_nodecapability::Map<'a>>,
556
557 /// If present, the node's UDP endpoints have changed to the new value.
558 pub endpoints: Option<Vec<SocketAddr>>,
559
560 /// If present, the node's wireguard public key has changed.
561 pub key: Option<NodePublicKey>,
562
563 /// If present, the signature of the node's wireguard public key has changed.
564 #[serde(borrow)]
565 pub key_signature: Option<MarshaledSignature<'a>>,
566
567 /// If present, the node's disco key has changed.
568 pub disco_key: Option<DiscoPublicKey>,
569 /// If present, the node's online status changed.
570 pub online: Option<bool>,
571 /// If present, the node's last seen time changed.
572 pub last_seen: Option<DateTime<Utc>>,
573
574 /// If present, the node's key expiry has changed to the new value.
575 pub key_expiry: Option<DateTime<Utc>>,
576}
577
578#[cfg(test)]
579mod test {
580 use super::*;
581
582 #[test]
583 fn endpoint() {
584 const TEST: &str = r#"{
585 "Version": 130,
586
587 "Compress": "",
588 "KeepAlive": false,
589 "Stream": false,
590 "ReadOnly": false,
591 "OmitPeers": false,
592 "DebugFlags": [],
593 "ConnectionHandleForTest": "",
594 "NodeKey": "nodekey:0000000000000000000000000000000000000000000000000000000000000000",
595 "DiscoKey": "discokey:0000000000000000000000000000000000000000000000000000000000000000",
596 "MapSessionHandle": "",
597 "MapSessionSeq": 0,
598 "TKAHead": "",
599
600 "Endpoints": [
601 "1.2.3.4:80"
602 ],
603 "EndpointTypes": [
604 1
605 ]
606 }"#;
607
608 let req = serde_json::from_str::<MapRequest>(TEST).unwrap();
609
610 assert_eq!(
611 req.endpoints,
612 &[Endpoint {
613 endpoint: "1.2.3.4:80".parse().unwrap(),
614 ty: EndpointType::Local,
615 }]
616 );
617
618 let serialized = serde_json::to_string_pretty(&req).unwrap();
619 std::println!("{serialized}");
620 }
621
622 #[test]
623 fn ssh_policy_present() {
624 const TEST: &str = r#"{
625 "Seq": 1,
626 "SSHPolicy": {
627 "rules": [
628 {
629 "principals": [{ "any": true }],
630 "sshUsers": { "*": "=" },
631 "action": { "accept": true }
632 }
633 ]
634 }
635 }"#;
636
637 let resp = serde_json::from_str::<MapResponse>(TEST).unwrap();
638 let policy = resp.ssh_policy.expect("ssh_policy should be Some");
639 assert_eq!(policy.rules.len(), 1);
640 assert!(policy.rules[0].principals[0].any);
641 assert!(policy.rules[0].action.as_ref().unwrap().accept);
642 }
643
644 #[test]
645 fn ssh_policy_absent() {
646 const TEST: &str = r#"{ "Seq": 1 }"#;
647 let resp = serde_json::from_str::<MapResponse>(TEST).unwrap();
648 assert!(resp.ssh_policy.is_none());
649 }
650
651 /// Go marshals empty slices/maps as JSON `null` for omitempty fields, so a control plane (esp.
652 /// an IPv6-off Headscale) sends `null` for array/map fields the client modeled as required
653 /// sequences. This used to fail the netmap decode with `invalid type: null, expected a
654 /// sequence`. A `MapResponse` (and its nested peer `Node` + `DNSConfig`) with `null` everywhere
655 /// a sequence/map is expected must now deserialize, treating `null` as the empty container.
656 #[test]
657 fn null_sequences_decode_as_empty() {
658 const TEST: &str = r#"{
659 "Seq": 1,
660 "PeersChangedPatch": null,
661 "PeerSeenChange": null,
662 "OnlineChange": null,
663 "PacketFilters": null,
664 "UserProfiles": null,
665 "Peers": [
666 {
667 "ID": 2,
668 "StableID": "n2",
669 "Name": "peer.tail.ts.net.",
670 "User": 1,
671 "Addresses": ["100.64.0.2/32"],
672 "AllowedIPs": null,
673 "Endpoints": null,
674 "PrimaryRoutes": null,
675 "Capabilities": null,
676 "CapMap": null,
677 "Tags": null,
678 "ExitNodeDNSResolvers": null,
679 "Key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000"
680 }
681 ],
682 "DNSConfig": {
683 "Resolvers": [
684 { "Addr": "1.1.1.1", "BootstrapResolution": null }
685 ],
686 "Routes": null,
687 "FallbackResolvers": null,
688 "Domains": null,
689 "Nameservers": null,
690 "CertDomains": null,
691 "ExtraRecords": null,
692 "ExitNodeFilteredSet": null
693 }
694 }"#;
695
696 let resp = serde_json::from_str::<MapResponse>(TEST)
697 .expect("MapResponse with null sequences must decode");
698 let peers = resp.peers.expect("peers present");
699 assert_eq!(peers.len(), 1);
700 let peer = &peers[0];
701 // Every null array on the peer Node decoded as empty (not a parse error).
702 assert!(peer.endpoints.is_empty());
703 assert!(peer.primary_routes.is_empty());
704 assert!(peer.exit_node_dns_resolvers.is_empty());
705 assert_eq!(peer.addresses.len(), 1);
706 // MapResponse-level null containers are empty too.
707 assert!(resp.peers_changed_patch.is_empty());
708 assert!(resp.peer_seen_change.is_empty());
709 assert!(resp.user_profiles.is_empty());
710 // DNSConfig null arrays decoded as empty.
711 let dns = resp.dns_config.expect("dns_config present");
712 assert!(dns.search_domains.is_empty());
713 assert!(dns.extra_records.is_empty());
714 // A present resolver whose `BootstrapResolution` is `null` decodes with an empty list
715 // (Resolver carries its own apply block) rather than failing the whole netmap decode.
716 assert_eq!(dns.resolvers.len(), 1);
717 let resolver = dns.resolvers[0].as_ref().expect("resolver present");
718 assert!(resolver.bootstrap_resolution.is_empty());
719 }
720
721 /// `MapResponse.Health` (Go capver 24) must decode into the `health` vec. Control sends this as
722 /// a JSON array of strings; previously the field didn't exist and the warnings were dropped.
723 #[test]
724 fn health_decodes() {
725 const TEST: &str = r#"{
726 "Seq": 1,
727 "Health": ["control says hello", "second warning"]
728 }"#;
729 let resp = serde_json::from_str::<MapResponse>(TEST).expect("must decode");
730 assert_eq!(resp.health, ["control says hello", "second warning"]);
731 }
732
733 /// `MapResponse.DisplayMessages` (Go capver 117) must decode into the typed map without error,
734 /// retaining the typed `DisplayMessage` values, the `null`-valued delete sentinel, and the
735 /// `"*"` clear-all key.
736 #[test]
737 fn display_messages_decode() {
738 const TEST: &str = r#"{
739 "Seq": 1,
740 "DisplayMessages": {
741 "warning-id": {
742 "Title": "Update available",
743 "Text": "A new version is available.",
744 "Severity": "high",
745 "ImpactsConnectivity": true,
746 "PrimaryAction": {
747 "URL": "https://example.com/update",
748 "Label": "Update now"
749 }
750 },
751 "stale-id": null,
752 "*": null
753 }
754 }"#;
755 let resp = serde_json::from_str::<MapResponse>(TEST).expect("must decode");
756 assert_eq!(resp.display_messages.len(), 3);
757
758 // The typed message is retained with all fields.
759 let msg = resp
760 .display_messages
761 .get("warning-id")
762 .expect("warning-id present")
763 .as_ref()
764 .expect("warning-id has a message body");
765 assert_eq!(msg.title, "Update available");
766 assert_eq!(msg.text, "A new version is available.");
767 assert_eq!(msg.severity, "high");
768 assert!(msg.impacts_connectivity);
769 let action = msg.primary_action.as_ref().expect("primary action present");
770 assert_eq!(action.url, "https://example.com/update");
771 assert_eq!(action.label, "Update now");
772
773 // The `null` value (Go's `nil` *DisplayMessage`) is the delete sentinel and decodes to
774 // `None`, not an error.
775 assert!(
776 resp.display_messages
777 .get("stale-id")
778 .expect("present")
779 .is_none()
780 );
781 // The `"*"` clear-all key is carried (its patch semantics are applied downstream later).
782 assert!(resp.display_messages.contains_key("*"));
783 assert!(resp.display_messages.get("*").expect("present").is_none());
784 }
785
786 /// A sparse `DisplayMessage` (only some fields) and one carrying an unknown field must both
787 /// decode — Go marshals `Title`/`Text`/`Severity` unconditionally but adds fields over time, so
788 /// the struct is `default` + has no `deny_unknown_fields`.
789 #[test]
790 fn display_message_tolerant_of_sparse_and_unknown_fields() {
791 const TEST: &str = r#"{
792 "Seq": 1,
793 "DisplayMessages": {
794 "sparse": { "Title": "Just a title" },
795 "future": {
796 "Title": "t",
797 "Text": "x",
798 "Severity": "low",
799 "SomeFutureFieldGoAdded": { "nested": true }
800 }
801 }
802 }"#;
803 let resp = serde_json::from_str::<MapResponse>(TEST).expect("must decode");
804 let sparse = resp
805 .display_messages
806 .get("sparse")
807 .expect("sparse present")
808 .as_ref()
809 .expect("has body");
810 assert_eq!(sparse.title, "Just a title");
811 assert_eq!(sparse.text, "");
812 assert!(sparse.primary_action.is_none());
813
814 let future = resp
815 .display_messages
816 .get("future")
817 .expect("future present")
818 .as_ref()
819 .expect("has body");
820 assert_eq!(future.severity, "low");
821 }
822
823 /// A `DisplayMessage` whose admin-authored `Title`/`Text` and the action `URL`/`Label` contain
824 /// JSON escapes must decode (the fields are `Cow<'a, str>`). A borrowed `&str` could not decode
825 /// the escaped form and would fail the whole `MapResponse` decode, dropping the netmap. Covers
826 /// the Go-default HTML escaping (`&`→`&`) in the URL query string, plus `\n`/`\"`/`\\` in the
827 /// body prose.
828 #[test]
829 fn display_message_with_escapes_decodes() {
830 const TEST: &str = r#"{
831 "Seq": 1,
832 "DisplayMessages": {
833 "warn": {
834 "Title": "Update \"now\"",
835 "Text": "Line1\nLine2: A & B \\ C",
836 "Severity": "high",
837 "PrimaryAction": {
838 "URL": "https://example.com/fix?a=1&b=2",
839 "Label": "Fix & continue"
840 }
841 }
842 }
843 }"#;
844 let resp = serde_json::from_str::<MapResponse>(TEST)
845 .expect("DisplayMessage with escaped text/url must decode");
846 let msg = resp
847 .display_messages
848 .get("warn")
849 .expect("present")
850 .as_ref()
851 .expect("has body");
852 assert_eq!(msg.title, r#"Update "now""#);
853 assert_eq!(msg.text, "Line1\nLine2: A & B \\ C");
854 let action = msg.primary_action.as_ref().expect("action present");
855 assert_eq!(action.url, "https://example.com/fix?a=1&b=2");
856 assert_eq!(action.label, "Fix & continue");
857 }
858
859 /// `null`/absent `Health` and `DisplayMessages` decode as empty (the struct-level `apply` block
860 /// extends the existing null-slice/map tolerance to the new fields), not as a parse error.
861 #[test]
862 fn health_and_display_messages_null_decode_as_empty() {
863 const TEST: &str = r#"{
864 "Seq": 1,
865 "Health": null,
866 "DisplayMessages": null
867 }"#;
868 let resp = serde_json::from_str::<MapResponse>(TEST).expect("null must decode as empty");
869 assert!(resp.health.is_empty());
870 assert!(resp.display_messages.is_empty());
871 }
872
873 /// `MapResponse::domain` is admin/tenant-authored text typed `Cow<'a, str>` so it tolerates JSON
874 /// escapes. A bare `&'a str` cannot zero-copy-borrow a string serde must unescape and fails the
875 /// WHOLE `MapResponse` decode (`invalid type: string "...", expected a borrowed string`) — which
876 /// silently drops the netmap. With `Cow`, serde owns the unescaped value and the decode succeeds.
877 #[test]
878 fn domain_with_escape_sequence_decodes() {
879 const TEST: &str = r#"{ "Seq": 1, "Domain": "ex\n\"a\\mple.com" }"#;
880 let resp = serde_json::from_str::<MapResponse>(TEST)
881 .expect("MapResponse with an escaped Domain must decode");
882 assert_eq!(resp.domain, "ex\n\"a\\mple.com");
883 }
884
885 /// The no-escape fast path still decodes (and borrows zero-copy, though that is not observable
886 /// from outside): a plain `Domain` yields its value unchanged.
887 #[test]
888 fn domain_without_escape_decodes() {
889 const TEST: &str = r#"{ "Seq": 1, "Domain": "example.com" }"#;
890 let resp = serde_json::from_str::<MapResponse>(TEST)
891 .expect("MapResponse with a plain Domain must decode");
892 assert_eq!(resp.domain, "example.com");
893 }
894
895 /// A single peer whose `Node::name` carries a JSON escape must NOT drop the whole netmap.
896 /// `Node::name` is `Cow<'a, str>`, so an escaped peer name (here `ho\nst`, i.e. a control name
897 /// arriving with a newline) is owned-and-unescaped by serde rather than failing the peer's
898 /// `Node` decode. Before the `Cow` conversion a bare `&'a str` failed that decode with `invalid
899 /// type: string "...", expected a borrowed string`, which bubbled up and silently dropped the
900 /// ENTIRE `MapResponse` (and thus every peer) from the netmap. This proves one escaped name no
901 /// longer poisons the whole map. The peer `Node` shape mirrors `null_sequences_decode_as_empty`.
902 #[test]
903 fn peer_name_with_escape_does_not_drop_netmap() {
904 const TEST: &str = r#"{
905 "Seq": 1,
906 "Peers": [
907 {
908 "ID": 2,
909 "StableID": "n2",
910 "Name": "ho\nst.tail.ts.net.",
911 "User": 1,
912 "Addresses": ["100.64.0.2/32"],
913 "AllowedIPs": null,
914 "Endpoints": null,
915 "PrimaryRoutes": null,
916 "Capabilities": null,
917 "CapMap": null,
918 "Tags": null,
919 "ExitNodeDNSResolvers": null,
920 "Key": "nodekey:0000000000000000000000000000000000000000000000000000000000000000"
921 }
922 ]
923 }"#;
924 let resp = serde_json::from_str::<MapResponse>(TEST)
925 .expect("MapResponse with an escaped peer Name must decode (not drop the netmap)");
926 let peers = resp
927 .peers
928 .expect("peers present — the escaped name must not drop the netmap");
929 assert_eq!(peers.len(), 1);
930 assert_eq!(peers[0].name, "ho\nst.tail.ts.net.");
931 }
932}