Skip to main content

ts_capabilityversion/
lib.rs

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