Skip to main content

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