1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
#![doc = include_str!("../README.md")]
use core::fmt;
/// Indicates the capability level of a Tailscale node. This must be kept in-sync with the
/// CapabilityVersion constants in the Golang codebase (`tailcfg/tailcfg.go`).
///
/// It can be thought of as a node's simple version number; a single monotonically increasing
/// integer, rather than the complex x.y.z-cccccc semver+hash(es) versioning scheme. Whenever a
/// node gains a capability or wants to negotiate a change in semantics with the control plane,
/// peers, or an official frontend (such as LocalAPI in the Golang codebase), bump this number and
/// document what's changed.
///
/// The capability versions 0, 1, 2, and 35 are undefined; you cannot create a
/// [`CapabilityVersion`] with these values.
///
/// Note: Prior to 2022-03-06, this value was known as the "`MapRequest` version", `mapVer`, or "map
/// cap"; you'll still see that name used in comments throughout the Golang codebase.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CapabilityVersion(u16);
impl Default for CapabilityVersion {
#[inline]
fn default() -> Self {
Self::CURRENT
}
}
impl CapabilityVersion {
/// 2020-??-??: implicit compression, keep-alives
pub const V3: Self = Self(3);
/// 2020-??-??: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
pub const V4: Self = Self(4);
/// 2020-10-19: implies IncludeIPv6; delta Peers/UserProfiles, supports MagicDNS
pub const V5: Self = Self(5);
/// 2020-12-07: means MapResponse.PacketFilter nil means unchanged
pub const V6: Self = Self(6);
/// 2020-12-15: FilterRule.SrcIPs accepts CIDRs+ranges, doesn't warn about 0.0.0.0/::
pub const V7: Self = Self(7);
/// 2020-12-19: client can buggily receive IPv6 addresses and routes if beta enabled server-side
pub const V8: Self = Self(8);
/// 2020-12-30: client doesn't auto-add implicit search domains from peers; only
/// DNSConfig.Domains
pub const V9: Self = Self(9);
/// 2021-01-17: client understands MapResponse.PeerSeenChange
pub const V10: Self = Self(10);
/// 2021-03-03: client understands IPv6; multiple default routes, and goroutine dumping
pub const V11: Self = Self(11);
/// 2021-03-04: client understands PingRequest
pub const V12: Self = Self(12);
/// 2021-03-19: client understands FilterRule.IPProto
pub const V13: Self = Self(13);
/// 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers
pub const V14: Self = Self(14);
/// 2021-04-12: client treats nil MapResponse.DNSConfig as meaning unchanged
pub const V15: Self = Self(15);
/// 2021-04-15: client understands Node.Online, MapResponse.OnlineChange
pub const V16: Self = Self(16);
/// 2021-04-18: MapResponse.Domain empty means unchanged
pub const V17: Self = Self(17);
/// 2021-04-19: MapResponse.Node nil means unchanged (all fields now omitempty)
pub const V18: Self = Self(18);
/// 2021-04-21: MapResponse.Debug.SleepSeconds
pub const V19: Self = Self(19);
/// 2021-06-11: MapResponse.LastSeen used even less
/// (<https://github.com/tailscale/tailscale/issues/2107>)
pub const V20: Self = Self(20);
/// 2021-06-15: added MapResponse.DNSConfig.CertDomains
pub const V21: Self = Self(21);
/// 2021-06-16: added MapResponse.DNSConfig.ExtraRecords
pub const V22: Self = Self(22);
/// 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
pub const V23: Self = Self(23);
/// 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
pub const V24: Self = Self(24);
/// 2021-11-01: MapResponse.Debug.Exit
pub const V25: Self = Self(25);
/// 2022-01-12: (nothing, just bumping for 1.20.0)
pub const V26: Self = Self(26);
/// 2022-02-18: start of SSHPolicy being respected
pub const V27: Self = Self(27);
/// 2022-03-09: client can communicate over Noise.
pub const V28: Self = Self(28);
/// 2022-03-21: MapResponse.PopBrowserURL
pub const V29: Self = Self(29);
/// 2022-03-22: client can request id tokens.
pub const V30: Self = Self(30);
/// 2022-04-15: PingRequest & PingResponse TSMP & disco support
pub const V31: Self = Self(31);
/// 2022-04-17: client knows FilterRule.CapMatch
pub const V32: Self = Self(32);
/// 2022-07-20: added MapResponse.PeersChangedPatch (DERPRegion + Endpoints)
pub const V33: Self = Self(33);
/// 2022-08-02: client understands CapabilityFileSharingTarget
pub const V34: Self = Self(34);
/// 2022-08-02: added PeersChangedPatch.{Key,DiscoKey,Online,LastSeen,KeyExpiry,Capabilities}
pub const V36: Self = Self(36);
/// 2022-08-09: added Debug.{SetForceBackgroundSTUN,SetRandomizeClientPort}; Debug are sticky
pub const V37: Self = Self(37);
/// 2022-08-11: added PingRequest.URLIsNoise
pub const V38: Self = Self(38);
/// 2022-08-15: clients can talk Noise over arbitrary HTTPS port
pub const V39: Self = Self(39);
/// 2022-08-22: added Node.KeySignature, PeersChangedPatch.KeySignature
pub const V40: Self = Self(40);
/// 2022-08-30: uses 100.100.100.100 for route-less ExtraRecords if global nameservers is set
pub const V41: Self = Self(41);
/// 2022-09-06: NextDNS DoH support; see <https://github.com/tailscale/tailscale/pull/5556>
pub const V42: Self = Self(42);
/// 2022-09-21: clients can return usernames for SSH
pub const V43: Self = Self(43);
/// 2022-09-22: MapResponse.ControlDialPlan
pub const V44: Self = Self(44);
/// 2022-09-26: c2n /debug/{goroutines,prefs,metrics}
pub const V45: Self = Self(45);
/// 2022-10-04: c2n /debug/component-logging
pub const V46: Self = Self(46);
/// 2022-10-11: Register{Request,Response}.NodeKeySignature
pub const V47: Self = Self(47);
/// 2022-11-02: Node.UnsignedPeerAPIOnly
pub const V48: Self = Self(48);
/// 2022-11-03: Client understands EarlyNoise
pub const V49: Self = Self(49);
/// 2022-11-14: Client understands CapabilityIngress
pub const V50: Self = Self(50);
/// 2022-11-30: Client understands CapabilityTailnetLockAlpha
pub const V51: Self = Self(51);
/// 2023-01-05: client can handle c2n POST /logtail/flush
pub const V52: Self = Self(52);
/// 2023-01-18: client respects explicit Node.Expired + auto-sets based on Node.KeyExpiry
pub const V53: Self = Self(53);
/// 2023-01-19: Node.Cap added, PeersChangedPatch.Cap, uses Node.Cap for ExitDNS before
/// Hostinfo.Services fallback
pub const V54: Self = Self(54);
/// 2023-01-23: start of c2n GET+POST /update handler
pub const V55: Self = Self(55);
/// 2023-01-24: Client understands CapabilityDebugTSDNSResolution
pub const V56: Self = Self(56);
/// 2023-01-25: Client understands CapabilityBindToInterfaceByRoute
pub const V57: Self = Self(57);
/// 2023-03-10: Client retries lite map updates before restarting map poll.
pub const V58: Self = Self(58);
/// 2023-03-16: Client understands Peers[].SelfNodeV4MasqAddrForThisPeer
pub const V59: Self = Self(59);
/// 2023-04-06: Client understands IsWireGuardOnly
pub const V60: Self = Self(60);
/// 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
pub const V61: Self = Self(61);
/// 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording
/// failure events
pub const V62: Self = Self(62);
/// 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding.
pub const V63: Self = Self(63);
/// 2023-07-11: Client understands s/CapabilityTailnetLockAlpha/CapabilityTailnetLock
pub const V64: Self = Self(64);
/// 2023-07-12: Client understands DERPMap.HomeParams + incremental DERPMap updates with params
pub const V65: Self = Self(65);
/// 2023-07-23: UserProfile.Groups added (available via WhoIs) (removed in 87)
pub const V66: Self = Self(66);
/// 2023-07-25: Client understands PeerCapMap
pub const V67: Self = Self(67);
/// 2023-08-09: Client has dedicated updateRoutine; MapRequest.Stream true means ignore
/// Hostinfo+Endpoints
pub const V68: Self = Self(68);
/// 2023-08-16: removed Debug.LogHeap* + GoroutineDumpURL; added c2n /debug/logheap
pub const V69: Self = Self(69);
/// 2023-08-16: removed most Debug fields; added NodeAttrDisable*, NodeAttrDebug* instead
pub const V70: Self = Self(70);
/// 2023-08-17: added NodeAttrOneCGNATEnable, NodeAttrOneCGNATDisable
pub const V71: Self = Self(71);
/// 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again
pub const V72: Self = Self(72);
/// 2023-09-01: Non-Windows clients expect to receive ClientVersion
pub const V73: Self = Self(73);
/// 2023-09-18: Client understands NodeCapMap
pub const V74: Self = Self(74);
/// 2023-09-12: Client understands NodeAttrDNSForwarderDisableTCPRetries
pub const V75: Self = Self(75);
/// 2023-09-20: Client understands ExitNodeDNSResolvers for IsWireGuardOnly nodes
pub const V76: Self = Self(76);
/// 2023-10-03: Client understands Peers[].SelfNodeV6MasqAddrForThisPeer
pub const V77: Self = Self(77);
/// 2023-10-05: can handle c2n Wake-on-LAN sending
pub const V78: Self = Self(78);
/// 2023-10-05: Client understands UrgentSecurityUpdate in ClientVersion
pub const V79: Self = Self(79);
/// 2023-11-16: can handle c2n GET /tls-cert-status
pub const V80: Self = Self(80);
/// 2023-11-17: MapResponse.PacketFilters (incremental packet filter updates)
pub const V81: Self = Self(81);
/// 2023-12-01: Client understands NodeAttrLinuxMustUseIPTables, NodeAttrLinuxMustUseNfTables,
/// c2n /netfilter-kind
pub const V82: Self = Self(82);
/// 2023-12-18: Client understands DefaultAutoUpdate
pub const V83: Self = Self(83);
/// 2024-01-04: Client understands SeamlessKeyRenewal
pub const V84: Self = Self(84);
/// 2024-01-05: Client understands MaxKeyDuration
pub const V85: Self = Self(85);
/// 2024-01-23: Client understands NodeAttrProbeUDPLifetime
pub const V86: Self = Self(86);
/// 2024-02-11: UserProfile.Groups removed (added in 66)
pub const V87: Self = Self(87);
/// 2024-03-05: Client understands NodeAttrSuggestExitNode
pub const V88: Self = Self(88);
/// 2024-03-23: Client no longer respects deleted PeerChange.Capabilities (use CapMap)
pub const V89: Self = Self(89);
/// 2024-04-03: Client understands PeerCapabilityTaildrive.
pub const V90: Self = Self(90);
/// 2024-04-24: Client understands PeerCapabilityTaildriveSharer.
pub const V91: Self = Self(91);
/// 2024-05-06: Client understands NodeAttrUserDialUseRoutes.
pub const V92: Self = Self(92);
/// 2024-05-06: added support for stateful firewalling.
pub const V93: Self = Self(93);
/// 2024-05-06: Client understands Node.IsJailed.
pub const V94: Self = Self(94);
/// 2024-05-06: Client uses NodeAttrUserDialUseRoutes to change DNS dialing behavior.
pub const V95: Self = Self(95);
/// 2024-05-29: Client understands NodeAttrSSHBehaviorV1
pub const V96: Self = Self(96);
/// 2024-06-06: Client understands NodeAttrDisableSplitDNSWhenNoCustomResolvers
pub const V97: Self = Self(97);
/// 2024-06-13: iOS/tvOS clients may provide serial number as part of posture information
pub const V98: Self = Self(98);
/// 2024-06-14: Client understands NodeAttrDisableLocalDNSOverrideViaNRPT
pub const V99: Self = Self(99);
/// 2024-06-18: Initial support for filtertype.Match.SrcCaps - actually usable in capver 109
/// (issue #12542)
pub const V100: Self = Self(100);
/// 2024-07-01: Client supports SSH agent forwarding when handling connections with /bin/su
pub const V101: Self = Self(101);
/// 2024-07-12: NodeAttrDisableMagicSockCryptoRouting support
pub const V102: Self = Self(102);
/// 2024-07-24: Client supports NodeAttrDisableCaptivePortalDetection
pub const V103: Self = Self(103);
/// 2024-08-03: SelfNodeV6MasqAddrForThisPeer now works
pub const V104: Self = Self(104);
/// 2024-08-05: Fixed SSH behavior on systems that use busybox (issue #12849)
pub const V105: Self = Self(105);
/// 2024-09-03: fix panic regression from cryptokey routing change (65fe0ba7b5)
pub const V106: Self = Self(106);
/// 2024-10-30: add App Connector to conffile (PR #13942)
pub const V107: Self = Self(107);
/// 2024-11-08: Client sends ServicesHash in Hostinfo, understands c2n GET /vip-services.
pub const V108: Self = Self(108);
/// 2024-11-18: Client supports filtertype.Match.SrcCaps (issue #12542)
pub const V109: Self = Self(109);
/// 2024-12-12: removed never-before-used Tailscale SSH public key support (#14373)
pub const V110: Self = Self(110);
/// 2025-01-14: Client supports a peer having Node.HomeDERP (issue #14636)
pub const V111: Self = Self(111);
/// 2025-01-14: Client interprets AllowedIPs of nil as meaning same as Addresses
pub const V112: Self = Self(112);
/// 2025-01-20: Client communicates to control whether funnel is enabled by sending
/// Hostinfo.IngressEnabled (#14688)
pub const V113: Self = Self(113);
/// 2025-01-30: NodeAttrMaxKeyDuration CapMap defined, clients might use it (no tailscaled code
/// change) (#14829)
pub const V114: Self = Self(114);
/// 2025-03-07: Client understands DERPRegion.NoMeasureNoHome.
pub const V115: Self = Self(115);
/// 2025-05-05: Client serves MagicDNS "AAAA" if NodeAttrMagicDNSPeerAAAA set on self node
pub const V116: Self = Self(116);
/// 2025-05-28: Client understands DisplayMessages (structured health messages), but not
/// necessarily PrimaryAction.
pub const V117: Self = Self(117);
/// 2025-07-01: Client sends Hostinfo.StateEncrypted to report whether the state file is
/// encrypted at rest (#15830)
pub const V118: Self = Self(118);
/// 2025-07-10: Client uses Hostinfo.Location.Priority to prioritize one route over another.
pub const V119: Self = Self(119);
/// 2025-07-15: Client understands peer relay disco messages, and implements peer client and
/// relay server functions
pub const V120: Self = Self(120);
/// 2025-07-19: Client understands peer relay endpoint alloc with
/// `disco.AllocateUDPRelayEndpointRequest` & `disco.AllocateUDPRelayEndpointResponse`
pub const V121: Self = Self(121);
/// 2025-07-21: Client sends Hostinfo.ExitNodeID to report which exit node it has selected, if any.
pub const V122: Self = Self(122);
/// 2025-07-28: fix deadlock regression from cryptokey routing change (issue #16651)
pub const V123: Self = Self(123);
/// 2025-08-08: removed NodeAttrDisableMagicSockCryptoRouting support, crypto routing is now mandatory
pub const V124: Self = Self(124);
/// 2025-08-11: dnstype.Resolver adds UseWithExitNode field.
pub const V125: Self = Self(125);
/// 2025-09-17: Client uses seamless key renewal unless disabled by control
/// (tailscale/corp#31479)
pub const V126: Self = Self(126);
/// 2025-09-19: can handle C2N /debug/netmap.
pub const V127: Self = Self(127);
/// 2025-10-02: can handle C2N /debug/health.
pub const V128: Self = Self(128);
/// 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449)
pub const V129: Self = Self(129);
/// 2025-10-06: client can send key.HardwareAttestationPublic and
/// key.HardwareAttestationKeySignature in MapRequest
pub const V130: Self = Self(130);
/// 2025-11-25: client respects NodeAttrDefaultAutoUpdate
pub const V131: Self = Self(131);
/// 2026-02-13: client respects NodeAttrDisableHostsFileUpdates
pub const V132: Self = Self(132);
/// 2026-02-17: client understands NodeAttrForceRegisterMagicDNSIPv4Only; MagicDNS IPv6
/// registered w/ OS by default
pub const V133: Self = Self(133);
/// The capability version this node declares to control.
///
/// **This is a claim control acts on, so it is pinned to the highest version whose behaviour
/// this tree actually implements — not to the newest constant defined above.** The constants
/// above are a transcription of upstream's list (they document what each version *means*);
/// this one is a promise. Declaring a version whose behaviour is missing does not gain a
/// feature, it just tells control to stop sending the compatibility path for it.
///
/// The declaration is held at **125** because the next three versions each name a behaviour
/// this tree does not have:
///
/// - **126** — seamless key renewal. Go renews the node key ahead of expiry without breaking
/// connections (`controlknobs.Knobs.SeamlessKeyRenewal`). This tree's expiry recovery is a
/// node-key rotation plus a full re-register with the stored auth key
/// (`ts_control::Config::reauth_on_expiry`, Go `doLogin`) — that is upstream's *non*-seamless
/// path, so connections do break across a renewal.
/// - **127** — c2n `GET|POST /debug/netmap` (Go `handleC2NDebugNetMap`,
/// `ipn/ipnlocal/c2n.go`). Needs a `netmap.NetworkMap` aggregate to marshal; this tree has
/// none — the netmap arrives as a stream of deltas accumulated by the runtime's peer tracker.
/// - **128** — c2n `/debug/health` (Go `handleC2NDebugHealth`, `ipn/ipnlocal/c2n.go`). Needs a
/// `health.Tracker`; this fork has no health subsystem.
///
/// and **138** (c2n `/debug/tka/log`, Go `handleC2NDebugTKALog` in
/// `feature/tailnetlock/tailnetlock.go`) is unimplemented for the same reason: the AUM chain
/// lives in `ts_runtime`, and the c2n responder in `ts_control` cannot reach it. All three
/// paths take the responder's `400 Bad Request` / `unknown c2n path` fallthrough, which is
/// exactly what Go's `handleC2N` writes for an unregistered path.
///
/// 125 is also a version a real client shipped (Tailscale `v1.88.0` declares it), which keeps
/// this coherent with the `IPNVersion` advertised in `ts_control::hostinfo`.
///
/// **Before raising this**, check that every version between the old value and the new one is
/// implemented here, not just the topmost one — capability versions are monotone, so declaring
/// `N` asserts everything at or below `N`.
pub const CURRENT: Self = Self::V125;
/// Create a new [`CapabilityVersion`] instance from a `u16`. Note that the versions 0, 1, 2,
/// and 35 are undefined and will result in an error.
pub const fn new(value: u16) -> Option<Self> {
match value {
v if v < 3 || v == 35 => None,
_ => Some(Self(value)),
}
}
}
impl fmt::Display for CapabilityVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Convert [`CapabilityVersion`] into a native-endian [`u16`]; little-endian on `x86_64`.
impl From<CapabilityVersion> for u16 {
fn from(value: CapabilityVersion) -> Self {
value.0
}
}
impl TryFrom<u16> for CapabilityVersion {
type Error = ();
fn try_from(value: u16) -> Result<Self, Self::Error> {
Self::new(value).ok_or(())
}
}
#[cfg(test)]
mod tests {
use super::CapabilityVersion;
/// The declared version must stay below the first capability this tree does not implement.
/// 126 (seamless key renewal), 127 (c2n `/debug/netmap`) and 128 (c2n `/debug/health`) are all
/// unimplemented, so 125 is the ceiling; see [`CapabilityVersion::CURRENT`] for the evidence
/// behind each. This test exists so raising the constant without doing the port fails here
/// rather than silently on the wire.
#[test]
fn current_is_not_declared_above_what_is_implemented() {
assert_eq!(CapabilityVersion::CURRENT, CapabilityVersion::V125);
assert!(CapabilityVersion::CURRENT < CapabilityVersion::V126);
}
}