10350 Commits

Author SHA1 Message Date
M. J. Fromberger
a737641b70 update more envknob usages to be default-true
Change-Id: I4b7f792ba842e15941a4280d8427ece171c63472
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-03-26 09:21:01 -07:00
M. J. Fromberger
152033f17f enable/disable/cleanup
Change-Id: I75ae3298e7e9bbf0c1f5fbb02fb2eb681d808d2f
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-03-26 09:20:51 -07:00
M. J. Fromberger
cf92a2a9a3 add discard method for the disk cache
Change-Id: I7f597e3b1ad5319c0baf82079b248be948bf4b22
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-03-26 09:20:51 -07:00
M. J. Fromberger
049ab7c0d9 tailcfg: add a NodeCapability for cache-network-maps
Updates #todo

Change-Id: I1df4dd791fdb485c6472a9f741037db6ed20c47e
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2026-03-26 09:20:51 -07:00
dependabot[bot]
b4519e97c3
.github: Bump actions/create-github-app-token from 2.2.1 to 3.0.0 (#19003)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](29824e69f5...f8d387b68d)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 10:08:56 -04:00
Fran Bull
2d5962f524 feature/conn25,ipn/ipnext,ipn/ipnlocal: add ExtraRouterConfigRoutes hook
conn25 needs to add routes to the operating system to direct handling
of the addresses in the magic IP range to the tailscale0 TUN and
tailscaled.

The way we do this for exit nodes and VIP services is that we add routes
to the Routes field of router.Config, and then the config is passed to
the WireGuard engine Reconfig.

conn25 is implemented as an ipnext.Extension and so this commit adds a
hook to ipnext.Hooks to allow any extension to provide routes to the
config. The hook if provided is called in routerConfigLocked, similarly
to exit nodes and VIP services.

Fixes tailscale/corp#38123

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-03-25 19:28:33 -07:00
Alex Valiushko
330a17b7d7
net/batching: use vectored writes on Linux (#19054)
On Linux batching.Conn will now write a vector of
coalesced buffers via sendmmsg(2) instead of copying
fragments into a single buffer.

Scatter-gather I/O has been available on Linux since the
earliest days (reworked in 2.6.24). Kernel passes fragments
to the driver if it supports it, otherwise linearizes
upon receiving the data.

Removing the copy overhead from userspace yields up to 4-5%
packet and bitrate improvement on Linux with GSO enabled:
46Gb/s 4.4m pps vs 44Gb/s 4.2m pps w/32 Peer Relay client flows.

Updates tailscale/corp#36989


Change-Id: Idb2248d0964fb011f1c8f957ca555eab6a6a6964

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
2026-03-25 16:38:54 -07:00
Patrick Guinard
18983eca66 wif: add AWS ecs for autogenerated OIDC tokens
Adds the ability to detect when running on AWS ECS and fetch tokens from
the ECS metadata endpoints in addition to IMDSv2

Fixes #18909

Signed-off-by: Patrick Guinard <patrick@public.com>
2026-03-25 14:41:41 -06:00
Nick Khyl
33da8a8d68 go.toolchain.*: bump for mips and synology segmentation violation fixes
Updates #19039
Updates tailscale/go#160
Updates tailscale/go#162
Updates golang/go#77730
Updates golang/go#77930

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2026-03-25 13:43:16 -05:00
Greg Steuck
954a2dfd31
net/dns: fix duplicate search line entries (OpenBSD, primarily)
Fixes #12360

Signed-off-by: Greg Steuck <greg@nest.cx>
2026-03-25 10:19:02 -07:00
Harry Harpham
4f43ad3042 tsnet: clean up state when Service listener is closed
Previous to this change, closing the listener returned by
Server.ListenService would free system resources, but not clean up state
in the Server's local backend. With this change, the local backend state
is now cleaned on close.

Fixes tailscale/corp#35860

Signed-off-by: Harry Harpham <harry@tailscale.com>
2026-03-25 10:16:29 -06:00
Harry Harpham
1794765cc6 tsnet: block rather than poll in setup for TestListenService
TestListenService needs to setup state (capabilities, advertised routes,
ACL tags, etc.). It is imperative that this state propagates to all nodes
in the test tailnet before proceeding with the test. To achieve this,
TestListenService currently polls each node's local backend in a loop.
Using local.Client.WatchIPNBus improves the situation by blocking until
a new netmap comes in.

Fixes tailscale/corp#36244

Signed-off-by: Harry Harpham <harry@tailscale.com>
2026-03-25 10:16:29 -06:00
Harry Harpham
47ef1a95db tsnet: use tstest.Shard in new tsnet tests
This helps us distribute tests across CI runners. Most tsnet tests call
tstest.Shard, but two recently added tests do not: tsnet.TestFunnelClose
and tsnet.TestListenService. This commit resolves the oversight.

Fixes tailscale/corp#36242

Signed-off-by: Harry Harpham <harry@tailscale.com>
2026-03-25 10:16:29 -06:00
Michael Ben-Ami
a57c6457c9 ipn/ipnlocal: debounce extra enqueues in ExtensionHost.AuthReconfigAsync
Fixes tailscale/corp#39065

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-03-25 09:11:15 -04:00
rtgnx
c026be18cc
ipn/ipnserver: use peercreds for actor.Username on freebsd (for Taildrive)
Signed-off-by: Adrian Cybulski <adrian@cybulski.cc>
2026-03-24 20:35:56 -07:00
Claus Lensbøl
9a4a2db0fc
control/controlclient: handle errors in rememberLastNetmapUpdator (#19112)
If errors occured, the updater could end up deadlocked.

Closing the done channel rather than adding to it, fixes a deadlock in
the corp tests.

Updates #19111

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-24 20:36:34 -04:00
Mike O'Driscoll
bb59942df2
types/key: use AvailableBuffer for WriteRawWithoutAllocating (#19102)
Use bufio.Writer.AvailableBuffer to write the 32-byte public key
directly into bufio's internal buffer as a single append+Write,
avoiding 32 separate WriteByte calls. Fall back to the existing
byte-at-a-time path when the buffer has insufficient space.

```
name                                old ns/op  new ns/op  speedup
NodeWriteRawWithoutAllocating-8     121        12.5       ~9.7x
(0 allocs/op in both)
```

Add BenchmarkNodeWriteRawWithoutAllocating and expand
TestNodeWriteRawWithoutAllocating to cover both fast (AvailableBuffer)
and slow (WriteByte fallback) paths with correctness and allocation
checks.

Updates tailscale/corp#38509

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2026-03-24 18:08:08 -04:00
Mike O'Driscoll
f52c1e3615
derp: use AvailableBuffer for WriteFrameHeader, consolidate tests (#19101)
Use bufio.Writer.AvailableBuffer to write the frame header directly
into bufio's internal buffer as a single append+Write, avoiding 5
separate WriteByte calls. Fall back to the existing writeUint32
byte-at-a-time path when the buffer has insufficient space.

```
name                  old ns/op  new ns/op  speedup
WriteFrameHeader-8    18.8       7.8        ~2.4x
(0 allocs/op in both)
```

Add TestWriteFrameHeader with correctness
checks, allocation assertions, and coverage of both fast and slow
write paths. Move BenchmarkReadFrameHeader from client_test.go to
derp_test.go alongside BenchmarkWriteFrameHeader, co-located with
the functions under test.

Updates tailscale/corp#38509

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2026-03-24 18:08:01 -04:00
kari-ts
9992b7c817
ipn,ipn/local: broadcast ClientVersion if AutoUpdate.Check (#19107)
If AutoUpdate.Check is false, the client has opted out of checking for updates, so we shouldn't broadcast ClientVersion. If the client has opted in, it should be included in the initial Notify.

Updates tailscale/corp#32629

Signed-off-by: kari-ts <kari@tailscale.com>
2026-03-24 15:06:20 -07:00
KevinLiang10
1e51d57cdd
ipn: fix the typo causing NoSNAT always set to true (#19110)
Fixes #19109

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
2026-03-24 16:41:58 -04:00
License Updater
066ce9a7b0 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2026-03-24 11:40:14 -07:00
Claus Lensbøl
87ec3235d9
control/controlclient: allow multiple non-streaming map requests (#19106)
A client with an active streaming session would break if using the same
client for a non-streaming session. Allow the client 1 streaming and n
non-streaming sessions at the same time.

Fixes #19105

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-24 14:19:21 -04:00
Jordan Whited
590546b17d disco: remove experimental label from BindUDPRelayHandshakeState
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-03-24 11:04:11 -07:00
Jordan Whited
f0ba1f3909 net/udprelay: remove experimental label from package docs
Update #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-03-24 10:41:17 -07:00
Fran Bull
85906b61f4 feature/conn25: call AuthReconfigAsync after address assignment
When the client of a connector assigns transit IP addresses for a
connector we need to let wireguard know that packets for the transit IPs
should be sent to the connector node. We do this by:
 * keeping a map of node -> transit IPs we've assigned for it
 * setting a callback hook within wireguard reconfig to ask us for these
   extra allowed IPs.
 * forcing wireguard to do a reconfig after we have assigned new transit
   IPs.

And this commit is the last part: forcing the wireguard reconfig after a
new address assignment.

Fixes tailscale/corp#38124

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-03-24 10:14:50 -07:00
Jordan Whited
9c36a71a90 feature/*,net/tstun: add tundev_txq_drops clientmetric on Linux
By polling RTM_GETSTATS via netlink. RTM_GETSTATS is a relatively
efficient and targeted (single device) polling method available since
Linux v4.7.

The tundevstats "feature" can be extended to other platforms in the
future, and it's trivial to add new rtnl_link_stats64 counters on
Linux.

Updates tailscale/corp#38181

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2026-03-24 09:44:58 -07:00
Michael Ben-Ami
bdcf976477 feature/conn25: guard extension Init() and PeerAPI handler with opt-in env var
Fixes tailscale/corp#39003

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-03-24 12:26:14 -04:00
Alex Chan
302e49dc4e cmd/tailscale/cli: add a debug command to print the statedir
Example:

```console
$ tailscale debug statedir
/tmp/ts/node1
```

Updates #18019

Change-Id: I7c93c94179bd7b56d0fa8fe57a9129df05c2c1df
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-03-24 15:16:43 +00:00
Mike O'Driscoll
1403920367
derp,types,util: use bufio Peek+Discard for allocation-free fast reads (#19067)
Replace byte-at-a-time ReadByte loops with Peek+Discard in the DERP
read path. Peek returns a slice into bufio's internal buffer without
allocating, and Discard advances the read pointer without copying.

Introduce util/bufiox with a BufferedReader interface and ReadFull
helper that uses Peek+copy+Discard as an allocation-free alternative
to io.ReadFull.

  - derp.ReadFrameHeader: replace 5× ReadByte with Peek(5)+Discard(5),
    reading the frame type and length directly from the peeked slice.
    Remove now-unused readUint32 helper.

    name                  old ns/op  new ns/op  speedup
    ReadFrameHeader-8     24.2       12.4       ~2x
    (0 allocs/op in both)

  - key.NodePublic.ReadRawWithoutAllocating: replace 32× ReadByte with
    bufiox.ReadFull. Addresses the "Dear future" comment about switching
    away from byte-at-a-time reads once a non-escaping alternative exists.

    name                              old ns/op  new ns/op  speedup
    NodeReadRawWithoutAllocating-8    140        43.6       ~3.2x
    (0 allocs/op in both)

  - derpserver.handleFramePing: replace io.ReadFull with bufiox.ReadFull.

Updates tailscale/corp#38509

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2026-03-24 10:52:20 -04:00
Alex Chan
1d0fde6fc2 all: use bart.Lite instead of bart.Table where appropriate
When we don't care about the payload value and are just checking whether
a set contains an IP/prefix, we can use `bart.Lite` for the same lookup
times but a lower memory footprint.

Fixes #19075

Change-Id: Ia709e8b718666cc61ea56eac1066467ae0b6e86c
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-03-24 14:45:23 +00:00
Tom Proctor
44ec71cf94
tsnet: print state change in auth loop more responsively (#18048)
tsnet has a 5s sleep as part of its logic waiting to log successful auth.
Add an additional channel that will interrupt this sleep early if the
local backend's state changes before then. This is early enough in the
bootstrap logic that the local client has not been set up yet, so we
subscribe directly on the local backend in keeping with the rest of the
function, but it would be nice to port the whole function to the new
eventbus in a separate change.

Note this does not affect how quickly auth actually happens, it just
ensures we more responsively log the fact that auth state has changed.

Updates #16340

Change-Id: I7a28fd3927bbcdead9a5aad39f4a3596b5f659b0

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2026-03-23 20:44:23 +00:00
Brendan Creane
0b4c0f2080
net/dns/resolver: treat DNS REFUSED responses as soft errors in forwarder race (#19053)
When racing multiple upstream DNS resolvers, a REFUSED (RCode 5) response
from a broken or misconfigured resolver could win the race and be returned
to the client before healthier resolvers had a chance to respond with a
valid answer. This caused complete DNS failure in cases where, e.g., a
broken upstream resolver returned REFUSED quickly while a working resolver
(such as 1.1.1.1) was still responding.

Previously, only SERVFAIL (RCode 2) was treated as a soft error. REFUSED
responses were returned as successful bytes and could win the race
immediately. This change also treats REFUSED as a soft error in the UDP
and TCP forwarding paths, so the race continues until a better answer
arrives. If all resolvers refuse, the first REFUSED response is returned
to the client.

Additionally, SERVFAIL responses from upstream resolvers are now returned
verbatim to the client rather than replaced with a locally synthesized
packet. Synthesized SERVFAIL responses were authoritative and guaranteed
to include a question section echoing the original query; upstream
responses carry no such guarantees but may include extended error
information (e.g. RFC 8914 extended DNS errors) that would otherwise
be lost.

Fixes #19024

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-03-23 10:40:05 -07:00
Amal Bansode
04ef9d80b5
ipn/ipnlocal: add a map for node public key to node ID lookups (#19051)
This path is currently only used by DERP servers that have also
enabled `verify-clients` to ensure that only authorized clients
within a Tailnet are allowed to use said DERP server.

The previous naive linear scan in NodeByKey would almost
certainly lead to bad outcomes with a large enough netmap, so
address an existing todo by building a map of node key -> node ID.

Updates #19042

Signed-off-by: Amal Bansode <amal@tailscale.com>
2026-03-23 10:23:28 -07:00
Tom Proctor
db3348fd25
.github/workflows: limit vet to the tailscale.com module (#19084)
This repo's module is tailscale.com, and the tailscale-client-go-v2 repo
uses tailscale.com/client/tailscale/v2. It seems from #19010 that if we
have the client module as a dependency in this module, go vet will start
to consider the client module as part of tailscale.com/...

I'm not sure if this is a bug in go vet, but for now let's take the easy
fix and specify ./... instead. In my testing, it seems like this is
sufficient to make sure it just walks the file hierarchy and doesn't
find the client module as a sub-path.

Updates tailscale/corp#38418

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2026-03-23 16:56:08 +00:00
dependabot[bot]
18528d1dd9 .github: Bump github/codeql-action from 4.32.6 to 4.34.1
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.6 to 4.34.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](0d579ffd05...3869755554)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 15:53:31 +00:00
Fran Bull
d3626c51f1 feature/conn25: add packet filter allow functions
That will be able to be plugged into the hooks in
wgengine/filter/filter.go to let connector packets flow.

Fixes tailscale/corp#37144
Fixes tailscale/corp#37145

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-03-23 08:40:58 -07:00
Alex Chan
67496e14c6 cmd/tailscale/cli: fix a typo in the whois help text
Updates #cleanup

Change-Id: I739052548b81a94c4e4997d15883ee755c57df3c
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-03-23 15:05:11 +00:00
Nahum Shalman
1d6ecb1e51
safesocket, ipn/ipnserver: use PeerCreds on solaris and illumos
Updates tailscale/peercred#10

Signed-off-by: Nahum Shalman <nahamu@gmail.com>
2026-03-23 07:45:35 -07:00
Charlie Tonneslan
43782601d0 util/osdiag: fix typo in comment (reciever -> receiver)
Signed-off-by: Charlie Tonneslan <cst0520@gmail.com>
2026-03-23 12:54:38 +00:00
jpelchat
323e0f87f9
docs/windows/policy: add CheckUpdates key to tailscale.admx (#19044)
Fixes: #19014
Signed-off-by: Jacob Pelchat <jacob@tailscale.com>
2026-03-23 08:42:45 -04:00
dependabot[bot]
6e5a64d4de .github: Bump actions/cache from 5.0.3 to 5.0.4
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](cdf6c1fa76...668228422a)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-23 12:34:15 +00:00
Alex Chan
34267d5afa cmd/tailscale: print a helpful error for Taildrive CLI on macOS GUI
Rather than printing `unknown subcommand: drive` for any Taildrive
commands run in the macOS GUI, print an error message directing the user
to the GUI client and the docs page.

Updates #17210
Fixes #18823

Change-Id: I6435007b5911baee79274b56e3ee101e6bb6d809
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-03-23 09:27:27 +00:00
Prakash Rudraraju
931fe56586 tsnet: fall back to 'tsnet' when os.Executable fails on darwin
Updates #19050

When tsnet.Server.start() is called with both Hostname and Dir explicitly
set, os.Executable() failure should not prevent the server from starting.
Extend the existing ios fallback to also cover darwin, where the same
failure occurs when the Go runtime is embedded in a framework launched
via Xcode's debug launcher.

Signed-off-by: Prakash Rudraraju <prakashrj@yahoo.com>
2026-03-20 19:15:25 -07:00
Michael Ben-Ami
ea7040eea2 ipn/{ipnext,ipnlocal}: expose authReconfig in ipnext.Host as AuthReconfigAsync
Also implement a limit of one on the number of goroutines that can be
waiting to do a reconfig via AuthReconfig, to prevent extensions from
calling too fast and taxing resources.

Even with the protection, the new method should only be used in
experimental or proof-of-concept contexts. The current intended use is
for an extension to be able force a reconfiguration of WireGuard, and
have the reconfiguration call back into the extension for extra Allowed
IPs.

If in the future if WireGuard is able to reconfigure individual peers more
dynamically, an extension might be able to hook into that process, and
this method on ipnext.Host may be deprecated.

Fixes tailscale/corp#38120
Updates tailscale/corp#38124
Updates tailscale/corp#38125

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
2026-03-20 17:29:11 -04:00
Andrew Lytvynov
3a5afc3358
feature/conn25: guard against an index out of bounds panic (#19066)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2026-03-20 11:44:54 -07:00
Andrew Lytvynov
34477cf3e7
tka: use constant-time comparison of disablement secret (#19064)
The actual secret is passed through argon2 first, so a timing attack is
not feasible remotely, and pretty unlikely locally. Still, clean this
up.

Fixes #19063

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2026-03-20 11:30:26 -07:00
Brendan Creane
ffa7df2789
ipn: reject advertised routes with non-address bits set (#18649)
* ipn: reject advertised routes with non-address bits set

The config file path, EditPrefs local API, and App Connector API were
accepting invalid subnet route prefixes with non-address bits set (e.g.,
2a01:4f9:c010:c015::1/64 instead of 2a01:4f9:c010:c015::/64). All three
paths now reject prefixes where prefix != prefix.Masked() with an error
message indicating the expected masked form.

Updates tailscale/corp#36738

Signed-off-by: Brendan Creane <bcreane@gmail.com>

* address review comments

Signed-off-by: Brendan Creane <bcreane@gmail.com>

---------

Signed-off-by: Brendan Creane <bcreane@gmail.com>
2026-03-20 10:10:43 -07:00
Fran Bull
79f71beb24 feature/conn25: implement IPMapper
Rename variables to match their types after the server -> connector
rename.

Updates tailscale/corp#37144
Updates tailscale/corp#37145

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-03-20 08:31:14 -07:00
Fran Bull
1e09eb0cb6 feature/conn25: implement IPMapper
Give the datapath hooks the lookup functions they need.

Updates tailscale/corp#37144
Updates tailscale/corp#37145

Signed-off-by: Fran Bull <fran@tailscale.com>
2026-03-20 08:31:14 -07:00
Claus Lensbøl
85bb5f84a5
wgengine/magicsock,control/controlclient: do not overwrite discokey with old key (#18606)
When a client starts up without being able to connect to control, it
sends its discoKey to other nodes it wants to communicate with over
TSMP. This disco key will be a newer key than the one control knows
about.

If the client that can connect to control gets a full netmap, ensure
that the disco key for the node not connected to control is not
overwritten with the stale key control knows about.

This is implemented through keeping track of mapSession and use that for
the discokey injection if it is available. This ensures that we are not
constantly resetting the wireguard connection when getting the wrong
keys from control.

This is implemented as:
 - If the key is received via TSMP:
   - Set lastSeen for the peer to now()
   - Set online for the peer to false
 - When processing new keys, only accept keys where either:
   - Peer is online
   - lastSeen is newer than existing last seen

If mapSession is not available, as in we are not yet connected to
control, punt down the disco key injection to magicsock.

Ideally, we will want to have mapSession be long lived at some point in
the near future so we only need to inject keys in one location and then
also use that for testing and loading the cache, but that is a yak for
another PR.

Updates #12639

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2026-03-20 08:56:27 -04:00