Compare commits

...

751 Commits

Author SHA1 Message Date
Brad Fitzpatrick 72f736134d cmd/testwrapper/flakytest: skip flaky tests if TS_SKIP_FLAKY_TESTS set
This is for a future test scheduler, so it can run potentially flaky
tests separately, doing all the non-flaky ones together in one batch.

Updates tailscale/corp#28679

Change-Id: Ic4a11f9bf394528ef75792fd622f17bc01a4ec8a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 hours ago
Andrew Lytvynov d7d12761ba
Add .stignore for syncthing (#18540)
This symlink tells synchting to ignore stuff that's in .gitignore.

Updates https://github.com/tailscale/corp/issues/36250

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
21 hours ago
Brad Fitzpatrick 8f8236feb3 cmd/printdep: add --next flag to use rc Go build hash instead
Updates tailscale/corp#36382

Change-Id: Ib7474b0aab901e98f0fe22761e26fd181650743c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
23 hours ago
Brad Fitzpatrick a374cc344e tool/gocross, pull-toolchain.sh: support a "next" Go toolchain
When TS_GO_NEXT=1 is set, update/use the
go.toolchain.next.{branch,rev} files instead.

This lets us do test deploys of Go release candidates on some
backends, without affecting all backends.

Updates tailscale/corp#36382

Change-Id: I00dbde87b219b720be5ea142325c4711f101a364
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
23 hours ago
Cameron Stokes aac12ba799
cmd/tailscale/cli: add json output option to `switch --list` (#18501)
* cmd/tailscale/cli: add json output option to `switch --list`

Closes #14783

Signed-off-by: Cameron Stokes <cameron@tailscale.com>
24 hours ago
Alex Chan ae62569159 hostinfo: retrieve OS version for Macs running the OSS client
Updates #18520

Change-Id: If86a1f702c704b003002aa7e2f5a6b1418b469cc
Signed-off-by: Alex Chan <alexc@tailscale.com>
1 day ago
Amal Bansode 6de5b01e04
ipn/localapi: stop logging "broken pipe" errors (#18487)
The Tailscale CLI has some methods to watch the IPN bus for
messages, say, the current netmap (`tailscale debug netmap`).
The Tailscale daemon supports this using a streaming HTTP
response. Sometimes, the client can close its connection
abruptly -- due to an interruption, or in the case of `debug netmap`,
intentionally after consuming one message.

If the server daemon is writing a response as the client closes
its end of the socket, the daemon typically encounters a "broken pipe"
error. The "Watch IPN Bus" handler currently logs such errors after
they're propagated by a JSON encoding/writer helper.

Since the Tailscale CLI nominally closes its socket with the daemon
in this slightly ungraceful way (viz. `debug netmap`), stop logging
these broken pipe errors as far as possible. This will help avoid
confounding users when they scan backend logs.

Updates #18477

Signed-off-by: Amal Bansode <amal@tailscale.com>
2 days ago
M. J. Fromberger 9385dfe7f6
ipn/ipnlocal/netmapcache: add a package to split and cache network maps (#18497)
This commit is based on part of #17925, reworked as a separate package.

Add a package that can store and load netmap.NetworkMap values in persistent
storage, using a basic columnar representation. This commit includes a default
storage interface based on plain files, but the interface can be implemented
with more structured storage if we want to later.

The tests are set up to require that all the fields of the NetworkMap are
handled, except those explicitly designated as not-cached, and check that a
fully-populated value can round-trip correctly through the cache.  Adding or
removing fields, either in the NetworkMap or in the cached representation, will
trigger either build failures (e.g., for type mismatch) or test failures (e.g.,
for representation changes or missing fields). This isn't quite as nice as
automatically updating the representation, which I also prototyped, but is much
simpler to maintain and less code.

This commit does not yet hook up the cache to the backend, that will be a
subsequent change.

Updates #12639

Change-Id: Icb48639e1d61f2aec59904ecd172c73e05ba7bf9
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
2 days ago
Harry Harpham 6e44cb6ab3 tsnet: make ListenService examples consistent with other tsnet examples
Fixes tailscale/corp#36365

Signed-off-by: Harry Harpham <harry@tailscale.com>
2 days ago
Andrew Dunham 8d875a301c net/dns: add test for DoH upgrade of system DNS
Someone asked me if we use DNS-over-HTTPS if the system's resolver is an
IP address that supports DoH and there's no global nameserver set (i.e.
no "Override DNS servers" set). I didn't know the answer offhand, and it
took a while for me to figure it out. The answer is yes, in cases where
we take over the system's DNS configuration and read the base config, we
do upgrade any DoH-capable resolver to use DoH. Here's a test that
verifies this behaviour (and hopefully helps as documentation the next
time someone has this question).

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2 days ago
Andrew Dunham 0e1b2b15f1 net/dns/publicdns: support CIRA Canadian Shield
RELNOTE=Add DNS-over-HTTPS support for CIRA Canadian Shield

Fixes #18524

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2 days ago
Fran Bull 9d13a6df9c appc,ipn/ipnlocal: Add split DNS entries for conn25 peers
If conn25 config is sent in the netmap: add split DNS entries to use
appropriately tagged peers' PeerAPI to resolve DNS requests for those
domains.

This will enable future work where we use the peers as connectors for
the configured domains.

Updates tailscale/corp#34252

Signed-off-by: Fran Bull <fran@tailscale.com>
2 days ago
James Tucker 1183f7a191 tstest/integration/testcontrol: fix unguarded read of DNS config
Fixes #18498

Signed-off-by: James Tucker <james@tailscale.com>
4 days ago
License Updater 76839587eb licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
5 days ago
Andrew Lytvynov bfa90ea9b3
go.toolchain.rev: update to Go 1.25.6 (#18507)
Updates #18506

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
5 days ago
Nick Khyl 2a69f48541 wf: allow limited broadcast to/from permitted interfaces when using an exit node on Windows
Similarly to allowing link-local multicast in #13661, we should also allow broadcast traffic
on permitted interfaces when the killswitch is enabled due to exit node usage on Windows.
This always includes internal interfaces, such as Hyper-V/WSL2, and also the LAN when
"Allow local network access" is enabled in the client.

Updates #18504

Signed-off-by: Nick Khyl <nickk@tailscale.com>
5 days ago
Will Norris 3ec5be3f51 all: remove AUTHORS file and references to it
This file was never truly necessary and has never actually been used in
the history of Tailscale's open source releases.

A Brief History of AUTHORS files
---

The AUTHORS file was a pattern developed at Google, originally for
Chromium, then adopted by Go and a bunch of other projects. The problem
was that Chromium originally had a copyright line only recognizing
Google as the copyright holder. Because Google (and most open source
projects) do not require copyright assignemnt for contributions, each
contributor maintains their copyright. Some large corporate contributors
then tried to add their own name to the copyright line in the LICENSE
file or in file headers. This quickly becomes unwieldy, and puts a
tremendous burden on anyone building on top of Chromium, since the
license requires that they keep all copyright lines intact.

The compromise was to create an AUTHORS file that would list all of the
copyright holders. The LICENSE file and source file headers would then
include that list by reference, listing the copyright holder as "The
Chromium Authors".

This also become cumbersome to simply keep the file up to date with a
high rate of new contributors. Plus it's not always obvious who the
copyright holder is. Sometimes it is the individual making the
contribution, but many times it may be their employer. There is no way
for the proejct maintainer to know.

Eventually, Google changed their policy to no longer recommend trying to
keep the AUTHORS file up to date proactively, and instead to only add to
it when requested: https://opensource.google/docs/releasing/authors.
They are also clear that:

> Adding contributors to the AUTHORS file is entirely within the
> project's discretion and has no implications for copyright ownership.

It was primarily added to appease a small number of large contributors
that insisted that they be recognized as copyright holders (which was
entirely their right to do). But it's not truly necessary, and not even
the most accurate way of identifying contributors and/or copyright
holders.

In practice, we've never added anyone to our AUTHORS file. It only lists
Tailscale, so it's not really serving any purpose. It also causes
confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header
in other open source repos which don't actually have an AUTHORS file, so
it's ambiguous what that means.

Instead, we just acknowledge that the contributors to Tailscale (whoever
they are) are copyright holders for their individual contributions. We
also have the benefit of using the DCO (developercertificate.org) which
provides some additional certification of their right to make the
contribution.

The source file changes were purely mechanical with:

    git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g'

Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
5 days ago
M. J. Fromberger ce12863ee5
ipn/ipnlocal: manage per-profile subdirectories in TailscaleVarRoot (#18485)
In order to better manage per-profile data resources on the client, add methods
to the LocalBackend to support creation of per-profile directory structures in
local storage. These methods build on the existing TailscaleVarRoot config, and
have the same limitation (i.e., if no local storage is available, it will
report an error when used).

The immediate motivation is to support netmap caching, but we can also use this
mechanism for other per-profile resources including pending taildrop files and
Tailnet Lock authority caches.

This commit only adds the directory-management plumbing; later commits will
handle migrating taildrop, TKA, etc. to this mechanism, as well as caching
network maps.

Updates #12639

Change-Id: Ia75741955c7bf885e49c1ad99f856f669a754169
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 days ago
Francois Marier df54751725
scripts/installer.sh: allow running dnf5 install script twice (#18492)
`dnf config-manager addrepo` will fail if the Tailscale repo is already
installed. Without the --overwrite flag, the installer will error out
instead of succeeding like with dnf3.

Fixes #18491

Signed-off-by: Francois Marier <francois@fmarier.org>
5 days ago
James Tucker 63d563e734 tsnet: add support for a user-supplied tun.Device
tsnet users can now provide a tun.Device, including any custom
implementation that conforms to the interface.

netstack has a new option CheckLocalTransportEndpoints that when used
alongside a TUN enables netstack listens and dials to correctly capture
traffic associated with those sockets. tsnet with a TUN sets this
option, while all other builds leave this at false to preserve existing
performance.

Updates #18423

Signed-off-by: James Tucker <james@tailscale.com>
6 days ago
Harry Harpham c062230cce tsnet: clarify that ListenService starts the server if necessary
Every other listen method on tsnet.Server makes this clarification, so
should ListenService.

Fixes tailscale/corp#36207
Signed-off-by: Harry Harpham <harry@tailscale.com>
6 days ago
Claus Lensbøl 151644f647
wgengine: send disco key via TSMP on first contact (#18215)
When we have not yet communicated with a peer, send a
TSMPDiscoAdvertisement to let the peer know of our disco key. This is in
most cases redundant, but will allow us to set up direct connections
when the client cannot access control.

Some parts taken from: #18073

Updates #12639

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
6 days ago
Alex Valiushko 4b7585df77
net/udprelay: add tailscaled_peer_relay_endpoints gauge (#18265)
New gauge reflects endpoints state via labels:
- open, when both peers are connected and ready to talk, and
- connecting. when at least one peer hasn't connected yet.

Corresponding client metrics are logged as
- udprelay_endpoints_connecting
- udprelay_endpoints_open

Updates tailscale/corp#30820

Change-Id: Idb1baa90a38c97847e14f9b2390093262ad0ea23

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
7 days ago
Josh Bleecher Snyder 6dc0bd834c util/limiter: don't panic when dumping a new Limiter
Fixes #18439

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
7 days ago
David Bond 2cb86cf65e
cmd/k8s-operator,k8s-operator: Allow the use of multiple tailnets (#18344)
This commit contains  the implementation of multi-tailnet support within the Kubernetes Operator

Each of our custom resources now expose the `spec.tailnet` field. This field is a string that must match the name of an existing `Tailnet` resource. A `Tailnet` resource looks like this:

```yaml
apiVersion: tailscale.com/v1alpha1
kind: Tailnet
metadata:
  name: example  # This is the name that must be referenced by other resources
spec:
  credentials:
    secretName: example-oauth
```

Each `Tailnet` references a `Secret` resource that contains a set of oauth credentials. This secret must be created in the same namespace as the operator:

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: example-oauth # This is the name that's referenced by the Tailnet resource.
  namespace: tailscale
stringData:
  client_id: "client-id"
  client_secret: "client-secret"
```

When created, the operator performs a basic check that the oauth client has access to all required scopes. This is done using read actions on devices, keys & services. While this doesn't capture a missing "write" permission, it catches completely missing permissions. Once this check passes, the `Tailnet` moves into a ready state and can be referenced. Attempting to use a `Tailnet` in a non-ready state will stall the deployment of `Connector`s, `ProxyGroup`s and `Recorder`s until the `Tailnet` becomes ready.

The `spec.tailnet` field informs the operator that a `Connector`, `ProxyGroup`, or `Recorder` must be given an auth key generated using the specified oauth client. For backwards compatibility, the set of credentials the operator is configured with are considered the default. That is, where `spec.tailnet` is not set, the resource will be deployed in the same tailnet as the operator. 

Updates https://github.com/tailscale/corp/issues/34561
1 week ago
Jonathan Nobels e30626c480
version: add support for reporting the mac variant from tailscale --version (#18462)
fixes tailscale/corp#27182

tailscale version --json now includes an osVariant field that will report
one of macsys, appstore or darwin. We can extend this to other
platforms where tailscaled can have multiple personalities.

This also adds the concept of a platform-specific callback for querying
an explicit application identifier.  On Apple, we can use
CFBundleGetIdentifier(mainBundle) to get the bundle identifier via cgo.
This removes all the ambiguity and lets us remove other less direct
methods (like env vars, locations, etc).

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
1 week ago
Alex Valiushko 0a5639dcc0
net/udprelay: advertise addresses from cloud metadata service (#18368)
Polls IMDS (currently only AWS) for extra IPs to advertise as udprelay.

Updates #17796

Change-Id: Iaaa899ef4575dc23b09a5b713ce6693f6a6a6964

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
1 week ago
Tom Meadows 7213b35d85
k8s-operator,kube: remove enableSessionRecording from Kubernetes Cap Map (#18452)
* k8s-operator,kube: removing enableSessionRecordings option. It seems
like it is going to create a confusing user experience and it's going to
be a very niche use case, so we have decided to defer this for now.

Updates tailscale/corp#35796

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>

* k8s-operator: adding metric for env var deprecation

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>

---------

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
1 week ago
Eduardo Sorribas 7676030355
net/portmapper: Stop replacing the internal port with the upnp external port (#18349)
net/portmapper: Stop replacing the internal port with the upnp external port

This causes the UPnP mapping to break in the next recreation of the
mapping.

Fixes #18348

Signed-off-by: Eduardo Sorribas <eduardo@sorribas.org>
1 week ago
Harry Harpham 3840183be9 tsnet: add support for Services
This change allows tsnet nodes to act as Service hosts by adding a new
function, tsnet.Server.ListenService. Invoking this function will
advertise the node as a host for the Service and create a listener to
receive traffic for the Service.

Fixes #17697
Fixes tailscale/corp#27200
Signed-off-by: Harry Harpham <harry@tailscale.com>
2 weeks ago
Harry Harpham 1b88e93ff5 ipn/ipnlocal: allow retrieval of serve config ETags from local API
This change adds API to ipn.LocalBackend to retrieve the ETag when
querying for the current serve config. This allows consumers of
ipn.LocalBackend.SetServeConfig to utilize the concurrency control
offered by ETags. Previous to this change, utilizing serve config ETags
required copying the local backend's internal ETag calcuation.

The local API server was previously copying the local backend's ETag
calculation as described above. With this change, the local API server
now uses the new ETag retrieval function instead. Serve config ETags are
therefore now opaque to clients, in line with best practices.

Fixes tailscale/corp#35857
Signed-off-by: Harry Harpham <harry@tailscale.com>
2 weeks ago
Jonathan Nobels 643e91f2eb
net/netmon: move TailscaleInterfaceIndex out of netmon.State (#18428)
fixes tailscale/tailscale#18418

Both Serve and PeerAPI broke when we moved the TailscaleInterfaceName
into State, which is updated asynchronously and may not be
available when we configure the listeners.

This extracts the explicit interface name property from netmon.State
and adds as a static struct with getters that have proper error
handling.

The bug is only found in sandboxed Darwin clients, where we
need to know the Tailscale interface details in order to set up the
listeners correctly (they must bind to our interface explicitly to escape
the network sandboxing that is applied by NECP).

Currently set only sandboxed macOS and Plan9 set this but it will
also be useful on Windows to simplify interface filtering in netns.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 weeks ago
Nick Khyl 1478028591 docs/windows/policy: use a separate value to track the configuration state of EnableDNSRegistration
Policy editors, such as gpedit.msc and gpme.msc, rely on both the presence and the value of the
registry value to determine whether a policy is enabled. Unless an enabledValue is specified
explicitly, it defaults to REG_DWORD 1.

Therefore, we cannot rely on the same registry value to track the policy configuration state when
it is already used by a policy option, such as a dropdown. Otherwise, while the policy setting
will be written and function correctly, it will appear as Not Configured in the policy editor
due to the value mismatch (for example, REG_SZ "always" vs REG_DWORD 1).

In this PR, we update the DNSRegistration policy setting to use the DNSRegistrationConfigured
registry value for tracking. This change has no effect on the client side and exists solely to
satisfy ADMX and policy editor requirements.

Updates #14917

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 weeks ago
Tom Meadows 1cc6f3282e
k8s-operator,kube: allowing k8s api request events to be enabled via grants (#18393)
Updates #35796

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
2 weeks ago
Aaron Klotz 54d77898da tool/gocross: update gocross-wrapper.ps1 to use absolute path for resolving tar
gocross-wrapper.ps1 is written to use the version of tar that ships with
Windows; we want to avoid conflicts with any other tar on the PATH, such
ones installed by MSYS and/or Cygwin.

Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2 weeks ago
Nick O'Neill 1a79abf5fb
VERSION.txt: this is v1.95.0 (#18414)
Signed-off-by: Nick O'Neill <nick@tailscale.com>
2 weeks ago
Simon Law 5aeee1d8a5
.github/workflows: double the timeout for golangci-lint (#18404)
Recently, the golangci-lint workflow has been taking longer and longer
to complete, causing it to timeout after the default of 5 minutes.

    Running error: context loading failed: failed to load packages: failed to load packages: failed to load with go/packages: context deadline exceeded
    Timeout exceeded: try increasing it by passing --timeout option

Although PR #18398 enabled the Go module cache, bootstrapping with a
cold cache still takes too long.

This PR doubles the default 5 minute timeout for golangci-lint to 10
minutes so that golangci-lint can finish downloading all of its
dependencies.

Note that this doesn’t affect the 5 minute timeout configured in
.golangci.yml, since running golangci-lint on your local instance
should still be plenty fast.

Fixes #18366

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2 weeks ago
Tom Meadows c3b7f24051
ipn,ipn/local: always accept routes for Tailscale Services (cgnat range) (#18173)
Updates #18198

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
Co-authored-by: James Tucker <raggi@tailscale.com>
2 weeks ago
Mario Minardi e9d82767e5 cmd/containerboot: allow for automatic ID token generation
Allow for optionally specifying an audience for containerboot. This is
passed to tailscale up to allow for containerboot to use automatic ID
token generation for authentication.

Updates https://github.com/tailscale/corp/issues/34430

Signed-off-by: Mario Minardi <mario@tailscale.com>
2 weeks ago
Mario Minardi 02af7c963c tsnet: allow for automatic ID token generation
Allow for optionally specifiying an audience for tsnet. This is passed
to the underlying identity federation logic to allow for tsnet auth to
use automatic ID token generation for authentication.

Updates https://github.com/tailscale/corp/issues/33316

Signed-off-by: Mario Minardi <mario@tailscale.com>
2 weeks ago
Irbe Krumina 28f163542c
.github/actions/go-cache: build cigocacher using remote path, fall back to ./tool/go (#18409)
If local tailscale/tailscale checkout is not available,
pulll cigocacher remotely.
Fall back to ./tool/go if no other Go installation
is present.

Updates tailscale/corp#32493

Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
2 weeks ago
Danni Popova 6a6aa805d6
cmd,feature: add identity token auto generation for workload identity (#18373)
Adds the ability to detect what provider the client is running on and tries fetch the ID token to use with Workload Identity.

Updates https://github.com/tailscale/corp/issues/33316

Signed-off-by: Danni Popova <danni@tailscale.com>
2 weeks ago
Anton Tolchanov 58042e2de3 metrics: add a NewSet and Set.NewLabelMap helpers
Updates tailscale/corp#31174

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2 weeks ago
Anton Tolchanov 17b0c7bfb3 metrics: add a NewLabelMap helper to create and register label maps
Updates tailscale/corp#31174

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2 weeks ago
Simon Law 76fb09c6bd
.github/workflows: fix timeouts by caching packages for golangci-lint (#18398)
Recently, the golangci-lint workflow has been taking longer and longer
to complete, causing it to timeout after the default of 5 minutes.

    Running error: context loading failed: failed to load packages: failed to load packages: failed to load with go/packages: context deadline exceeded
    Timeout exceeded: try increasing it by passing --timeout option

This PR upgrades actions/setup-go to version 6, the latest, and
enables caching for Go modules and build outputs. This should speed up
linting because most packages won’t have to be downloaded over and
over again.

Fixes #18366

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2 weeks ago
Irbe Krumina 8c17d871b3
ipn/store/kubestore: don't load write replica certs in memory (#18395)
Fixes a bug where, for kube HA proxies, TLS certs for the replica
responsible for cert issuance where loaded in memory on startup,
although the in-memory store was not updated after renewal (to
avoid failing re-issuance for re-created Ingresses).
Now the 'write' replica always reads certs from the kube Secret.

Updates tailscale/tailscale#18394

Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
2 weeks ago
Harry Harpham 87e108e10c docs: add instructions on referencing pull requests in commit messages
Updates #cleanup
Signed-off-by: Harry Harpham <harry@tailscale.com>
2 weeks ago
Harry Harpham 78c8d14254 tsnet: use errors.Join and idiomatic field order
Updates #18376 (follow up on feedback)
Signed-off-by: Harry Harpham <harry@tailscale.com>
2 weeks ago
Raj Singh aadc4f2ef4
wgengine/magicsock: add home DERP region usermetric (#18062)
Expose the node's home DERP region ID as a Prometheus gauge via the
usermetrics endpoint.

Fixes #18061

Signed-off-by: Raj Singh <raj@tailscale.com>
3 weeks ago
Patrick O'Doherty 5db95ec376
go.mod: bump github.com/containerd/containerd@v1.7.29 (#18374)
Updates #cleanup

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
3 weeks ago
Harry Harpham 3c1be083a4 tsnet: ensure funnel listener cleans up after itself when closed
Previously the funnel listener would leave artifacts in the serve
config. This caused weird out-of-sync effects like the admin panel
showing that funnel was enabled for a node, but the node rejecting
packets because the listener was closed.

This change resolves these synchronization issues by ensuring that
funnel listeners clean up the serve config when closed.

See also:
e109cf9fdd

Updates #cleanup
Signed-off-by: Harry Harpham <harry@tailscale.com>
3 weeks ago
Harry Harpham f9762064cf tsnet: reset serve config only once
Prior to this change, we were resetting the tsnet's serve config every
time tsnet.Server.Up was run. This is important to do on startup, to
prevent messy interactions with stale configuration when the code has
changed.

However, Up is frequently run as a just-in-case step (for example, by
Server.ListenTLS/ListenFunnel and possibly by consumers of tsnet). When
the serve config is reset on each of these calls to Up, this creates
situations in which the serve config disappears unexpectedly. The
solution is to reset the serve config only on the first call to Up.

Fixes #8800
Updates tailscale/corp#27200
Signed-off-by: Harry Harpham <harry@tailscale.com>
3 weeks ago
Jordan Whited 5f34f14e14 net/udprelay: apply netns Control func to server socket(s)
To prevent peer relay servers from sending packets *over* Tailscale.

Updates tailscale/corp#35651

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 weeks ago
Mario Minardi 4c37141ab7 cmd,internal,feature: add workload idenity support to gitops pusher
Add support for authenticating the gitops-pusher using workload identity
federation.

Updates https://github.com/tailscale/corp/issues/34172

Signed-off-by: Mario Minardi <mario@tailscale.com>
3 weeks ago
Simon Law 3e45e5b420
feature/featuretags: make QR codes modular (#18358)
QR codes are used by `tailscale up --qr` to provide an easy way to
open a web-page without transcribing a difficult URI. However, there’s
no need for this feature if the client will never be called
interactively. So this PR adds the `ts_omit_qrcodes` build tag.

Updates #18182

Signed-off-by: Simon Law <sfllaw@tailscale.com>
3 weeks ago
Andrew Dunham 6aac87a84c net/portmapper, go.mod: unfork our goupnp dependency
Updates #7436

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
3 weeks ago
Tom Proctor 5019dc8eb2
go.mod: bump mkctr dep (#18365)
Brings in tailscale/mkctr#29.

Updates tailscale/corp#32085

Change-Id: I90160ed1cdc47118ac8fd0712d63a7b590e739d3

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
3 weeks ago
Tom Proctor 5be02ee6f8 cmd/k8s-operator/e2e,go.mod: remove client v2 dependency
It's not worth adding the v2 client just for these e2e tests. Remove
that dependency for now to keep a clear separation, but we should revive
the v2 client version if we ever decide to take that dependency for the
tailscale/tailscale repo as a whole.

Updates tailscale/corp#32085

Change-Id: Ic51ce233d5f14ce2d25f31a6c4bb9cf545057dd0
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
3 weeks ago
Tom Proctor 73cb3b491e
cmd/k8s-operator/e2e: run self-contained e2e tests with devcontrol (#17415)
* cmd/k8s-operator/e2e: run self-contained e2e tests with devcontrol

Adds orchestration for more of the e2e testing setup requirements to
make it easier to run them in CI, but also run them locally in a way
that's consistent with CI. Requires running devcontrol, but otherwise
supports creating all the scaffolding required to exercise the operator
and proxies.

Updates tailscale/corp#32085

Change-Id: Ia7bff38af3801fd141ad17452aa5a68b7e724ca6
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>

* cmd/k8s-operator/e2e: being more specific on tmp dir cleanup

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>

---------

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
Co-authored-by: chaosinthecrd <tom@tmlabs.co.uk>
3 weeks ago
Simon Law 522a6e385e
cmd/tailscale/cli, util/qrcodes: format QR codes on Linux consoles (#18182)
Raw Linux consoles support UTF-8, but we cannot assume that all UTF-8
characters are available. The default Fixed and Terminus fonts don’t
contain half-block characters (`▀` and `▄`), but do contain the
full-block character (`█`).

Sometimes, Linux doesn’t have a framebuffer, so it falls back to VGA.
When this happens, the full-block character could be anywhere in
extended ASCII block, because we don’t know which code page is active.

This PR introduces `--qr-format=auto` which tries to heuristically
detect when Tailscale is printing to a raw Linux console, whether
UTF-8 is enabled, and which block characters have been mapped in the
console font.

If Unicode characters are unavailable, the new `--qr-format=ascii`
formatter uses `#` characters instead of full-block characters.

Fixes #12935

Signed-off-by: Simon Law <sfllaw@tailscale.com>
3 weeks ago
Raj Singh e66531041b
cmd/containerboot: add OAuth and WIF auth support (#18311)
Fixes tailscale/corp#34430

Signed-off-by: Raj Singh <raj@tailscale.com>
3 weeks ago
Andrew Lytvynov 6c67deff38
cmd/distsign: add CLI for verifying package signatures (#18239)
Updates #35374

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 weeks ago
Naman Sood 480ee9fec0
ipn,cmd/tailscale/cli: set correct SNI name for TLS-terminated TCP Services (#17752)
Fixes #17749.

Signed-off-by: Naman Sood <mail@nsood.in>
3 weeks ago
Alex Valiushko 4c3cf8bb11
wgengine/magicsock: extract IMDS utilities into a standalone package (#18334)
Moves magicksock.cloudInfo into util/cloudinfo with minimal changes.

Updates #17796

Change-Id: I83f32473b9180074d5cdbf00fa31e5b3f579f189

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
3 weeks ago
Mario Minardi a662c541ab .github/workflows: bump create-pull-request to 8.0.0
Bump peter-evans/create-pull-request to 8.0.0 to ensure compatibility
with actions/checkout 6.x.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
3 weeks ago
dependabot[bot] 9a6282b515 .github: Bump actions/checkout from 4.2.2 to 5.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.2.2 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
3 weeks ago
Harry Harpham 7de1b0b330
cmd/tailscale/cli: remove Services-specific subcommands from funnel (#18225)
The funnel command is sort of an alias for the serve command. This means
that the subcommands added to serve to support Services appear as
subcommands for funnel as well, despite having no meaning for funnel.
This change removes all such Services-specific subcommands from funnel.

Fixes tailscale/corp#34167

Signed-off-by: Harry Harpham <harry@tailscale.com>
3 weeks ago
Irbe Krumina 8ea90ba80d
cmd/tailscaled,ipn/{ipnlocal,store/kubestore}: don't create attestation keys for stores that are not bound to a node (#18322)
Ensure that hardware attestation keys are not added to tailscaled
state stores that are Kubernetes Secrets or AWS SSM as those Tailscale
devices should be able to be recreated on different nodes, for example,
when moving Pods between nodes.

Updates tailscale/tailscale#18302

Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
3 weeks ago
Andrew Lytvynov 68617bb82e
cmd/tailscaled: disable state encryption / attestation by default (#18336)
TPM-based features have been incredibly painful due to the heterogeneous
devices in the wild, and many situations in which the TPM "changes" (is
reset or replaced). All of this leads to a lot of customer issues.

We hoped to iron out all the kinks and get all users to benefit from
state encryption and hardware attestation without manually opting in,
but the long tail of kinks is just too long.

This change disables TPM-based features on Windows and Linux by default.
Node state should get auto-decrypted on update, and old attestation keys
will be removed.

There's also tailscaled-on-macOS, but it won't have a TPM or Keychain
bindings anyway.

Updates #18302
Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 weeks ago
Andrew Lytvynov 2e77b75e96
ipn/ipnlocal: don't fail profile unmarshal due to attestation keys (#18335)
Soft-fail on initial unmarshal and try again, ignoring the
AttestationKey. This helps in cases where something about the
attestation key storage (usually a TPM) is messed up. The old key will
be lost, but at least the node can start again.

Updates #18302
Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 weeks ago
James Tucker 39a61888b8 ssh/tailssh: send audit messages on SSH login (Linux)
Send LOGIN audit messages to the kernel audit subsystem on Linux
when users successfully authenticate to Tailscale SSH. This provides
administrators with audit trail integration via auditd or journald,
recording details about both the Tailscale user (whois) and the
mapped local user account.

The implementation uses raw netlink sockets to send AUDIT_USER_LOGIN
messages to the kernel audit subsystem. It requires CAP_AUDIT_WRITE
capability, which is checked at runtime. If the capability is not
present, audit logging is silently skipped.

Audit messages are sent to the kernel (pid 0) and consumed by either
auditd (written to /var/log/audit/audit.log) or journald (available
via journalctl _TRANSPORT=audit), depending on system configuration.

Note: This may result in duplicate messages on a system where
auditd/journald audit logs are enabled and the system has and supports
`login -h`. Sadly Linux login code paths are still an inconsistent wild
west so we accept the potential duplication rather than trying to avoid
it.

Fixes #18332

Signed-off-by: James Tucker <james@tailscale.com>
3 weeks ago
Vince Liem b7081522e7
scripts/installer.sh: add ultramarine to supported OS list 3 weeks ago
Raj Singh d451cd54a7
cmd/derper: add --acme-email flag for GCP cert mode (#18278)
GCP Certificate Manager requires an email contact on ACME accounts.
Add --acme-email flag that is required for --certmode=gcp and
optional for --certmode=letsencrypt.

Fixes #18277

Signed-off-by: Raj Singh <raj@tailscale.com>
1 month ago
Nick Khyl 2917ea8d0e ipn/ipnauth, safesocket: defer named pipe client's token retrieval until ipnserver needs it
An error returned by net.Listener.Accept() causes the owning http.Server to shut down.
With the deprecation of net.Error.Temporary(), there's no way for the http.Server to test
whether the returned error is temporary / retryable or not (see golang/go#66252).

Because of that, errors returned by (*safesocket.winIOPipeListener).Accept() cause the LocalAPI
server (aka ipnserver.Server) to shut down, and tailscaled process to exit.

While this might be acceptable in the case of non-recoverable errors, such as programmer errors,
we shouldn't shut down the entire tailscaled process for client- or connection-specific errors,
such as when we couldn't obtain the client's access token because the client attempts to connect
at the Anonymous impersonation level. Instead, the LocalAPI server should gracefully handle
these errors by denying access and returning a 401 Unauthorized to the client.

In tailscale/tscert#15, we fixed a known bug where Caddy and other apps using tscert would attempt
to connect at the Anonymous impersonation level and fail. However, we should also fix this on the tailscaled
side to prevent a potential DoS, where a local app could deliberately open the Tailscale LocalAPI named pipe
at the Anonymous impersonation level and cause tailscaled to exit.

In this PR, we defer token retrieval until (*WindowsClientConn).Token() is called and propagate the returned token
or error via ipnauth.GetConnIdentity() to ipnserver, which handles it the same way as other ipnauth-related errors.

Fixes #18212
Fixes tailscale/tscert#13

Signed-off-by: Nick Khyl <nickk@tailscale.com>
1 month ago
Alex Chan 9c3a420e15 cmd/tailscale/cli: document why there's no --force-reauth on login
Change-Id: Ied799fefbbb4612c7ba57b8369a418b7704eebf8
Updates #18273
Signed-off-by: Alex Chan <alexc@tailscale.com>
1 month ago
Alex Valiushko ee59470270
net/udprelay: remove tailscaled_peer_relay_endpoints_total (#18254)
This gauge will be reworked to include endpoint state in future.

Updates tailscale/corp#30820

Change-Id: I66f349d89422b46eec4ecbaf1a99ad656c7301f9

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
1 month ago
Irbe Krumina 90b4358113
cmd/k8s-operator,ipn/ipnlocal: allow opting out of ACME order replace extension (#18252)
In dynamically changing environments where ACME account keys and certs
are stored separately, it can happen that the account key would get
deleted (and recreated) between issuances. If that is the case,
we currently fail renewals and the only way to recover is for users
to delete certs.
This adds a config knob to allow opting out of the replaces extension
and utilizes it in the Kubernetes operator where there are known
user workflows that could end up with this edge case.

Updates #18251

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
1 month ago
Alex Valiushko c40f352103
net/udprelay: expose peer relay metrics (#18218)
Adding both user and client metrics for peer relay forwarded bytes and
packets, and the total endpoints gauge.

User metrics:
tailscaled_peer_relay_forwarded_packets_total{transport_in, transport_out}
tailscaled_peer_relay_forwarded_bytes_total{transport_in, transport_out}
tailscaled_peer_relay_endpoints_total{}

Where the transport labels can be of "udp4" or "udp6".

Client metrics:
udprelay_forwarded_(packets|bytes)_udp(4|6)_udp(4|6)
udprelay_endpoints

RELNOTE: Expose tailscaled metrics for peer relay.

Updates tailscale/corp#30820

Change-Id: I1a905d15bdc5ee84e28017e0b93210e2d9660259

Signed-off-by: Alex Valiushko <alexvaliushko@tailscale.com>
1 month ago
Tom Proctor bb3529fcd4
cmd/containerboot: support egress to Tailscale Service FQDNs (#17493)
Adds support for targeting FQDNs that are a Tailscale Service. Uses the
same method of searching for Services as the tailscale configure
kubeconfig command. This fixes using the tailscale.com/tailnet-fqdn
annotation for Kubernetes Service when the specified FQDN is a Tailscale
Service.

Fixes #16534

Change-Id: I422795de76dc83ae30e7e757bc4fbd8eec21cc64

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
Signed-off-by: Becky Pauley <becky@tailscale.com>
1 month ago
Tom Proctor eed5e95e27 docs: use -x for cherry-picks
Updates #cleanup

Change-Id: I5222e23b716b342d7c6d113fc539d2021024348e
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
1 month ago
Irbe Krumina b73fb467e4
ipn/ipnlocal: log cert renewal failures (#18246)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
1 month ago
Brendan Creane e4847fa77b
go.toolchain.rev: update to Go 1.25.5 (#18123)
Updates #18122

Signed-off-by: Brendan Creane <bcreane@gmail.com>
1 month ago
Andrew Lytvynov ce7e1dea45
types/persist: omit Persist.AttestationKey based on IsZero (#18241)
IsZero is required by the interface, so we should use that before trying
to serialize the key.

Updates #35412

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
1 month ago
Tom Meadows b21cba0921
cmd/k8s-operator: fixes helm template for oauth secret volume mount (#18230)
Fixes #18228

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
1 month ago
Andrew Dunham 323604b76c net/dns/resolver: log source IP of forwarded queries
When the TS_DEBUG_DNS_FORWARD_SEND envknob is turned on, also log the
source IP:port of the query that tailscaled is forwarding.

Updates tailscale/corp#35374

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
1 month ago
Jonathan Nobels 3e89068792
net/netmon, wgengine/userspace: purge ChangeDelta.Major and address TODOs (#17823)
updates tailscale/corp#33891

Addresses several older the TODO's in netmon.  This removes the 
Major flag precomputes the ChangeDelta state, rather than making
consumers of ChangeDeltas sort that out themselves.   We're also seeing
a lot of ChangeDelta's being flagged as "Major" when they are
not interesting, triggering rebinds in wgengine that are not needed.  This
cleans that up and adds a host of additional tests.

The dependencies are cleaned, notably removing dependency on netmon
itself for calculating what is interesting, and what is not.  This includes letting
individual platforms set a bespoke global "IsInterestingInterface"
function.  This is only used on Darwin.

RebindRequired now roughly follows how "Major" was historically
calculated but includes some additional checks for various
uninteresting events such as changes in interface addresses that
shouldn't trigger a rebind.  This significantly reduces thrashing (by
roughly half on Darwin clients which switching between nics).   The individual
values that we roll  into RebindRequired are also exposed so that
components consuming netmap.ChangeDelta can ask more
targeted questions.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
1 month ago
Will Norris 0fd1670a59 client/local: add method to set gauge metric to a value
The existing client metric methods only support incrementing (or
decrementing) a delta value.  This new method allows setting the metric
to a specific value.

Updates tailscale/corp#35327

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
1 month ago
stratself f174ecb6fd
words: 33 tails and 26 scales (#18213)
Updates #words

Signed-off-by: stratself <126093083+stratself@users.noreply.github.com>
1 month ago
Jordan Whited a663639bea net/udprelay: replace map+sync.Mutex with sync.Map for VNI lookup
This commit also introduces a sync.Mutex for guarding mutatable fields
on serverEndpoint, now that it is no longer guarded by the sync.Mutex
in Server.

These changes reduce lock contention and by effect increase aggregate
throughput under high flow count load. A benchmark on Linux with AWS
c8gn instances showed a ~30% increase in aggregate throughput (37Gb/s
vs 28Gb/s) for 12 tailscaled flows.

Updates tailscale/corp#35264

Signed-off-by: Jordan Whited <jordan@tailscale.com>
1 month ago
Will Norris 951d711054 client/systray: add missing deferred unlock for httpCache mutex
Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
1 month ago
Tom Proctor d0d993f5d6 .github,cmd/cigocacher: add flags --version --stats --cigocached-host
Add flags:

* --cigocached-host to support alternative host resolution in other
  environments, like the corp repo.
* --stats to reduce the amount of bash script we need.
* --version to support a caching tool/cigocacher script that will
  download from GitHub releases.

Updates tailscale/corp#10808

Change-Id: Ib2447bc5f79058669a70f2c49cef6aedd7afc049
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
1 month ago
Tom Meadows d7a5624841
cmd/k8s-operator: fix statefulset template yaml indentation (#18194)
Fixes #17000

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
1 month ago
Irbe Krumina cb5fa35f57
.github/workfkows,Dockerfile,Dockerfile.base: add a test for base image (#18180)
Test that the base image builds and has the right iptables binary
linked.

Updates #17854

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2 months ago
James 'zofrex' Sanderson 3ef9787379
tsweb: add Unwrap to loggingResponseWriter for ResponseController (#18195)
The new http.ResponseController type added in Go 1.20:
https://go.dev/doc/go1.20#http_responsecontroller requires ResponseWriters
that are wrapping the original passed to ServeHTTP to implement an Unwrap
method: https://pkg.go.dev/net/http#NewResponseController

With this in place, it is possible to call methods such as Flush and
SetReadDeadline on a loggingResponseWriter without needing to implement them
there ourselves.

Updates tailscale/corp#34763
Updates tailscale/corp#34813

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2 months ago
Raj Singh 65182f2119
ipn/ipnlocal: add ProxyProtocol support to VIP service TCP handler (#18175)
tcpHandlerForVIPService was missing ProxyProtocol support that
tcpHandlerForServe already had. Extract the shared logic into
forwardTCPWithProxyProtocol helper and use it in both handlers.

Fixes #18172

Signed-off-by: Raj Singh <raj@tailscale.com>
2 months ago
Joe Tsai 9613b4eecc
logtail: add metrics (#18184)
Add metrics about logtail uploading and underlying buffer.
Add metrics to the in-memory buffer implementation.

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2 months ago
Brad Fitzpatrick 0df4631308 ipn/ipnlocal: avoid ResetAndStop panic
Updates #18187

Change-Id: If7375efb7df0452a5e85b742fc4c4eecbbd62717
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Simon Law 6ace3995f0
portlist: skip tests on Linux 6.14.x with /proc/net/tcp bug (#18185)
PR #18033 skipped tests for the versions of Linux 6.6 and 6.12 that
had a regression in /proc/net/tcp that causes seek operations to fail
with “illegal seek”.

This PR skips tests for Linux 6.14.0, which is the default Ubuntu
kernel, that also contains this regression.

Updates #16966

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2 months ago
Joe Tsai 6428ba01ef
logtail/filch: rewrite the package (#18143)
The filch implementation is fairly broken:

* When Filch.cur exceeds MaxFileSize, it calls moveContents
to copy the entirety of cur into alt (while holding the write lock).
By nature, this is the movement of a lot of data in a hot path,
meaning that all log calls will be globally blocked!
It also means that log uploads will be blocked during the move.

* The implementation of moveContents is buggy in that
it copies data from cur into the start of alt,
but fails to truncate alt to the number of bytes copied.
Consequently, there are unrelated lines near the end,
leading to out-of-order lines when being read back.

* Data filched via stderr do not directly respect MaxFileSize,
which is only checked every 100 Filch.Write calls.
This means that it is possible that the file grows far beyond
the specified max file size before moveContents is called.

* If both log files have data when New is called,
it also copies the entirety of cur into alt.
This can block the startup of a process copying lots of data
before the process can do any useful work.

* TryReadLine is implemented using bufio.Scanner.
Unfortunately, it will choke on any lines longer than
bufio.MaxScanTokenSize, rather than gracefully skip over them.

The re-implementation avoids a lot of these problems
by fundamentally eliminating the need for moveContent.
We enforce MaxFileSize by simply rotating the log files
whenever the current file exceeds MaxFileSize/2.
This is a constant-time operation regardless of file size.

To more gracefully handle lines longer than bufio.MaxScanTokenSize,
we skip over these lines (without growing the read buffer)
and report an error. This allows subsequent lines to be read.

In order to improve debugging, we add a lot of metrics.

Note that the the mechanism of dup2 with stderr
is inherently racy with a the two file approach.
The order of operations during a rotation is carefully chosen
to reduce the race window to be as short as possible.
Thus, this is slightly less racy than before.

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2 months ago
Claus Lensbøl c870d3811d
net/{packet,tstun},wgengine: update disco key when receiving via TSMP (#18158)
When receiving a TSMPDiscoAdvertisement from peer, update the discokey
for said peer.

Some parts taken from: https://github.com/tailscale/tailscale/pull/18073/

Updates #12639

Co-authored-by: James Tucker <james@tailscale.com>
2 months ago
Irbe Krumina 723b9af21a
Dockerfile,Dockerfile.base: link iptables to legacy binary (#18177)
Re-instate the linking of iptables installed in Tailscale container
to the legacy iptables version. In environments where the legacy
iptables is not needed, we should be able to run nftables instead,
but this will ensure that Tailscale keeps working in environments
that don't support nftables, such as some Synology NAS hosts.

Updates #17854

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2 months ago
Raj Singh 8eda947530
cmd/derper: add GCP Certificate Manager support (#18161)
Add --certmode=gcp for using Google Cloud Certificate Manager's
public CA instead of Let's Encrypt. GCP requires External Account
Binding (EAB) credentials for ACME registration, so this adds
--acme-eab-kid and --acme-eab-key flags.

The EAB key accepts both base64url and standard base64 encoding
to support both ACME spec format and gcloud output.

Fixes tailscale/corp#34881

Signed-off-by: Raj Singh <raj@tailscale.com>
Co-authored-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Claus Lensbøl 1dfdee8521
net/dns: retrample resolve.conf when another process has trampled it (#18069)
When using the resolve.conf file for setting DNS, it is possible that
some other services will trample the file and overwrite our set DNS
server. Experiments has shown this to be a racy error depending on how
quickly processes start.

Make an attempt to trample back the file a limited number of times if
the file is changed.

Updates #16635

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2 months ago
Jordan Whited a9b37c510c net/udprelay: re-use mono.Time in control packet handling
Fixes tailscale/corp#35100

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Simar 363d882306 net/udprelay: use `mono.Time` instead of `time.Time`
Fixes: https://github.com/tailscale/tailscale/issues/18064

Signed-off-by: Simar <simar@linux.com>
2 months ago
Fran Bull 076d5c7214 appc,feature: add the start of new conn25 app connector
When peers request an IP address mapping to be stored, the connector
stores it in memory.

Fixes tailscale/corp#34251
Signed-off-by: Fran Bull <fran@tailscale.com>
2 months ago
Tom Proctor dd1bb8ee42 .github: add cigocacher release workflow
To save rebuilding cigocacher on each CI job, build it on-demand, and
publish a release similar to how we publish releases for tool/go to
consume. Once the first release is done, we can add a new
tool/cigocacher script that pins to a specific release for each branch
to download.

Updates tailscale/corp#10808

Change-Id: I7694b2c2240020ba2335eb467522cdd029469b6c
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
Alex Chan 378ee20b9a cmd/tailscale/cli: stabilise the output of `tailscale lock status --json`
This patch stabilises the JSON output, and improves it in the following
ways:

* The AUM hash in Head uses the base32-encoded form of an AUM hash,
  consistent with how it's presented elsewhere
* TrustedKeys are the same format as the keys as `tailnet lock log --json`
* SigKind, Pubkey and KeyID are all presented consistently with other
  JSON output in NodeKeySignature
* FilteredPeers don't have a NodeKeySignature, because it will always
  be empty

For reference, here's the JSON output from the CLI prior to this change:

```json
{
  "Enabled": true,
  "Head": [
    196,
    69,
    63,
    243,
    213,
    133,
    123,
    46,
    183,
    203,
    143,
    34,
    184,
    85,
    80,
    1,
    221,
    92,
    49,
    213,
    93,
    106,
    5,
    206,
    176,
    250,
    58,
    165,
    155,
    136,
    11,
    13
  ],
  "PublicKey": "nlpub:0f99af5c02216193963ce9304bb4ca418846eddebe237f37a6de1c59097ed0b8",
  "NodeKey": "nodekey:8abfe98b38151748919f6e346ad16436201c3ecd453b01e9d6d3a38e1826000d",
  "NodeKeySigned": true,
  "NodeKeySignature": {
    "SigKind": 1,
    "Pubkey": "bnCKv+mLOBUXSJGfbjRq0WQ2IBw+zUU7AenW06OOGCYADQ==",
    "KeyID": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg=",
    "Signature": "4DPW4v6MyLLwQ8AMDm27BVDGABjeC9gg1EfqRdKgzVXi/mJDwY9PTAoX0+0WTRs5SUksWjY0u1CLxq5xgjFGBA==",
    "Nested": null,
    "WrappingPubkey": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg="
  },
  "TrustedKeys": [
    {
      "Key": "nlpub:0f99af5c02216193963ce9304bb4ca418846eddebe237f37a6de1c59097ed0b8",
      "Metadata": null,
      "Votes": 1
    },
    {
      "Key": "nlpub:de2254c040e728140d92bc967d51284e9daea103a28a97a215694c5bda2128b8",
      "Metadata": null,
      "Votes": 1
    }
  ],
  "VisiblePeers": [
    {
      "Name": "signing2.taila62b.unknown.c.ts.net.",
      "ID": 7525920332164264,
      "StableID": "nRX6TbAWm121DEVEL",
      "TailscaleIPs": [
        "100.110.67.20",
        "fd7a:115c:a1e0::9c01:4314"
      ],
      "NodeKey": "nodekey:10bf4a5c168051d700a29123cd81568377849da458abef4b328794ca9cae4313",
      "NodeKeySignature": {
        "SigKind": 1,
        "Pubkey": "bnAQv0pcFoBR1wCikSPNgVaDd4SdpFir70syh5TKnK5DEw==",
        "KeyID": "D5mvXAIhYZOWPOkwS7TKQYhG7d6+I383pt4cWQl+0Lg=",
        "Signature": "h9fhwHiNdkTqOGVQNdW6AVFoio6MFaFobPiK9ydywgmtYxcExJ38b76Tabdc56aNLxf8IfCaRw2VYPcQG2J/AA==",
        "Nested": null,
        "WrappingPubkey": "3iJUwEDnKBQNkryWfVEoTp2uoQOiipeiFWlMW9ohKLg="
      }
    }
  ],
  "FilteredPeers": [
    {
      "Name": "node3.taila62b.unknown.c.ts.net.",
      "ID": 5200614049042386,
      "StableID": "n3jAr7KNch11DEVEL",
      "TailscaleIPs": [
        "100.95.29.124",
        "fd7a:115c:a1e0::f901:1d7c"
      ],
      "NodeKey": "nodekey:454d2c8602c10574c5ec3a6790f159714802012b7b8bb8d2ab47d637f9df1d7b",
      "NodeKeySignature": {
        "SigKind": 0,
        "Pubkey": null,
        "KeyID": null,
        "Signature": null,
        "Nested": null,
        "WrappingPubkey": null
      }
    }
  ],
  "StateID": 16885615198276932820
}
```

Updates https://github.com/tailscale/corp/issues/22355
Updates https://github.com/tailscale/tailscale/issues/17619

Signed-off-by: Alex Chan <alexc@tailscale.com>

Change-Id: I65b58ff4520033e6b70fc3b1ba7fc91c1f70a960
2 months ago
Nick Khyl da0ea8ef3e Revert "ipn/ipnlocal: shut down old control client synchronously on reset"
It appears (*controlclient.Auto).Shutdown() can still deadlock when called with b.mu held, and therefore the changes in #18127 are unsafe.

This reverts #18127 until we figure out what causes it.

This reverts commit d199ecac80.

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Erisa A c7b10cb39f
scripts/installer.sh: add SteamOS handling (#18159)
Fixes #12943

Signed-off-by: Erisa A <erisa@tailscale.com>
2 months ago
Alex Chan 7d3097d3b5 tka: add some more tests for Bootstrap()
This improves our test coverage of the Bootstrap() method, especially
around catching AUMs that shouldn't pass validation.

Updates #cleanup

Change-Id: Idc61fcbc6daaa98c36d20ec61e45ce48771b85de
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Irbe Krumina 2a0ddb7897
cmd/k8s-operator: warn if users attempt to expose a headless Service (#18140)
Previously, if users attempted to expose a headless Service to tailnet,
this just silently did not work.
This PR makes the operator throw a warning event + update Service's
status with an error message.

Updates #18139

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2 months ago
Irbe Krumina d5c893195b
cmd/k8s-operator: don't log errors on not found objects. (#18142)
The event queue gets deleted events, which means that sometimes
the object that should be reconciled no longer exists.
Don't log user facing errors if that is the case.

Updates #18141

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2 months ago
Claus Lensbøl d349370e55
client/systray: change systray to start after graphical.target (#18138)
The service was starting after systemd itself, and while this
surprisingly worked for some situations, it broke for others.

Change it to start after a GUI has been initialized.

Updates #17656

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2 months ago
James 'zofrex' Sanderson cf40cf5ccb
ipn/ipnlocal: add peer API endpoints to Hostinfo on initial client creation (#17851)
Previously we only set this when it updated, which was fine for the first
call to Start(), but after that point future updates would be skipped if
nothing had changed. If Start() was called again, it would wipe the peer API
endpoints and they wouldn't get added back again, breaking exit nodes (and
anything else requiring peer API to be advertised).

Updates tailscale/corp#27173

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2 months ago
Peter A. f4d34f38be cmd/tailscale,ipn: add Unix socket support for serve
Based on PR #16700 by @lox, adapted to current codebase.

Adds support for proxying HTTP requests to Unix domain sockets via
tailscale serve unix:/path/to/socket, enabling exposure of services
like Docker, containerd, PHP-FPM over Tailscale without TCP bridging.

The implementation includes reasonable protections against exposure of
tailscaled's own socket.

Adaptations from original PR:
- Use net.Dialer.DialContext instead of net.Dial for context propagation
- Use http.Transport with Protocols API (current h2c approach, not http2.Transport)
- Resolve conflicts with hasScheme variable in ExpandProxyTargetValue

Updates #9771

Signed-off-by: Peter A. <ink.splatters@pm.me>
Co-authored-by: Lachlan Donald <lachlan@ljd.cc>
2 months ago
Nick Khyl 557457f3c2
ipn/ipnlocal: fix LocalBackend deadlock when packet arrives during profile switch (#18126)
If a packet arrives while WireGuard is being reconfigured with b.mu held, such as during a profile switch,
calling back into (*LocalBackend).GetPeerAPIPort from (*Wrapper).filterPacketInboundFromWireGuard
may deadlock when it tries to acquire b.mu.

This occurs because a peer cannot be removed while an inbound packet is being processed.
The reconfig and profile switch wait for (*Peer).RoutineSequentialReceiver to return, but it never finishes
because GetPeerAPIPort needs b.mu, which the waiting goroutine already holds.

In this PR, we make peerAPIPorts a new syncs.AtomicValue field that is written with b.mu held
but can be read by GetPeerAPIPort without holding the mutex, which fixes the deadlock.

There might be other long-term ways to address the issue, such as moving peer API listeners
from LocalBackend to nodeBackend so they can be accessed without holding b.mu,
but these changes are too large and risky at this stage in the v1.92 release cycle.

Updates #18124

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Nick Khyl d199ecac80 ipn/ipnlocal: shut down old control client synchronously on reset
Previously, callers of (*LocalBackend).resetControlClientLocked were supposed
to call Shutdown on the returned controlclient.Client after releasing b.mu.
In #17804, we started calling Shutdown while holding b.mu, which caused
deadlocks during profile switches due to the (*ExecQueue).RunSync implementation.

We first patched this in #18053 by calling Shutdown in a new goroutine,
which avoided the deadlocks but made TestStateMachine flaky because
the shutdown order was no longer guaranteed.

In #18070, we updated (*ExecQueue).RunSync to allow shutting down
the queue without waiting for RunSync to return. With that change,
shutting down the control client while holding b.mu became safe.

Therefore, this PR updates (*LocalBackend).resetControlClientLocked
to shut down the old client synchronously during the reset, instead of
returning it and shifting that responsibility to the callers.

This fixes the flaky tests and simplifies the code.

Fixes #18052

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Andrew Lytvynov 7bc25f77f4
go.toolchain.rev: update to Go 1.25.5 (#18123)
Updates #18122

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Jordan Whited 6a44990b09 net/udprelay: bind multiple sockets per af on Linux
This commit uses SO_REUSEPORT (when supported) to bind multiple sockets
per address family. Increasing the number of sockets can increase
aggregate throughput when serving many peer relay client flows.
Benchmarks show 3x improvement in max aggregate bitrate in some
environments.

Updates tailscale/corp#34745

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Alex Chan e33f6aa3ba go.mod: bump the version of setec
Updates https://github.com/tailscale/corp/issues/34813

Change-Id: I926f1bad5bf143d82ddb36f51f70deb24fa11e71
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Tom Proctor f8cd07fb8a .github: make cigocacher script more robust
We got a flake in https://github.com/tailscale/tailscale/actions/runs/19867229792/job/56933249360
but it's not obvious to me where it failed. Make it more robust and
print out more useful error messages for next time.

Updates tailscale/corp#10808

Change-Id: I9ca08ea1103b9ad968c9cc0c42a493981ea62435
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
Brad Fitzpatrick b8c58ca7c1 wgengine: fix TSMP/ICMP callback leak
Fixes #18112

Change-Id: I85d5c482b01673799d51faeb6cb0579903597502
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Gesa Stupperich 536188c1b5 tsnet: enable node registration via federated identity
Updates: tailscale.com/corp#34148

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
2 months ago
Joe Tsai 957a443b23
cmd/netlogfmt: allow empty --resolve-addrs flag (#18103)
Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2 months ago
Raj Singh bd5c50909f
scripts/installer: add TAILSCALE_VERSION environment variable (#18014)
Add support for pinning specific Tailscale versions during installation
via the TAILSCALE_VERSION environment variable.

Example usage:
  curl -fsSL https://tailscale.com/install.sh | TAILSCALE_VERSION=1.88.4 sh

Fixes #17776

Signed-off-by: Raj Singh <raj@tailscale.com>
2 months ago
Tom Proctor 22a815b6d2 tool: bump binaryen wasm optimiser version 111 -> 125
111 is 3 years old, and there have been a lot of speed improvements
since then. We run wasm-opt twice as part of the CI wasm job, and it
currently takes about 3 minutes each time. With 125, it takes ~40
seconds, a 4.5x speed-up.

Updates #cleanup

Change-Id: I671ae6cefa3997a23cdcab6871896b6b03e83a4f
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
License Updater 8976b34cb8 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2 months ago
Naasir 77dcdc223e cleanup: fix typos across multiple files
Does not affect code.

Updates #cleanup

Signed-off-by: Naasir <yoursdeveloper@protonmail.com>
2 months ago
Tom Proctor ece6e27f39 .github,cmd/cigocacher: use cigocacher for windows
Implements a new disk put function for cigocacher that does not cause
locking issues on Windows when there are multiple processes reading and
writing the same files concurrently. Integrates cigocacher into test.yml
for Windows where we are running on larger runners that support
connecting to private Azure vnet resources where cigocached is hosted.

Updates tailscale/corp#10808

Change-Id: I0d0e9b670e49e0f9abf01ff3d605cd660dd85ebb
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
Tom Proctor 97f1fd6d48 .github: only save cache on main
The cache artifacts from a full run of test.yml are 14GB. Only save
artifacts from the main branch to ensure we don't thrash too much. Most
branches should get decent performance with a hit from recent main.

Fixes tailscale/corp#34739

Change-Id: Ia83269d878e4781e3ddf33f1db2f21d06ea2130f
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
Shaikh Naasir 37b4dd047f
k8s-operator: Fix typos in egress-pod-readiness.go
Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Alex Chan bd12d8f12f cmd/tailscale/cli: soften the warning on `--force-reauth` for seamless
Thanks to seamless key renewal, you can now do a force-reauth without
losing your connection in all circumstances. We softened the interactive
warning (see #17262) so let's soften the help text as well.

Updates https://github.com/tailscale/corp/issues/32429

Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Anton Tolchanov 34dff57137 feature/posture: log method and full URL for posture identity requests
Updates tailscale/corp#34676

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2 months ago
Fernando Serboncini f36eb81e61
cmd/k8s-operator fix populateTLSSecret on tests (#18088)
The call for populateTLSSecret was broken between PRs.

Updates #cleanup

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
2 months ago
Fernando Serboncini 7c5c02b77a
cmd/k8s-operator: add support for taiscale.com/http-redirect (#17596)
* cmd/k8s-operator: add support for taiscale.com/http-redirect

The k8s-operator now supports a tailscale.com/http-redirect annotation
on Ingress resources. When enabled, this automatically creates port 80
handlers that automatically redirect to the equivalent HTTPS location.

Fixes #11252

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>

* Fix for permanent redirect

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>

* lint

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>

* warn for redirect+endpoint

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>

* tests

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>

---------

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
2 months ago
Mario Minardi 411cee0dc9 .github/workflows: only run golang ci lint when go files have changed
Restrict running the golangci-lint workflow to when the workflow file
itself or a .go file, go.mod, or go.sum have actually been modified.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2 months ago
dependabot[bot] b40272e767 build(deps): bump braces from 3.0.2 to 3.0.3 in /client/web
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-version: 3.0.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
dependabot[bot] 22bdf34a00 build(deps): bump cross-spawn from 7.0.3 to 7.0.6 in /client/web
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
dependabot[bot] c0c0d45114 build(deps-dev): bump vitest from 1.3.1 to 1.6.1 in /client/web
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 1.3.1 to 1.6.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
dependabot[bot] 3e2476ec13 build(deps-dev): bump vite from 5.1.7 to 5.4.21 in /client/web
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.7 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
dependabot[bot] 9500689bc1 build(deps): bump js-yaml from 4.1.0 to 4.1.1 in /client/web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2 months ago
Mario Minardi 9cc07bf9c0 .github/workflows: skip draft PRs for request review workflows
Skip the "request review" workflows for PRs that are in draft to reduce
noise / skip adding reviewers to PRs that are intentionally marked as
not ready to review.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2 months ago
Brad Fitzpatrick 74ed589042 syncs: add means of declare locking assumptions for debug mode validation
Updates #17852

Change-Id: I42a64a990dcc8f708fa23a516a40731a19967aba
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Jonathan Nobels 3f9f0ed93c
VERSION.txt: this is v1.93.0 (#18074)
Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 months ago
James Tucker 5ee0c6bf1d derp/derpserver: add a unique sender cardinality estimate
Adds an observation point that may identify potentially abusive traffic
patterns at outlier values.

Updates tailscale/corp#24681

Signed-off-by: James Tucker <james@tailscale.com>
2 months ago
Andrew Lytvynov 9eff8a4503
feature/tpm: return opening errors from both /dev/tpmrm0 and /dev/tpm0 (#18071)
This might help users diagnose why TPM access is failing for tpmrm0.

Fixes #18026

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Brad Fitzpatrick 8af7778ce0 util/execqueue: don't hold mutex in RunSync
We don't hold q.mu while running normal ExecQueue.Add funcs, so we
shouldn't in RunSync either. Otherwise code it calls can't shut down
the queue, as seen in #18502.

Updates #18052

Co-authored-by: Nick Khyl <nickk@tailscale.com>
Change-Id: Ic5e53440411eca5e9fabac7f4a68a9f6ef026de1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Alex Chan b7658a4ad2 tstest/integration: add integration test for Tailnet Lock
This patch adds an integration test for Tailnet Lock, checking that a node can't
talk to peers in the tailnet until it becomes signed.

This patch also introduces a new package `tstest/tkatest`, which has some helpers
for constructing a mock control server that responds to TKA requests. This allows
us to reduce boilerplate in the IPN tests.

Updates tailscale/corp#33599

Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Jordan Whited 824027305a cmd/tailscale/cli,ipn,all: make peer relay server port a *uint16
In preparation for exposing its configuration via ipn.ConfigVAlpha,
change {Masked}Prefs.RelayServerPort from *int to *uint16. This takes a
defensive stance against invalid inputs at JSON decode time.

'tailscale set --relay-server-port' is currently the only input to this
pref, and has always sanitized input to fit within a uint16.

Updates tailscale/corp#34591

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Sachin Iyer 53476ce872 ipn/serve: validate service paths in HasPathHandler
Fixes #17839

Signed-off-by: Sachin Iyer <siyer@detail.dev>
2 months ago
Claus Lensbøl c54d243690
net/tstun: add TSMPDiscoAdvertisement to TSMPPing (#17995)
Adds a new types of TSMP messages for advertising disco keys keys
to/from a peer, and implements the advertising triggered by a TSMP ping.

Needed as part of the effort to cache the netmap and still let clients
connect without control being reachable.

Updates #12639

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
Co-authored-by: James Tucker <james@tailscale.com>
2 months ago
Alex Chan b38dd1ae06 ipn/ipnlocal: don't panic if there are no suitable exit nodes
In suggestExitNodeLocked, if no exit node candidates have a home DERP or
valid location info, `bestCandidates` is an empty slice. This slice is
passed to `selectNode` (`randomNode` in prod):

```go func randomNode(nodes views.Slice[tailcfg.NodeView], …) tailcfg.NodeView {
	…
	return nodes.At(rand.IntN(nodes.Len()))
}
```

An empty slice becomes a call to `rand.IntN(0)`, which panics.

This patch changes the behaviour, so if we've filtered out all the
candidates before calling `selectNode`, reset the list and then pick
from any of the available candidates.

This patch also updates our tests to give us more coverage of `randomNode`,
so we can spot other potential issues.

Updates #17661

Change-Id: I63eb5e4494d45a1df5b1f4b1b5c6d5576322aa72
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Fran Bull f4a4bab105 tsconsensus: skip integration tests in CI
There is an issue to add non-integration tests: #18022

Fixes #15627 #16340

Signed-off-by: Fran Bull <fran@tailscale.com>
2 months ago
Brad Fitzpatrick ac0b15356d tailcfg, control/controlclient: start moving MapResponse.DefaultAutoUpdate to a nodeattr
And fix up the TestAutoUpdateDefaults integration tests as they
weren't testing reality: the DefaultAutoUpdate is supposed to only be
relevant on the first MapResponse in the stream, but the tests weren't
testing that. They were instead injecting a 2nd+ MapResponse.

This changes the test control server to add a hook to modify the first
map response, and then makes the test control when the node goes up
and down to make new map responses.

Also, the test now runs on macOS where the auto-update feature being
disabled would've previously t.Skipped the whole test.

Updates #11502

Change-Id: If2319bd1f71e108b57d79fe500b2acedbc76e1a6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Simon Law 848978e664
ipn/ipnlocal: test traffic-steering when feature is not enabled (#17997)
In PR tailscale/corp#34401, the `traffic-steering` feature flag does
not automatically enable traffic steering for all nodes. Instead, an
admin must add the `traffic-steering` node attribute to each client
node that they want opted-in.

For backwards compatibility with older clients, tailscale/corp#34401
strips out the `traffic-steering` node attribute if the feature flag
is not enabled, even if it is set in the policy file. This lets us
safely disable the feature flag.

This PR adds a missing test case for suggested exit nodes that have no
priority.

Updates tailscale/corp#34399

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2 months ago
Nick Khyl 7073f246d3 ipn/ipnlocal: do not call controlclient.Client.Shutdown with b.mu held
This fixes a regression in #17804 that caused a deadlock.

Updates #18052

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
David Bond d4821cdc2f
cmd/k8s-operator: allow HA ingresses to be deleted when VIP service does not exist (#18050)
This commit fixes a bug in our HA ingress reconciler where ingress resources would
be stuck in a deleting state should their associated VIP service be deleted within
control.

The reconciliation loop would check for the existence of the VIP service and if not
found perform no additional cleanup steps. The code has been modified to continue
onwards even if the VIP service is not found.

Fixes: https://github.com/tailscale/tailscale/issues/18049

Signed-off-by: David Bond <davidsbond93@gmail.com>
2 months ago
Simon Law 9c3a2aa797
ipn/ipnlocal: replace log.Printf with logf (#18045)
Updates #cleanup

Signed-off-by: Simon Law <sfllaw@tailscale.com>
2 months ago
Jordan Whited 7426eca163 cmd/tailscale,feature/relayserver,ipn: add relay-server-static-endpoints set flag
Updates tailscale/corp#31489
Updates #17791

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Jordan Whited 755309c04e net/udprelay: use blake2s-256 MAC for handshake challenge
This commit replaces crypto/rand challenge generation with a blake2s-256
MAC. This enables the peer relay server to respond to multiple forward
disco.BindUDPRelayEndpoint messages per handshake generation without
sacrificing the proof of IP ownership properties of the handshake.

Responding to multiple forward disco.BindUDPRelayEndpoint messages per
handshake generation improves client address/path selection where
lowest client->server path/addr one-way delay does not necessarily
equate to lowest client<->server round trip delay.

It also improves situations where outbound traffic is filtered
independent of input, and the first reply
disco.BindUDPRelayEndpointChallenge message is dropped on the reply
path, but a later reply using a different source would make it through.

Reduction in serverEndpoint state saves 112 bytes per instance, trading
for slightly more expensive crypto ops: 277ns/op vs 321ns/op on an M1
Macbook Pro.

Updates tailscale/corp#34414

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Tom Proctor 6637003cc8 cmd/cigocacher,go.mod: add cigocacher cmd
Adds cmd/cigocacher as the client to cigocached for Go caching over
HTTP. The HTTP cache is best-effort only, and builds will fall back to
disk-only cache if it's not available, much like regular builds.

Not yet used in CI; that will follow in another PR once we have runners
available in this repo with the right network setup for reaching
cigocached.

Updates tailscale/corp#10808

Change-Id: I13ae1a12450eb2a05bd9843f358474243989e967
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
2 months ago
Andrew Dunham 698eecda04 ipn/ipnlocal: fix panic in driveTransport on network error
When the underlying transport returns a network error, the RoundTrip
method returns (nil, error). The defer was attempting to access resp
without checking if it was nil first, causing a panic. Fix this by
checking for nil in the defer.

Also changes driveTransport.tr from *http.Transport to http.RoundTripper
and adds a test.

Fixes #17306

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
Change-Id: Icf38a020b45aaa9cfbc1415d55fd8b70b978f54c
2 months ago
Andrew Dunham a20cdb5c93 tstest/integration/testcontrol: de-flake TestUserMetricsRouteGauges
SetSubnetRoutes was not sending update notifications to nodes when their
approved routes changed, causing nodes to not fetch updated netmaps with
PrimaryRoutes populated. This resulted in TestUserMetricsRouteGauges
flaking because it waited for PrimaryRoutes to be set, which only happened
if the node happened to poll for other reasons.

Now send updateSelfChanged notification to affected nodes so they fetch
an updated netmap immediately.

Fixes #17962

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2 months ago
Andrew Dunham 16587746ed portlist,tstest: skip tests on kernels with /proc/net/tcp regression
Linux kernel versions 6.6.102-104 and 6.12.42-45 have a regression
in /proc/net/tcp that causes seek operations to fail with "illegal seek".
This breaks portlist tests on these kernels.

Add kernel version detection for Linux systems and a SkipOnKernelVersions
helper to tstest. Use it to skip affected portlist tests on the broken
kernel versions.

Thanks to philiptaron for the list of kernels with the issue and fix.

Updates #16966

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
2 months ago
Nick Khyl 1ccece0f78 util/eventbus: use unbounded event queues for DeliveredEvents in subscribers
Bounded DeliveredEvent queues reduce memory usage, but they can deadlock under load.
Two common scenarios trigger deadlocks when the number of events published in a short
period exceeds twice the queue capacity (there's a PublishedEvent queue of the same size):
 - a subscriber tries to acquire the same mutex as held by a publisher, or
 - a subscriber for A events publishes B events

Avoiding these scenarios is not practical and would limit eventbus usefulness and reduce its adoption,
pushing us back to callbacks and other legacy mechanisms. These deadlocks already occurred in customer
devices, dev machines, and tests. They also make it harder to identify and fix slow subscribers and similar
issues we have been seeing recently.

Choosing an arbitrary large fixed queue capacity would only mask the problem. A client running
on a sufficiently large and complex customer environment can exceed any meaningful constant limit,
since event volume depends on the number of peers and other factors. Behavior also changes
based on scheduling of publishers and subscribers by the Go runtime, OS, and hardware, as the issue
is essentially a race between publishers and subscribers. Additionally, on lower-end devices,
an unreasonably high constant capacity is practically the same as using unbounded queues.

Therefore, this PR changes the event queue implementation to be unbounded by default.
The PublishedEvent queue keeps its existing capacity of 16 items, while subscribers'
DeliveredEvent queues become unbounded.

This change fixes known deadlocks and makes the system stable under load,
at the cost of higher potential memory usage, including cases where a queue grows
during an event burst and does not shrink when load decreases.

Further improvements can be implemented in the future as needed.

Fixes #17973
Fixes #18012

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Jordan Whited 9245c7131b feature/relayserver: don't publish from within a subscribe fn goroutine
Updates #17830

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
Claus Lensbøl e7f5ca1d5e
wgengine/userspace: run link change subscribers in eventqueue (#18024)
Updates #17996

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
2 months ago
Nick Khyl 3780f25d51 util/eventbus: add tests for a subscriber publishing events
As of 2025-11-20, publishing more events than the eventbus's
internal queues can hold may deadlock if a subscriber tries
to publish events itself.

This commit adds a test that demonstrates this deadlock,
and skips it until the bug is fixed.

Updates #18012

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Nick Khyl 016ccae2da util/eventbus: add tests for a subscriber trying to acquire the same mutex as a publisher
As of 2025-11-20, publishing more events than the eventbus's
internal queues can hold may deadlock if a subscriber tries
to acquire a mutex that can also be held by a publisher.

This commit adds a test that demonstrates this deadlock,
and skips it until the bug is fixed.

Updates #17973

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2 months ago
Alex Chan ce95bc77fb tka: don't panic if no clock set in tka.Mem
This is causing confusing panics in tailscale/corp#34485. We'll keep
using the tka.ChonkMem constructor as much as we can, but don't panic
if you create a tka.Mem directly -- we know what the sensible thing is.

Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>

Change-Id: I49309f5f403fc26ce4f9a6cf0edc8eddf6a6f3a4
2 months ago
Andrew Lytvynov c679aaba32
cmd/tailscaled,ipn: show a health warning when state store fails to open (#17883)
With the introduction of node sealing, store.New fails in some cases due
to the TPM device being reset or unavailable. Currently it results in
tailscaled crashing at startup, which is not obvious to the user until
they check the logs.

Instead of crashing tailscaled at startup, start with an in-memory store
with a health warning about state initialization and a link to (future)
docs on what to do. When this health message is set, also block any
login attempts to avoid masking the problem with an ephemeral node
registration.

Updates #15830
Updates #17654

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Andrew Lytvynov de8ed203e0
go.mod: bump golang.org/x/crypto (#18011)
Pick up fixes for https://pkg.go.dev/vuln/GO-2025-4134

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Harry Harpham ac74d28190
ipn/ipnlocal: add validations when setting serve config (#17950)
These validations were previously performed in the CLI frontend. There
are two motivations for moving these to the local backend:
1. The backend controls synchronization around the relevant state, so
   only the backend can guarantee many of these validations.
2. Doing these validations in the back-end avoids the need to repeat
   them across every frontend (e.g. the CLI and tsnet).

Updates tailscale/corp#27200

Signed-off-by: Harry Harpham <harry@tailscale.com>
2 months ago
David Bond 42a5262016
cmd/k8s-operator: add multi replica support for recorders (#17864)
This commit adds the `spec.replicas` field to the `Recorder` custom
resource that allows for a highly available deployment of `tsrecorder`
within a kubernetes cluster.

Many changes were required here as the code hard-coded the assumption
of a single replica. This has required a few loops, similar to what we
do for the `Connector` resource to create auth and state secrets. It
was also required to add a check to remove dangling state and auth
secrets should the recorder be scaled down.

Updates: https://github.com/tailscale/tailscale/issues/17965

Signed-off-by: David Bond <davidsbond93@gmail.com>
2 months ago
Jonathan Nobels 682172ca2d net/netns: remove spammy logs for interface binding caps
fixes tailscale/tailscale#17990

The logging for the netns caps is spammy.  Log only on changes
to the values and don't log Darwin specific stuff on non Darwin
clients.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2 months ago
Brad Fitzpatrick 7d19813618 net/batching: fix import formatting
From #17842

Updates #cleanup

Change-Id: Ie041b50659361b50558d5ec1f557688d09935f7c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
David Bond 86a849860e
cmd/k8s-operator: use stable image for k8s-nameserver (#17985)
This commit modifies the kubernetes operator to use the "stable" version
of `k8s-nameserver` by default.

Updates: https://github.com/tailscale/corp/issues/19028

Signed-off-by: David Bond <davidsbond93@gmail.com>
2 months ago
KevinLiang10 a0d059d74c
cmd/tailscale/cli: allow remote target as service destination (#17607)
This commit enables user to set service backend to remote destinations, that can be a partial
URL or a full URL. The commit also prevents user to set remote destinations on linux system
when socket mark is not working. For user on any version of mac extension they can't serve a
service either. The socket mark usability is determined by a new local api.

Fixes tailscale/corp#24783

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
2 months ago
License Updater 12c598de28 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2 months ago
Alex Chan 976bf24f5e ipn/ipnlocal: remove the always-true CanSupportNetworkLock()
Now that we support using an in-memory backend for TKA state (#17946),
this function always returns `nil` – we can always support Network Lock.
We don't need it any more.

Plus, clean up a couple of errant TODOs from that PR.

Updates tailscale/corp#33599

Change-Id: Ief93bb9adebb82b9ad1b3e406d1ae9d2fa234877
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Brad Fitzpatrick 6ac4356bce util/eventbus: simplify some reflect in Bus.pump
Updates #cleanup

Change-Id: Ib7b497e22c6cdd80578c69cf728d45754e6f909e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Alex Chan 336df56f85 cmd/tailscale/cli: remove Latin abbreviations from CLI help text
Our style guide recommends avoiding Latin abbreviations in technical
documentation, which includes the CLI help text. This is causing linter
issues for the docs site, because this help text is copied into the docs.
See http://go/style-guide/kb/language-and-grammar/abbreviations#latin-abbreviations

Updates #cleanup

Change-Id: I980c28d996466f0503aaaa65127685f4af608039
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Alex Chan aeda3e8183 ipn/ipnlocal: reduce profileManager boilerplate in network-lock tests
Updates tailscale/corp#33537

Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Raj Singh 62d64c05e1
cmd/k8s-operator: fix type comparison in apiserver proxy template (#17981)
ArgoCD sends boolean values but the template expects strings, causing
"incompatible types for comparison" errors. Wrap values with toString
so both work.

Fixes #17158

Signed-off-by: Raj Singh <raj@tailscale.com>
2 months ago
Alex Chan e1dd9222d4 ipn/ipnlocal, tka: compact TKA state after every sync
Previously a TKA compaction would only run when a node starts, which means a long-running node could use unbounded storage as it accumulates ever-increasing amounts of TKA state. This patch changes TKA so it runs a compaction after every sync.

Updates https://github.com/tailscale/corp/issues/33537

Change-Id: I91df887ea0c5a5b00cb6caced85aeffa2a4b24ee
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
David Bond 38ccdbe35c
cmd/k8s-operator: default to stable image (#17848)
This commit modifies the helm/static manifest configuration for the
k8s-operator to prefer the stable image tag. This avoids making those
using static manifests seeing unstable behaviour by default if they
do not manually make the change.

This is managed for us when using helm but not when generating the
static manifests.

Updates https://github.com/tailscale/tailscale/issues/10655

Signed-off-by: David Bond <davidsbond93@gmail.com>
2 months ago
Brad Fitzpatrick 408336a089 feature/featuretags: add CacheNetMap feature tag for upcoming work
(trying to get in smaller obvious chunks ahead of later PRs to make
them smaller)

Updates #17925

Change-Id: I184002001055790484e4792af8ffe2a9a2465b2e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 5b0c57f497 tailcfg: add some omitzero, adjust some omitempty to omitzero
Updates tailscale/corp#25406

Change-Id: I7832dbe3dce3774bcc831e3111feb75bcc9e021d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Joe Tsai 3b865d7c33
cmd/netlogfmt: support resolving IP addresses to synonymous labels (#17955)
We now embed node information into network flow logs.
By default, netlogfmt still prints out using Tailscale IP addresses.
Support a "--resolve-addrs=TYPE" flag that can be used to specify
resolving IP addresses as node IDs, hostnames, users, or tags.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2 months ago
James Tucker c09c95ef67 types/key,wgengine/magicsock,control/controlclient,ipn: add debug disco key rotation
Adds the ability to rotate discovery keys on running clients, needed for
testing upcoming disco key distribution changes.

Introduces key.DiscoKey, an atomic container for a disco private key,
public key, and the public key's ShortString, replacing the prior
separate atomic fields.

magicsock.Conn has a new RotateDiscoKey method, and access to this is
provided via localapi and a CLI debug command.

Note that this implementation is primarily for testing as it stands, and
regular use should likely introduce an additional mechanism that allows
the old key to be used for some time, to provide a seamless key rotation
rather than one that invalidates all sessions.

Updates tailscale/corp#34037

Signed-off-by: James Tucker <james@tailscale.com>
2 months ago
Fran Bull da508c504d appc: add ippool type
As part of the conn25 work we will want to be able to keep track of a
pool of IP Addresses and know which have been used and which have not.

Fixes tailscale/corp#34247

Signed-off-by: Fran Bull <fran@tailscale.com>
2 months ago
Alex Chan d0daa5a398 tka: marshal AUMHash totext even if Tailnet Lock is omitted
We use `tka.AUMHash` in `netmap.NetworkMap`, and we serialise it as JSON
in the `/debug/netmap` C2N endpoint. If the binary omits Tailnet Lock support,
the debug endpoint returns an error because it's unable to marshal the
AUMHash.

This patch adds a sentinel value so this marshalling works, and we can
use the debug endpoint.

Updates https://github.com/tailscale/tailscale/issues/17115

Signed-off-by: Alex Chan <alexc@tailscale.com>

Change-Id: I51ec1491a74e9b9f49d1766abd89681049e09ce4
2 months ago
Anton Tolchanov 04a9d25a54 tka: mark young AUMs as active even if the chain is long
Existing compaction logic seems to have had an assumption that
markActiveChain would cover a longer part of the chain than
markYoungAUMs. This prevented long, but fresh, chains, from being
compacted correctly.

Updates tailscale/corp#33537

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2 months ago
Brad Fitzpatrick bd29b189fe types/netmap,*: remove some redundant fields from NetMap
Updates #12639

Change-Id: Ia50b15529bd1c002cdd2c937cdfbe69c06fa2dc8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 2a6cbb70d9 .github/workflows: make go_generate check detect new files
Updates #17957

Change-Id: I904fd5b544ac3090b58c678c4726e7ace41a52dd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 4e2f2d1088 feature/buildfeatures: re-run go generate
6a73c0bdf5 added a feature tag but didn't re-run go generate on ./feature/buildfeatures.

Updates #9192

Change-Id: I7819450453e6b34c60cad29d2273e3e118291643
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Alex Chan af7c26aa05 cmd/vet/jsontags: fix a typo in an error message
Updates #17945

Change-Id: I8987271420feb190f5e4d85caff305c8d4e84aae
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Alex Chan 85373ef822 tka: move RemoveAll() to CompactableChonk
I added a RemoveAll() method on tka.Chonk in #17946, but it's only used
in the node to purge local AUMs. We don't need it in the SQLite storage,
which currently implements tka.Chonk, so move it to CompactableChonk
instead.

Also add some automated tests, as a safety net.

Updates tailscale/corp#33599

Change-Id: I54de9ccf1d6a3d29b36a94eccb0ebd235acd4ebc
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Alex Chan c2e474e729 all: rename variables with lowercase-l/uppercase-I
See http://go/no-ell

Signed-off-by: Alex Chan <alexc@tailscale.com>

Updates #cleanup

Change-Id: I8c976b51ce7a60f06315048b1920516129cc1d5d
2 months ago
James 'zofrex' Sanderson 9048ea25db
ipn/localapi: log calls to localapi (#17880)
Updates tailscale/corp#34238

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2 months ago
James 'zofrex' Sanderson a2e9dfacde
cmd/tailscale/cli: warn if a simple up would change prefs (#17877)
Updates tailscale/corp#21570

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2 months ago
Joe Tsai 4860c460f5
wgengine/netlog: strip dot suffix from node name (#17954)
The REST API does not return a node name
with a trailing dot, while the internal node name
reported in the netmap does have one.

In order to be consistent with the API,
strip the dot when recording node information.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2 months ago
James Tucker 41662f5128 ssh/tailssh: fix incubator tests on macOS arm64
Perform a path check first before attempting exec of `true`.

Try /usr/bin/true first, as that is now and increasingly so, the more
common and more portable path.

Fixes tests on macOS arm64 where exec was returning a different kind of
path error than previously checked.

Updates #16569

Signed-off-by: James Tucker <james@tailscale.com>
2 months ago
Andrew Lytvynov 26f9b50247
feature/tpm: disable dictionary attack protection on sealing key (#17952)
DA protection is not super helpful because we don't set an authorization
password on the key. But if authorization fails for other reasons (like
TPM being reset), we will eventually cause DA lockout with tailscaled
trying to load the key. DA lockout then leads to (1) issues for other
processes using the TPM and (2) the underlying authorization error being
masked in logs.

Updates #17654

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Brad Fitzpatrick f1cddc6ecf ipn{,/local},cmd/tailscale: add "sync" flag and pref to disable control map poll
For manual (human) testing, this lets the user disable control plane
map polls with "tailscale set --sync=false" (which survives restarts)
and "tailscale set --sync" to restore.

A high severity health warning is shown while this is active.

Updates #12639
Updates #17945

Change-Id: I83668fa5de3b5e5e25444df0815ec2a859153a6d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 165a24744e tka: fix typo in comment
Let's fix all the typos, which lets the code be more readable, lest we
confuse our readers.

Updates #cleanup

Change-Id: I4954601b0592b1fda40269009647bb517a4457be
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Alex Chan 1723cb83ed ipn/ipnlocal: use an in-memory TKA store if FS is unavailable
This requires making the internals of LocalBackend a bit more generic,
and implementing the `tka.CompactableChonk` interface for `tka.Mem`.

Signed-off-by: Alex Chan <alexc@tailscale.com>

Updates https://github.com/tailscale/corp/issues/33599
2 months ago
Andrew Lytvynov d01081683c
go.mod: bump golang.org/x/crypto (#17907)
Pick up a fix for https://pkg.go.dev/vuln/GO-2025-4116 (even though
we're not affected).

Updates #cleanup

Change-Id: I9f2571b17c1f14db58ece8a5a34785805217d9dd

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2 months ago
Alex Chan 200383dce5 various: add more missing apostrophes in comments
Updates #cleanup

Change-Id: I79a0fda9783064a226ee9bcee2c1148212f6df7b
Signed-off-by: Alex Chan <alexc@tailscale.com>
2 months ago
Brad Fitzpatrick 1e95bfa184 ipn: fix typo in comment
Updates #cleanup

Change-Id: Iec66518abd656c64943a58eb6d92f342e627a613
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick a5b2f18567 control/controlclient: remove some public API, move to Options & test-only
Includes adding StartPaused, which will be used in a future change to
enable netmap caching testing.

Updates #12639

Change-Id: Iec39915d33b8d75e9b8315b281b1af2f5d13a44a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Alex Chan 139c395d7d cmd/tailscale/cli: stabilise the output of `tailscale lock log --json`
This patch changes the behaviour of `tailscale lock log --json` to make
it more useful for users. It also introduces versioning of our JSON output.

## Changes to `tailscale lock log --json`

Previously this command would print the hash and base64-encoded bytes of
each AUM, and users would need their own CBOR decoder to interpret it in
a useful way:

```json
[
  {
    "Hash": [
      80,
      136,
      151,
      …
    ],
    "Change": "checkpoint",
    "Raw": "pAEFAvYFpQH2AopYIAkPN+8V3cJpkoC5ZY2+RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm+NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl/D93I1M56/rowU+UIlYIPZ/SxT9EA2Idy9kaCbsFzjX/s3Ms7584wWGbWd/f/QAWCBHYZzYiAPpQ+NXN+1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK/RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz+dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav/k6dDF4GiAVgg5Eh00epI7PPW2sjKCc/nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc+m45ay5PB/OB4AA9Fdki4KJq9Ll+PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
  }
]
```

Now we print the AUM in an expanded form that can be easily read by scripts,
although we include the raw bytes for verification and auditing.

```json
{
  "SchemaVersion": "1",
  "Messages": [
    {
      "Hash": "KCEJPRKNSXJG2TPH3EHQRLJNLIIK2DV53FUNPADWA7BZJWBDRXZQ",
      "AUM": {
        "MessageKind": "checkpoint",
        "PrevAUMHash": null,
        "Key": null,
        "KeyID": null,
        "State": {
          …
        },
        "Votes": null,
        "Meta": null,
        "Signatures": [
          {
            "KeyID": "tlpub:e44874d1ea48ecf3d6dac8ca09cfe70dc958ad83b656393432016c3ed229c8d6",
            "Signature": "8yAKKHPpuOWsuTwfzgeAAPRXZIuCiavS5fjxeiCR2JTlYaU23NxNChezg7tVlEXdH+z151u9na/PQknxsSBwBQ=="
          }
        ]
      },
      "Raw": "pAEFAvYFpQH2AopYIAkPN-8V3cJpkoC5ZY2-RI2Bcg2q5G7tRAQQd67W3YpnWCDPOo4KGeQBd8hdGsjoEQpSXyiPdlm-NXAlJ5dS1qEbFlggylNJDQM5ZQ2ULNsXxg2ZBFkPl_D93I1M56_rowU-UIlYIPZ_SxT9EA2Idy9kaCbsFzjX_s3Ms7584wWGbWd_f_QAWCBHYZzYiAPpQ-NXN-1Wn2fopQYk4yl7kNQcMXUKNAdt1lggcfjcuVACOH0J9pRNvYZQFOkbiBmLOW1hPKJsbC1D1GdYIKrJ38XMgpVMuTuBxM4YwoLmrK_RgXQw1uVEL3cywl3QWCA0FilVVv8uys8BNhS62cfNvCew1Pw5wIgSe3Prv8d8pFggQrwIt6ldYtyFPQcC5V18qrCnt7VpThACaz5RYzpx7RNYIKskOA7UoNiVtMkOrV2QoXv6EvDpbO26a01lVeh8UCeEA4KjAQECAQNYIORIdNHqSOzz1trIygnP5w3JWK2DtlY5NDIBbD7SKcjWowEBAgEDWCD27LpxiZNiA19k0QZhOWmJRvBdK2mz-dHu7rf0iGTPFwQb69Gt42fKNn0FGwRUiav_k6dDF4GiAVgg5Eh00epI7PPW2sjKCc_nDclYrYO2Vjk0MgFsPtIpyNYCWEDzIAooc-m45ay5PB_OB4AA9Fdki4KJq9Ll-PF6IJHYlOVhpTbc3E0KF7ODu1WURd0f7PXnW72dr89CSfGxIHAF"
    }
  ]
}
```

This output was previously marked as unstable, and it wasn't very useful,
so changing it should be fine.

## Versioning our JSON output

This patch introduces a way to version our JSON output on the CLI, so we
can make backwards-incompatible changes in future without breaking existing
scripts or integrations.

You can run this command in two ways:

```
tailscale lock log --json
tailscale lock log --json=1
```

Passing an explicit version number allows you to pick a specific JSON schema.
If we ever want to change the schema, we increment the version number and
users must opt-in to the new output.

A bare `--json` flag will always return schema version 1, for compatibility
with existing scripts.

Updates https://github.com/tailscale/tailscale/issues/17613
Updates https://github.com/tailscale/corp/issues/23258

Signed-off-by: Alex Chan <alexc@tailscale.com>

Change-Id: I897f78521cc1a81651f5476228c0882d7b723606
2 months ago
Brad Fitzpatrick 99b06eac49 syncs: add Mutex/RWMutex alias/wrappers for future mutex debugging
Updates #17852

Change-Id: I477340fb8e40686870e981ade11cd61597c34a20
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Andrew Dunham 3a41c0c585 ipn/ipnlocal: add PROXY protocol support to Funnel/Serve
This adds the --proxy-protocol flag to 'tailscale serve' and
'tailscale funnel', which tells the Tailscale client to prepend a PROXY
protocol[1] header when making connections to the proxied-to backend.

I've verified that this works with our existing funnel servers without
additional work, since they pass along source address information via
PeerAPI already.

Updates #7747

[1]: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt

Change-Id: I647c24d319375c1b33e995555a541b7615d2d203
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2 months ago
Brad Fitzpatrick 653d0738f9 types/netmap: remove PrivateKey from NetworkMap
It's an unnecessary nuisance having it. We go out of our way to redact
it in so many places when we don't even need it there anyway.

Updates #12639

Change-Id: I5fc72e19e9cf36caeb42cf80ba430873f67167c3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 98aadbaf54 util/cache: remove unused code
Updates #cleanup

Change-Id: I9be7029c5d2a7d6297125d0147e93205a7c68989
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Brad Fitzpatrick 4e01e8a66e wgengine/netlog: fix send to closed channel in test
Fixes #17922

Change-Id: I2cd600b0ecda389079f2004985ac9a25ffbbfdd1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2 months ago
Avery Palmer 8aa46a3956 util/clientmetric: fix regression causing Metric.v to be uninitialised
m.v was uninitialised when Tailscale built with ts_omit_logtail
Fixes #17918

Signed-off-by: Avery Palmer <quagsirus@catpowered.net>
2 months ago
Xinyu Kuo 8444659ed8 cmd/tailscale/cli: fix panic in netcheck with mismatched DERP region IDs
Fixes #17564

Signed-off-by: Xinyu Kuo <gxylong@126.com>
2 months ago
Jordan Whited e1f0ad7a05
net/udprelay: implement Server.SetStaticAddrPorts (#17909)
Only used in tests for now.

Updates tailscale/corp#31489

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2 months ago
James Tucker a96ef432cf control/controlclient,ipn/ipnlocal: replace State enum with boolean flags
Remove the State enum (StateNew, StateNotAuthenticated, etc.) from
controlclient and replace it with two explicit boolean fields:
- LoginFinished: indicates successful authentication
- Synced: indicates we've received at least one netmap

This makes the state more composable and easier to reason about, as
multiple conditions can be true independently rather than being
encoded in a single enum value.

The State enum was originally intended as the state machine for the
whole client, but that abstraction moved to ipn.Backend long ago.
This change continues moving away from the legacy state machine by
representing state as a combination of independent facts.

Also adds test helpers in ipnlocal that check independent, observable
facts (hasValidNetMap, needsLogin, etc.) rather than relying on
derived state enums, making tests more robust.

Updates #12639

Signed-off-by: James Tucker <james@tailscale.com>
2 months ago
Andrew Lytvynov c5919b4ed1
feature/tpm: check IsZero in clone instead of just nil (#17884)
The key.NewEmptyHardwareAttestationKey hook returns a non-nil empty
attestationKey, which means that the nil check in Clone doesn't trigger
and proceeds to try and clone an empty key. Check IsZero instead to
reduce log spam from Clone.

As a drive-by, make tpmAvailable check a sync.Once because the result
won't change.

Updates #17882

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
Andrew Lytvynov 888a5d4812
ipn/localapi: use constant-time comparison for RequiredPassword (#17906)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
Alex Chan 9134440008 various: adds missing apostrophes to comments
Updates #cleanup

Change-Id: I7bf29cc153c3c04e087f9bdb146c3437bed0129a
Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Simon Law bd36817e84
scripts/installer.sh: compare major versions numerically (#17904)
Most /etc/os-release files set the VERSION_ID to a `MAJOR.MINOR`
string, but we were trying to compare this numerically against a major
version number. I can only assume that Linux Mint used switched from a
plain integer, since shells only do integer comparisons.

This patch extracts a VERSION_MAJOR from the VERSION_ID using
parameter expansion and unifies all the other ad-hoc comparisons to
use it.

Fixes #15841

Signed-off-by: Simon Law <sfllaw@tailscale.com>
Co-authored-by: Xavier <xhienne@users.noreply.github.com>
3 months ago
M. J. Fromberger ab4b990d51
net/netmon: do not abandon a subscriber when exiting early (#17899)
LinkChangeLogLimiter keeps a subscription to track rate limits for log
messages.  But when its context ended, it would exit the subscription loop,
leaving the subscriber still alive. Ensure the subscriber gets cleaned up
when the context ends, so we don't stall event processing.

Updates tailscale/corp#34311

Change-Id: I82749e482e9a00dfc47f04afbc69dd0237537cb2
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Brad Fitzpatrick ce10f7c14c wgengine/wgcfg/nmcfg: reduce wireguard reconfig log spam
On the corp tailnet (using Mullvad exit nodes + bunch of expired
devices + subnet routers), these were generating big ~35 KB blobs of
logging regularly.

This logging shouldn't even exist at this level, and should be rate
limited at a higher level, but for now as a bandaid, make it less
spammy.

Updates #cleanup

Change-Id: I0b5e9e6e859f13df5f982cd71cd5af85b73f0c0a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Andrew Dunham 208a32af5b logpolicy: fix nil pointer dereference with invalid TS_LOG_TARGET
When TS_LOG_TARGET is set to an invalid URL, url.Parse returns an error
and nil pointer, which caused a panic when accessing u.Host.

Now we check the error from url.Parse and log a helpful message while
falling back to the default log host.

Fixes #17792

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
3 months ago
Brad Fitzpatrick 052602752f control/controlclient: make Observer optional
As a baby step towards eventbus-ifying controlclient, make the
Observer optional.

This also means callers that don't care (like this network lock test,
and some tests in other repos) can omit it, rather than passing in a
no-op one.

Updates #12639

Change-Id: Ibd776b45b4425c08db19405bc3172b238e87da4e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Jordan Whited 0285e1d5fb
feature/relayserver: fix Shutdown() deadlock (#17898)
Updates #17894

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
James 'zofrex' Sanderson 124301fbb6
ipn/ipnlocal: log prefs changes and reason in Start (#17876)
Updates tailscale/corp#34238

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
3 months ago
Alex Chan b5cd29932e tka: add a test for unmarshaling existing AUMs
Updates https://github.com/tailscale/tailscale/issues/17613

Change-Id: I693a580949eef59263353af6e7e03a7af9bbaa0b
Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Jordan Whited 9e4d1fd87f
feature/relayserver,ipn/ipnlocal,net/udprelay: plumb DERPMap (#17881)
This commit replaces usage of local.Client in net/udprelay with DERPMap
plumbing over the eventbus. This has been a longstanding TODO. This work
was also accelerated by a memory leak in net/http when using
local.Client over long periods of time. So, this commit also addresses
said leak.

Updates #17801

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
Brad Fitzpatrick 146ea42822 ipn/ipnlocal: remove all the weird locking (LockedOnEntry, UnlockEarly, etc)
Fixes #11649
Updates #16369

Co-authored-by: James Sanderson <jsanderson@tailscale.com>
Change-Id: I63eaa18fe870ddf81d84b949efac4d1b44c3db86
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Andrew Dunham 08e74effc0 cmd/cloner: support cloning arbitrarily-nested maps
Fixes #17870

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
3 months ago
Naman Sood ca9b68aafd
cmd/tailscale/cli: remove service flag from funnel command (#17850)
Fixes #17849.

Signed-off-by: Naman Sood <mail@nsood.in>
3 months ago
Andrew Dunham 6ac80b7334 cmd/{cloner,viewer}: handle maps of views
Instead of trying to call View() on something that's already a View
type (or trying to Clone the view unnecessarily), we can re-use the
existing View values in a map[T]ViewType.

Fixes #17866

Signed-off-by: Andrew Dunham <andrew@tailscale.com>
3 months ago
Jordan Whited f4f9dd7f8c
net/udprelay: replace VNI pool with selection algorithm (#17868)
This reduces memory usage when tailscaled is acting as a peer relay.

Updates #17801

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
License Updater 31fe75ad9e licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
3 months ago
Fran Bull 37aa7e6935 util/dnsname: fix test error message
Updates #17788

Signed-off-by: Fran Bull <fran@tailscale.com>
3 months ago
Brad Fitzpatrick f387b1010e wgengine/wgcfg: remove two unused Config fields
They distracted me in some refactoring. They're set but never used.

Updates #17858

Change-Id: I6ec7d6841ab684a55bccca7b7cbf7da9c782694f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Fran Bull 27a0168cdc util/dnsname: increase maxNameLength to account for trailing dot
Fixes #17788

Signed-off-by: Fran Bull <fran@tailscale.com>
3 months ago
Jonathan Nobels e8d2f96449
ipn/ipnlocal, net/netns: add node cap to disable netns interface binding on netext Apple clients (#17691)
updates tailscale/corp#31571

It appears that on the latest macOS, iOS and tVOS versions, the work
that netns is doing to bind outgoing connections to the default interface (and all
of the trimmings and workarounds in netmon et al that make that work) are
not needed. The kernel is extension-aware and doing nothing, is the right
thing.  This is, however, not the case for tailscaled (which is not a
special process).

To allow us to test this assertion (and where it might break things), we add a
new node cap that turns this behaviour off only for network-extension equipped clients,
making it possible to turn this off tailnet-wide, without breaking any tailscaled
macos nodes.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
3 months ago
Sachin Iyer 16e90dcb27
net/batching: fix gro size handling for misordered UDP_GRO messages (#17842)
Fixes #17835

Signed-off-by: Sachin Iyer <siyer@detail.dev>
3 months ago
Sachin Iyer d37884c734
cmd/k8s-operator: remove early return in ingress matching (#17841)
Fixes #17834

Signed-off-by: Sachin Iyer <siyer@detail.dev>
3 months ago
Sachin Iyer 85cb64c4ff
wf: correct IPv6 link-local range from ff80::/10 to fe80::/10 (#17840)
Fixes #17833

Signed-off-by: Sachin Iyer <siyer@detail.dev>
3 months ago
Sachin Iyer 3280dac797 wgengine/router/osrouter: fix linux magicsock port changing
Fixes #17837

Signed-off-by: Sachin Iyer <siyer@detail.dev>
3 months ago
Brad Fitzpatrick 1eba5b0cbd util/eventbus: log goroutine stacks when hung in CI
Updates #17680

Change-Id: Ie48dc2d64b7583d68578a28af52f6926f903ca4f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick 42ce5c88be wgengine/magicsock: unblock Conn.Synchronize on Conn.Close
I noticed a deadlock in a test in a in-development PR where during a
shutdown storm of things (from a tsnet.Server.Close), LocalBackend was
trying to call magicsock.Conn.Synchronize but the magicsock and/or
eventbus was already shut down and no longer processing events.

Updates #16369

Change-Id: I58b1f86c8959303c3fb46e2e3b7f38f6385036f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Jordan Whited 2ad2d4d409
wgengine/magicsock: fix UDPRelayAllocReq/Resp deadlock (#17831)
Updates #17830

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
Jordan Whited 18806de400
wgengine/magicsock: validate endpoint.derpAddr in Conn.onUDPRelayAllocResp (#17828)
Otherwise a zero value will panic in Conn.sendUDPStd.

Updates #17827

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
Brad Fitzpatrick 4650061326 ipn/ipnlocal: fix state_test data race seen in CI
Unfortunately I closed the tab and lost it in my sea of CI failures
I'm currently fighting.

Updates #cleanup

Change-Id: I4e3a652d57d52b75238f25d104fc1987add64191
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick 6e24f50946 tsnet: add tstest.Shard on the slow tests
So they're not all run N times on the sharded oss builders
and are only run one time each.

Updates tailscale/corp#28679

Change-Id: Ie21e84b06731fdc8ec3212eceb136c8fc26b0115
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick 8ed6bb3198 ipn/ipnlocal: move vipServiceHash etc to serve.go, out of local.go
Updates #12614

Change-Id: I3c16b94fcb997088ff18d5a21355e0279845ed7e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick e0e8731130 feature, ipn/ipnlocal: add, use feature.CanSystemdStatus for more DCE
When systemd notification support was omitted from the build, or on
non-Linux systems, we were unnecessarily emitting code and generating
garbage stringifying addresses upon transition to the Running state.

Updates #12614

Change-Id: If713f47351c7922bb70e9da85bf92725b25954b9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Jordan Whited e059382174
wgengine/magicsock: clean up determineEndpoints docs (#17822)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
3 months ago
Brad Fitzpatrick fe5501a4e9 wgengine: make getStatus a bit cheaper (less alloc-y)
This removes one of the O(n=peers) allocs in getStatus, as
Engine.getStatus happens more often than Reconfig.

Updates #17814

Change-Id: I8a87fbebbecca3aedadba38e46cc418fd163c2b0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Alex Chan 4c67df42f6 tka: log a better error if there are no chain candidates
Previously if `chains` was empty, it would be passed to `computeActiveAncestor()`,
which would fail with the misleading error "multiple distinct chains".

Updates tailscale/corp#33846

Signed-off-by: Alex Chan <alexc@tailscale.com>
Change-Id: Ib93a755dbdf4127f81cbf69f3eece5a388db31c8
3 months ago
Alex Chan c7dbd3987e tka: remove an unused parameter from `computeActiveAncestor`
Updates #cleanup

Change-Id: I86ee7a0d048dafc8c0d030291261240050451721
Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Andrew Lytvynov ae3dff15e4
ipn/ipnlocal: clean up some of the weird locking (#17802)
* lock released early just to call `b.send` when it can call
  `b.sendToLocked` instead
* `UnlockEarly` called to release the lock before trivially fast
  operations, we can wait for a defer there

Updates #11649

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
Brad Fitzpatrick 2e265213fd tsnet: fix TestConn to be fast, not flaky
Fixes #17805

Change-Id: I36e37cb0cfb2ea7b2341fd4b9809fbf1dd46d991
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick de733c5951 tailcfg: kill off rest of HairPinning symbols
It was disabled in May 2024 in #12205 (9eb72bb51).

This removes the unused symbols.

Updates #188
Updates tailscale/corp#19106
Updates tailscale/corp#19116

Change-Id: I5208b7b750b18226ed703532ed58c4ea17195a8e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick 875a9c526d tsnet: skip a 30s long flaky-ish test on macOS
Updates #17805

Change-Id: I540f50d067eee12e430dfd9de6871dc784fffb8a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Raj Singh bab5e68d0a
net/udprelay: use GetGlobalAddrs and add local port endpoint (#17797)
Use GetGlobalAddrs() to discover all STUN endpoints, handling bad NATs
that create multiple mappings. When MappingVariesByDestIP is true, also
add the first STUN IPv4 address with the relay's local port for static
port mapping scenarios.

Updates #17796

Signed-off-by: Raj Singh <raj@tailscale.com>
3 months ago
Tom Proctor d4c5b278b3 cmd/k8s-operator: support workload identity federation
The feature is currently in private alpha, so requires a tailnet feature
flag. Initially focuses on supporting the operator's own auth, because the
operator is the only device we maintain that uses static long-lived
credentials. All other operator-created devices use single-use auth keys.

Testing steps:

* Create a cluster with an API server accessible over public internet
* kubectl get --raw /.well-known/openid-configuration | jq '.issuer'
* Create a federated OAuth client in the Tailscale admin console with:
  * The issuer from the previous step
  * Subject claim `system:serviceaccount:tailscale:operator`
  * Write scopes services, devices:core, auth_keys
  * Tag tag:k8s-operator
* Allow the Tailscale control plane to get the public portion of
  the ServiceAccount token signing key without authentication:
  * kubectl create clusterrolebinding oidc-discovery \
      --clusterrole=system:service-account-issuer-discovery \
      --group=system:unauthenticated
* helm install --set oauth.clientId=... --set oauth.audience=...

Updates #17457

Change-Id: Ib29c85ba97b093c70b002f4f41793ffc02e6c6e9
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
3 months ago
Tom Proctor 1ed117dbc0 cmd/k8s-operator: remove Services feature flag detection
Now that the feature is in beta, no one should encounter this error.

Updates #cleanup

Change-Id: I69ed3f460b7f28c44da43ce2f552042f980a0420
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
3 months ago
Joe Tsai 5b40f0bc54
cmd/vet: add static vet checker that runs jsontags (#17778)
This starts running the jsontags vet checker on the module.
All existing findings are adding to an allowlist.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai 446752687c
cmd/vet: move jsontags into vet (#17777)
The cmd/jsontags is non-idiomatic since it is not a main binary.
Move it to a vet directory, which will eventually contain a vettool binary.

Update tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai 77123a569b
wgengine/netlog: include node OS in logged attributes (#17755)
Include the node's OS with network flow log information.

Refactor the JSON-length computation to be a bit more precise.

Updates tailscale/corp#33352
Fixes tailscale/corp#34030

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Andrew Lytvynov db7dcd516f
Revert "control/controlclient: back out HW key attestation (#17664)" (#17732)
This reverts commit a760cbe33f.

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
M. J. Fromberger 4c856078e4
util/eventbus: block for the subscriber during SubscribeFunc close (#17642)
Prior to this change a SubscriberFunc treated the call to the subscriber's
function as the completion of delivery. But that means when we are closing the
subscriber, that callback could continue to execute for some time after the
close returns.

For channel-based subscribers that works OK because the close takes effect
before the subscriber ever sees the event. To make the two subscriber types
symmetric, we should also wait for the callback to finish before returning.
This ensures that a Close of the client means the same thing with both kinds of
subscriber.

Updates #17638

Change-Id: I82fd31bcaa4e92fab07981ac0e57e6e3a7d9d60b
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
M. J. Fromberger 061e6266cf
util/eventbus: allow logging of slow subscribers (#17705)
Add options to the eventbus.Bus to plumb in a logger.

Route that logger in to the subscriber machinery, and trigger a log message to
it when a subscriber fails to respond to its delivered events for 5s or more.

The log message includes the package, filename, and line number of the call
site that created the subscription.

Add tests that verify this works.

Updates #17680

Change-Id: I0546516476b1e13e6a9cf79f19db2fe55e56c698
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Andrew Lytvynov f522b9dbb7
feature/tpm: protect all TPM handle operations with a mutex (#17708)
In particular on Windows, the `transport.TPMCloser` we get is not safe
for concurrent use. This is especially noticeable because
`tpm.attestationKey.Clone` uses the same open handle as the original
key. So wrap the operations on ak.tpm with a mutex and make a deep copy
with a new connection in Clone.

Updates #15830
Updates #17662
Updates #17644

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
James 'zofrex' Sanderson b6c6960e40
control/controlclient: remove unused reference to mapCtx (#17614)
Updates #cleanup

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
3 months ago
Gesa Stupperich adee8b9180 cmd/tailscale/cli/serve_v2: improve validation error
Specify the app apability that failed the test, instead of the
entire comma-separated list.

Fixes #cleanup

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
3 months ago
M. J. Fromberger 95426b79a9
logtail: avoid racing eventbus subscriptions with shutdown (#17695)
In #17639 we moved the subscription into NewLogger to ensure we would not race
subscribing with shutdown of the eventbus client. Doing so fixed that problem,
but exposed another: As we were only servicing events occasionally when waiting
for the network to come up, we could leave the eventbus to stall in cases where
a number of network deltas arrived later and weren't processed.

To address that, let's separate the concerns: As before, we'll Subscribe early
to avoid conflicts with shutdown; but instead of using the subscriber directly
to determine readiness, we'll keep track of the last-known network state in a
selectable condition that the subscriber updates for us.  When we want to wait,
we'll wait on that condition (or until our context ends), ensuring all the
events get processed in a timely manner.

Updates #17638
Updates #15160

Change-Id: I28339a372be4ab24be46e2834a218874c33a0d2d
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Fernando Serboncini d68513b0db
ipn: add support for HTTP Redirects (#17594)
Adds a new Redirect field to HTTPHandler for serving HTTP redirects
from the Tailscale serve config. The redirect URL supports template
variables ${HOST} and ${REQUEST_URI} that are resolved per request.

By default, it redirects using HTTP Status 302 (Found). For another
redirect status, like 301 - Moved Permanently, pass the HTTP status
code followed by ':' on Redirect, like: "301:https://tailscale.com"

Updates #11252
Updates #11330

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
3 months ago
Erisa A 05d2dcaf49
words: remove a fish (#17704)
Some combinations are problematic in non-fish contexts.

Updates #words

Signed-off-by: Erisa A <erisa@tailscale.com>
3 months ago
Brad Fitzpatrick 8996254647 sessionrecording: fix regression in recent http2 package change
In 3f5c560fd4 I changed to use std net/http's HTTP/2 support,
instead of pulling in x/net/http2.

But I forgot to update DialTLSContext to DialContext, which meant it
was falling back to using the std net.Dialer for its dials, instead
of the passed-in one.

The tests only passed because they were using localhost addresses, so
the std net.Dialer worked. But in prod, where a tsnet Dialer would be
needed, it didn't work, and would time out for 10 seconds before
resorting to the old protocol.

So this fixes the tests to use an isolated in-memory network to prevent
that class of problem in the future. With the test change, the old code
fails and the new code passes.

Thanks to @jasonodonnell for debugging!

Updates #17304
Updates 3f5c560fd4

Change-Id: I3602bafd07dc6548e2c62985af9ac0afb3a0e967
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Brad Fitzpatrick d5a40c01ab cmd/k8s-operator/generate: skip tests if no network or Helm is down
Updates helm/helm#31434

Change-Id: I5eb20e97ff543f883d5646c9324f50f54180851d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Harry Harpham 74f1d8bd87
cmd/tailscale/cli: unhide serve get-config and serve set-config (#17598)
Fixes tailscale/corp#33152

Signed-off-by: Harry Harpham <harry@tailscale.com>
3 months ago
Fernando Serboncini da90e3d8f2
cmd/k8s-operator: rename 'l' variables (#17700)
Single letter 'l' variables can eventually become confusing when
they're rendered in some fonts that make them similar to 1 or I.

Updates #cleanup

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
3 months ago
M. J. Fromberger 06b092388e
ipn/ipnlocal: do not stall event processing for appc route updates (#17663)
A follow-up to #17411. Put AppConnector events into a task queue, as they may
take some time to process. Ensure that the queue is stopped at shutdown so that
cleanup will remain orderly.

Because events are delivered on a separate goroutine, slow processing of an
event does not cause an immediate problem; however, a subscriber that blocks
for a long time will push back on the bus as a whole. See
https://godoc.org/tailscale.com/util/eventbus#hdr-Expected_subscriber_behavior
for more discussion.

Updates #17192
Updates #15160

Change-Id: Ib313cc68aec273daf2b1ad79538266c81ef063e3
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Alex Chan 3c19addc21 tka: rename a mutex to `mu` instead of single-letter `l`
See http://go/no-ell

Updates tailscale/corp#33846

Signed-off-by: Alex Chan <alexc@tailscale.com>

Change-Id: I88ecd9db847e04237c1feab9dfcede5ca1050cc5
3 months ago
Joe Tsai 9ac8105fda
cmd/jsontags: add static analyzer for incompatible `json` struct tags (#17670)
This migrates an internal tool to open source
so that we can run it on the tailscale.com module as well.

This PR does not yet set up a CI to run this analyzer.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai 478342a642
wgengine/netlog: embed node information in network flow logs (#17668)
This rewrites the netlog package to support embedding node information in network flow logs.
Some bit of complexity comes in trying to pre-compute the expected size of the log message
after JSON serialization to ensure that we can respect maximum body limits in log uploading.

We also fix a bug in tstun, where we were recording the IP address after SNAT,
which was resulting in non-sensible connection flows being logged.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai fcb614a53e
cmd/jsonimports: add static analyzer for consistent "json" imports (#17669)
This migrates an internal tool to open source
so that we can run it on the tailscale.com module as well.
We add the "util/safediff" also as a dependency of the tool.

This PR does not yet set up a CI to run this analyzer.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
M. J. Fromberger 09a2a1048d
derp: fix an unchecked error in a test (#17694)
Found by staticcheck, the test was calling derphttp.NewClient but not checking
its error result before doing other things to it.

Updates #cleanup

Change-Id: I4ade35a7de7c473571f176e747866bc0ab5774db
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Brad Fitzpatrick edb11e0e60 wgengine/magicsock: fix js/wasm crash regression loading non-existent portmapper
Thanks for the report, @Need-an-AwP!

Fixes #17681
Updates #9394

Change-Id: I2e0b722ef9b460bd7e79499192d1a315504ca84c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Will Norris 0a5ba8280f CODE_OF_CONDUCT.md: update code of conduct
Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
3 months ago
M. J. Fromberger db5815fb97
Revert "logtail: avoid racing eventbus subscriptions with Shutdown (#17639)" (#17684)
This reverts commit 4346615d77.
We averted the shutdown race, but will need to service the subscriber even when
we are not waiting for a change so that we do not delay the bus as a whole.

Updates #17638

Change-Id: I5488466ed83f5ad1141c95267f5ae54878a24657
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Mario Minardi 02681732d1
.github: drop branches filter with single asterisk from workflows (#17682)
Drop usage of the branches filter with a single asterisk as this matches
against zero or more characters but not a forward slash, resulting in
PRs to branch names with forwards slashes in them not having these
workflow run against them as expected.

Updates https://github.com/tailscale/corp/issues/33523

Signed-off-by: Mario Minardi <mario@tailscale.com>
3 months ago
Gesa Stupperich d2e4a20f26 ipn/ipnlocal/serve: error when PeerCaps serialisation fails
Also consolidates variable and header naming and amends the
CLI behavior
* multiple app-caps have to be specified as comma-separated
  list
* simple regex-based validation of app capability names is
  carried out during flag parsing

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
3 months ago
Gesa Stupperich d6fa899eba ipn/ipnlocal/serve: remove grant header truncation logic
Given that we filter based on the usercaps argument now, truncation
should not be necessary anymore.

Updates tailscale/corp/#28372

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
3 months ago
Gesa Stupperich 576aacd459 ipn/ipnlocal/serve: add grant headers
Updates tailscale/corp/#28372

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
3 months ago
srwareham f4e2720821
cmd/tailscale/cli: move JetKVM scripts to /userdata/init.d for persistence (#17610)
Updates #16524
Updates jetkvm/rv1106-system#34

Signed-off-by: srwareham <ebriouscoding@gmail.com>
3 months ago
Max Coulombe 34e992f59d
feature/identityfederation: strip query params on clientID (#17666)
Updates #9192

Signed-off-by: mcoulombe <max@tailscale.com>
3 months ago
Patrick O'Doherty a760cbe33f
control/controlclient: back out HW key attestation (#17664)
Temporarily back out the TPM-based hw attestation code while we debug
Windows exceptions.

Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
3 months ago
M. J. Fromberger 4346615d77
logtail: avoid racing eventbus subscriptions with Shutdown (#17639)
When the eventbus is enabled, set up the subscription for change deltas at the
beginning when the client is created, rather than waiting for the first
awaitInternetUp check.

Otherwise, it is possible for a check to race with the client close in
Shutdown, which triggers a panic.

Updates #17638

Change-Id: I461c07939eca46699072b14b1814ecf28eec750c
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Claus Lensbøl fd0e541e5d
net/tsdial: do not panic if setting the same eventbus twice (#17640)
Updates #17638

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
3 months ago
Claus Lensbøl 7418583e47
health: compare warnable codes to avoid errors on release branch (#17637)
This compares the warnings we actually care about and skips the unstable
warnings and the changes with no warnings.

Fixes #17635

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
3 months ago
Alex Chan d47c697748 ipn/ipnlocal: skip TKA bootstrap request if Tailnet Lock is unavailable
If you run tailscaled without passing a `--statedir`, Tailnet Lock is
unavailable -- we don't have a folder to store the AUMs in.

This causes a lot of unnecessary requests to bootstrap TKA, because
every time the node receives a NetMap with some TKA state, it tries to
bootstrap, fetches the bootstrap TKA state from the control plane, then
fails with the error:

    TKA sync error: bootstrap: network-lock is not supported in this
    configuration, try setting --statedir

We can't prevent the error, but we can skip the control plane request
that immediately gets dropped on the floor.

In local testing, a new node joining a tailnet caused *three* control
plane requests which were unused.

Updates tailscale/corp#19441

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Brad Fitzpatrick 8576a802ca util/linuxfw: fix 32-bit arm regression with iptables
This fixes a regression from dd615c8fdd that moved the
newIPTablesRunner constructor from a any-Linux-GOARCH file to one that
was only amd64 and arm64, thus breaking iptables on other platforms
(notably 32-bit "arm", as seen on older Pis running Buster with
iptables)

Tested by hand on a Raspberry Pi 2 w/ Buster + iptables for now, for
lack of automated 32-bit arm tests at the moment. But filed #17629.

Fixes #17623
Updates #17629

Change-Id: Iac1a3d78f35d8428821b46f0fed3f3717891c1bd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
Patrick O'Doherty 672b1f0e76
feature/tpm: use withSRK to probe TPM availability (#17627)
On some platforms e.g. ChromeOS the owner hierarchy might not always be
available to us. To avoid stale sealing exceptions later we probe to
confirm it's working rather than rely solely on family indicator status.

Updates #17622

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
3 months ago
Patrick O'Doherty 36ad24b20f
feature/tpm: check TPM family data for compatibility (#17624)
Check that the TPM we have opened is advertised as a 2.0 family device
before using it for state sealing / hardware attestation.

Updates #17622

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
3 months ago
Will Norris afaa23c3b4 CODE_OF_CONDUCT: update document title
Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
3 months ago
Will Norris c2d62d25c6 CODE_OF_CONDUCT: convert to semantic line breaks
This reformats the existing text to have line breaks at sentences. This
commit contains no textual changes to the code of conduct, but is done
to make any subsequent changes easier to review. (sembr.org)

Also apply prettier formatting for consistency.

Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
3 months ago
Alex Chan c59c859f7d tsconsensus: mark several of these tests as known flaky
Updates https://github.com/tailscale/tailscale/issues/15627

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan 23359dc727 tka: don't try to read AUMs which are partway through being written
Fixes https://github.com/tailscale/tailscale/issues/17600

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan 2b448f0696 ipn, tka: improve the logging around TKA sync and AUM errors
*   When we do the TKA sync, log whether TKA is enabled and whether
    we want it to be enabled. This would help us see if a node is
    making bootstrap errors.

*   When we fail to look up an AUM locally, log the ID of the AUM
    rather than a generic "file does not exist" error.

    These AUM IDs are cryptographic hashes of the TKA state, which
    itself just contains public keys and signatures. These IDs aren't
    sensitive and logging them is safe.

Signed-off-by: Alex Chan <alexc@tailscale.com>

Updates https://github.com/tailscale/corp/issues/33594
3 months ago
Alex Chan 3944809a11 .github/workflows: pin the google/oss-fuzz GitHub Actions
Updates https://github.com/tailscale/corp/issues/31017

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Harry Harpham 675b1c6d54
cmd/tailscale/cli: error when advertising a Service from an untagged node (#17577)
Service hosts must be tagged nodes, meaning it is only valid to
advertise a Service from a machine which has at least one ACL tag.

Fixes tailscale/corp#33197

Signed-off-by: Harry Harpham <harry@tailscale.com>
3 months ago
Claus Lensbøl ab435ce3a6
client/systray: warn users launching the application with sudo (#17595)
If users start the application with sudo, DBUS is likely not available
or will not have the correct endpoints. We want to warn users when doing
this.

Closes #17593

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
3 months ago
M. J. Fromberger 3dde233cd3
ipn/ipnlocal: use eventbus.SubscribeFunc in LocalBackend (#17524)
This does not change which subscriptions are made, it only swaps them to use
the SubscribeFunc API instead of Subscribe.

Updates #15160
Updates #17487

Change-Id: Id56027836c96942206200567a118f8bcf9c07f64
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
3 months ago
Nick Khyl bf47d8e72b VERSION.txt: this is v1.91.0
Signed-off-by: Nick Khyl <nickk@tailscale.com>
3 months ago
License Updater 4e1c270f90 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
3 months ago
Alex Chan 4673992b96 tka: created a shared testing library for Chonk
This patch creates a set of tests that should be true for all implementations of Chonk and CompactableChonk, which we can share with the SQLite implementation in corp.

It includes all the existing tests, plus a test for LastActiveAncestor which was in corp but not in oss.

Updates https://github.com/tailscale/corp/issues/33465

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan c961d58091 cmd/tailscale: improve the error message for `lock log` with no lock
Previously, running `tailscale lock log` in a tailnet without Tailnet
Lock enabled would return a potentially confusing error:

    $ tailscale lock log
    2025/10/20 11:07:09 failed to connect to local Tailscale service; is Tailscale running?

It would return this error even if Tailscale was running.

This patch fixes the error to be:

    $ tailscale lock log
    Tailnet Lock is not enabled

Fixes #17586

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Max Coulombe 6a73c0bdf5
cmd/tailscale/cli,feature: add support for identity federation (#17529)
Add new arguments to `tailscale up` so authkeys can be generated dynamically via identity federation.

Updates #9192

Signed-off-by: mcoulombe <max@tailscale.com>
3 months ago
Brad Fitzpatrick 54cee33bae go.toolchain.rev: update to Go 1.25.3
Updates tailscale/go#140
Updates tailscale/go#142
Updates tailscale/go#138

Change-Id: Id25b6fa4e31eee243fec17667f14cdc48243c59e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
3 months ago
David Bond 9083ef1ac4
cmd/k8s-operator: allow pod tolerations on nameservers (#17260)
This commit modifies the `DNSConfig` custom resource to allow specifying
[tolerations](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/)
on the nameserver pods.

This will allow users to dictate where their nameserver pods are located
within their clusters.

Fixes: https://github.com/tailscale/tailscale/issues/17092

Signed-off-by: David Bond <davidsbond93@gmail.com>
3 months ago
Andrew Lytvynov 6493206ac7
.github/workflows: pin nix-related github actions (#17574)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
3 months ago
Alex Chan 8d119f62ee wgengine/magicsock: minor tidies in Test_endpoint_maybeProbeUDPLifetimeLocked
* Remove a couple of single-letter `l` variables
* Use named struct parameters in the test cases for readability
* Delete `wantAfterInactivityForFn` parameter when it returns the
  default zero

Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan 55a43c3736 tka: don't look up parent/child information from purged AUMs
We soft-delete AUMs when they're purged, but when we call `ChildAUMs()`,
we look up soft-deleted AUMs to find the `Children` field.

This patch changes the behaviour of `ChildAUMs()` so it only looks at
not-deleted AUMs. This means we don't need to record child information
on AUMs any more, which is a minor space saving for any newly-recorded
AUMs.

Updates https://github.com/tailscale/tailscale/issues/17566
Updates https://github.com/tailscale/corp/issues/27166

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan c3acf25d62 tka: remove an unused Mem.Orphans() method
This method was added in cca25f6 in the initial in-memory implementation
of Chonk, but it's not part of the Chonk interface and isn't implemented
or used anywhere else. Let's get rid of it.

Updates https://github.com/tailscale/corp/issues/33465

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
Alex Chan 0ce88aa343 all: use a consistent capitalisation for "Tailnet Lock"
Updates https://github.com/tailscale/corp/issues/13108

Signed-off-by: Alex Chan <alexc@tailscale.com>
3 months ago
David Bond 419fba40e0
k8s-operator/api-proxy: put kube api server events behind environment variable (#17550)
This commit modifies the k8s-operator's api proxy implementation to only
enable forwarding of api requests to tsrecorder when an environment
variable is set.

This new environment variable is named `TS_EXPERIMENTAL_KUBE_API_EVENTS`.

Updates https://github.com/tailscale/corp/issues/32448

Signed-off-by: David Bond <davidsbond93@gmail.com>
3 months ago
Joe Tsai e804b64358
wgengine/netlog: merge connstats into package (#17557)
Merge the connstats package into the netlog package
and unexport all of its declarations.

Remove the buildfeatures.HasConnStats and use HasNetLog instead.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai e75f13bd93
net/connstats: prepare to remove package (#17554)
The connstats package was an unnecessary layer of indirection.
It was seperated out of wgengine/netlog so that net/tstun and
wgengine/magicsock wouldn't need a depenedency on the concrete
implementation of network flow logging.

Instead, we simply register a callback for counting connections.
This PR does the bare minimum work to prepare tstun and magicsock
to only care about that callback.

A future PR will delete connstats and merge it into netlog.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
3 months ago
Joe Tsai 6d897c4ab4
types/netlogtype: remove CBOR representation (#17545)
Remove CBOR representation since it was never used.
We should support CBOR in the future, but for remove it
for now so that it is less work to add more fields.

Also, rely on just omitzero for JSON now that it is supported in Go 1.24.

Updates tailscale/corp#33352

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
4 months ago
Jordan Whited 743e5ac696
cmd/tailscale: surface relay-server-port set flag (#17528)
Fixes tailscale/corp#31186

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 1a93a8a704 feature/tpm: quiet log output a bit
I was debugging a customer issue and saw in their 1.88.3 logs:

    TPM: error opening: stat /dev/tpm0: no such file or directory

That's unnecessary output. The lack of TPM will be reported by
them having a nil Hostinfo.TPM, which is plenty elsewhere in logs.

Let's only write out an "error opening" line if it's an interesting
error. (perhaps permissions, or EIO, etc)

Updates #cleanup

Change-Id: I3f987f6bf1d3ada03473ca3eef555e9cfafc7677
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Claus Lensbøl 005e264b54
util/eventbus/eventbustest: add support for synctest instead of timers (#17522)
Before synctest, timers was needed to allow the events to flow into the
test bus. There is still a timer, but this one is not derived from the
test deadline and it is mostly arbitrary as synctest will render it
practically non-existent.

With this approach, tests that do not need to test for the absence of
events do not rely on synctest.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
Patrick O'Doherty d8a6d0183c
ipn/ipnlocal: strip AttestationKey in redacted prefs view (#17527)
Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
4 months ago
Aaron Klotz 7c49cab1a6 clientupdate, util/osshare, util/winutil, version: improve Windows GUI filename resolution and WinUI build awareness
On Windows arm64 we are going to need to ship two different GUI builds;
one for Win10 (GOARCH=386) and one for Win11 (GOARCH=amd64, tags +=
winui). Due to quirks in MSI packaging, they cannot both share the
same filename. This requires some fixes in places where we have
hardcoded "tailscale-ipn" as the GUI filename.

We also do some cleanup in clientupdate to ensure that autoupdates
will continue to work correctly with the temporary "-winui" package
variant.

Fixes #17480
Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
4 months ago
Patrick O'Doherty e45557afc0
types/persist: add AttestationKey (#17281)
Extend Persist with AttestationKey to record a hardware-backed
attestation key for the node's identity.

Add a flag to tailscaled to allow users to control the use of
hardware-backed keys to bind node identity to individual machines.

Updates tailscale/corp#31269


Change-Id: Idcf40d730a448d85f07f1bebf387f086d4c58be3

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
4 months ago
Joe Tsai a2dc517d7d
all: specify explicit JSON format for time.Duration (#17307)
The default representation of time.Duration has different
JSON representation between v1 and v2.

Apply an explicit format flag that uses the v1 representation
so that this behavior does not change if serialized with v2.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
4 months ago
Jonathan Nobels 8e98ecb5f7
net/netmon: handle net.IPAddr types during interface address parsing (#17523)
updates tailscale/tailscale#16836

Android's altNetInterfaces implementation now returns net.IPAddr
types which netmon wasn't handling.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
4 months ago
Jordan Whited af15ee9c5f
wgengine/magicsock: add clientmetrics for TX bytes/packets by af & conn type (#17515)
Updates tailscale/corp#33206

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
M. J. Fromberger 0a33aae823
util/eventbus: run subscriber functions in a goroutine (#17510)
With a channel subscriber, the subscription processing always occurs on another
goroutine. The SubscriberFunc (prior to this commit) runs its callbacks on the
client's own goroutine. This changes the semantics, though: In addition to more
directly pushing back on the publisher, a publisher and subscriber can deadlock
in a SubscriberFunc but succeed on a Subscriber. They should behave
equivalently regardless which interface they use.

Arguably the caller should deal with this by creating its own goroutine if it
needs to. However, that loses much of the benefit of the SubscriberFunc API, as
it will need to manage the lifecycle of that goroutine. So, for practical
ergonomics, let's make the SubscriberFunc do this management on the user's
behalf. (We discussed doing this in #17432, but decided not to do it yet).  We
can optimize this approach further, if we need to, without changing the API.

Updates #17487

Change-Id: I19ea9e8f246f7b406711f5a16518ef7ff21a1ac9
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Naman Sood f157f3288d
cmd/tailscale/cli,ipn/conffile: add declarative config mode for Services (#17435)
This commit adds the subcommands `get-config` and `set-config` to Serve,
which can be used to read the current Tailscale Services configuration
in a standard syntax and provide a configuration to declaratively apply
with that same syntax.

Both commands must be provided with either `--service=svc:service` for
one service, or `--all` for all services. When writing a config,
`--set-config --all` will overwrite all existing Services configuration,
and `--set-config --service=svc:service` will overwrite all
configuration for that particular Service. Incremental changes are not
supported.

Fixes tailscale/corp#30983.

cmd/tailscale/cli: hide serve "get-config"/"set-config" commands for now

tailscale/corp#33152 tracks unhiding them when docs exist.

Signed-off-by: Naman Sood <mail@nsood.in>
4 months ago
Tom Meadows 08eae9affd
sessionrecording: add destination to struct for tsrecorder (#17520)
when tsrecorder receives events, it populates this field with
information about the node the request was sent to.

Updates #17141

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
4 months ago
Anton Tolchanov 072e6a39f4 tsweb/varz: add support for ShardedInt metrics
Fixes tailscale/corp#33236

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
4 months ago
M. J. Fromberger 154d36f73d
wgengine/magicsock: do not apply node view updates to a closed Conn (#17517)
Fixes #17516

Change-Id: Iae2dab42d6f7bc618478d360a1005537c1fa1bbd
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Jordan Whited 16a05c7680
wgengine/magicsock: fix docs for send clientmetrics (#17514)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Jordan Whited adf308a064
wgengine/magicsock: add clientmetrics for RX bytes by af & conn type (#17512)
Updates tailscale/corp#33206

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Jordan Whited d72370a6eb
wgengine/magicsock: remove unused arg in deregisterMetrics (#17513)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Jordan Whited e2233b7942
feature/relayserver: init server at config time instead of request time (#17484)
The lazy init led to confusion and a belief that was something was
wrong. It's reasonable to expect the daemon to listen on the port at the
time it's configured.

Updates tailscale/corp#33094

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 0f4dec928e feature/featuretags: make bird depend on advertiseroutes
Updates #cleanup

Change-Id: I87082919064a5652c0d976cadd6d159787bb224a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 9123932710 net/dns, wgengine: use viewer/cloner for Config
Per earlier TODO.

Updates #17506

Change-Id: I21fe851c4bcced98fcee844cb428ca9c2f6b0588
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick f270c3158a net/dns, ipn/ipnlocal: fix regressions from change moving away from deephash
I got sidetracked apparently and never finished writing this Clone
code in 316afe7d02 (#17448). (It really should use views instead.)

And then I missed one of the users of "routerChanged" that was broken up
into "routerChanged" vs "dnsChanged".

This broke integration tests elsewhere.

Fixes #17506

Change-Id: I533bf0fcf3da9ac6eb4a6cdef03b8df2c1fb4c8e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Mike O'Driscoll 7edb5b7d43
flake.nix: update Nix to use tailscale/go 1.25.2 (#17500)
Update Nix flake to use go 1.25.2
Create the hash from the toolchain rev file automatically from
update-flake.sh

Updates tailscale/go#135

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
4 months ago
Alex Chan b7fe1cea9f cmd/tailscale/cli: only print authURLs and device approval URLs once
This patch fixes several issues related to printing login and device
approval URLs, especially when `tailscale up` is interrupted:

1.  Only print a login URL that will cause `tailscale up` to complete.
    Don't print expired URLs or URLs from previous login attempts.

2.  Print the device approval URL if you run `tailscale up` after
    previously completing a login, but before approving the device.

3.  Use the correct control URL for device approval if you run a bare
    `tailscale up` after previously completing a login, but before
    approving the device.

4.  Don't print the device approval URL more than once (or at least,
    not consecutively).

Updates tailscale/corp#31476
Updates #17361

## How these fixes work

This patch went through a lot of trial and error, and there may still
be bugs! These notes capture the different scenarios and considerations
as we wrote it, which are also captured by integration tests.

1.  We were getting stale login URLs from the initial IPN state
    notification.

    When the IPN watcher was moved to before Start() in c011369, we
    mistakenly continued to request the initial state. This is only
    necessary if you start watching after you call Start(), because
    you may have missed some notifications.

    By getting the initial state before calling Start(), we'd get
    a stale login URL. If you clicked that URL, you could complete
    the login in the control server (if it wasn't expired), but your
    instance of `tailscale up` would hang, because it's listening for
    login updates from a different login URL.

    In this patch, we no longer request the initial state, and so we
    don't print a stale URL.

2.  Once you skip the initial state from IPN, the following sequence:

    *   Run `tailscale up`
    *   Log into a tailnet with device approval
    *   ^C after the device approval URL is printed, but without approving
    *   Run `tailscale up` again

    means that nothing would ever be printed.

    `tailscale up` would send tailscaled the pref `WantRunning: true`,
    but that was already the case so nothing changes. You never get any
    IPN notifications, and in particular you never get a state change to
    `NeedsMachineAuth`. This means we'd never print the device approval URL.

    In this patch, we add a hard-coded rule that if you're doing a simple up
    (which won't trigger any other IPN notifications) and you start in the
    `NeedsMachineAuth` state, we print the device approval message without
    waiting for an IPN notification.

3.  Consider the following sequence:

    *   Run `tailscale up --login-server=<custom server>`
    *   Log into a tailnet with device approval
    *   ^C after the device approval URL is printed, but without approving
    *   Run `tailscale up` again

    We'd print the device approval URL for the default control server,
    rather than the real control server, because we were using the `prefs`
    from the CLI arguments (which are all the defaults) rather than the
    `curPrefs` (which contain the custom login server).

    In this patch, we use the `prefs` if the user has specified any settings
    (and other code will ensure this is a complete set of settings) or
    `curPrefs` if it's a simple `tailscale up`.

4.  Consider the following sequence: you've logged in, but not completed
    device approval, and you run `down` and `up` in quick succession.

    *   `up`: sees state=NeedsMachineAuth
    *   `up`: sends `{wantRunning: true}`, prints out the device approval URL
    *   `down`: changes state to Stopped
    *   `up`: changes state to Starting
    *   tailscaled: changes state to NeedsMachineAuth
    *   `up`: gets an IPN notification with the state change, and prints
        a second device approval URL

    Either URL works, but this is annoying for the user.

    In this patch, we track whether the last printed URL was the device
    approval URL, and if so, we skip printing it a second time.

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Alex Chan bb6bd46570 tstest/integration: log all the output printed by `tailscale up`
Updates tailscale/corp#31476
Updates #17361

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Alex Chan 06f12186d9 tstest/integration: test `tailscale up` when device approval is required
This patch extends the integration tests for `tailscale up` to include tailnets
where new devices need to be approved. It doesn't change the CLI, because it's
mostly working correctly already -- these tests are just to prevent future
regressions.

I've added support for `MachineAuthorized` to mock control, and I've refactored
`TestOneNodeUpAuth` to be more flexible. It now takes a sequence of steps to
run and asserts whether we got a login URL and/or machine approval URL after
each step.

Updates tailscale/corp#31476
Updates #17361

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Jordan Whited 4543ea5c8a
wgengine/magicsock: start peer relay path discovery sooner (#17485)
This commit also shuffles the hasPeerRelayServers atomic load
to happen sooner, reducing the cost for clients with no peer relay
servers.

Updates tailscale/corp#33099

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 9a72513fa4 go.toolchain.rev: bump Go to 1.25.2
Updates tailscale/go#135

Change-Id: I89cfb49b998b2fd0264f8d5f4a61af839cd06626
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Claus Lensbøl 57bd875856
control/controlclient: add missing comment (#17498)
Updates #cleanup

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
Brad Fitzpatrick 9556a0c6da control/ts2021: fix data race during concurrent Close and conn ending
Fixes tailscale/corp#33125

Change-Id: I9911f5059d5ebe42ecf7db9becb2326cca240765
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 109cb50d5f ipn/ipnlocal: use eventbus.SubscribeFunc in expiryManager
Updates #15160
Updates #17487

Change-Id: I8721e3ac1af505630edca7c5cb50695b0aad832a
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger 241ea1c98b wgengine/magicsock: use eventbus.SubscribeFunc in Conn
Updates #15160
Updates #17487

Change-Id: Ic9eb8d82b21d9dc38cb3c681b87101dfbc95af16
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger 5833730577 wgengine/router: use eventbus.SubscribeFunc in linuxRouter
Updates #15160
Updates #17487

Change-Id: Ib798e2321e55a078c8bd37f366fe4e73054e4520
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger 2a3d67e9b7 wgengine: use eventbus.SubscribeFunc in userspaceEngine
Updates #15160
Updates #17487

Change-Id: Id852098c4f9c2fdeab9151b0b8c14dceff73b99d
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
James 'zofrex' Sanderson 2d1014ead1
ipn/ipnlocal: fix data race on captiveCtx in enterStateLockedOnEntry (#17495)
Updates #17491

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
4 months ago
Tom Meadows 0586d5d40d
k8s-operator/sessionrecording: gives the connection to the recorder from the hijacker a dedicated context (#17403)
The hijacker on k8s-proxy's reverse proxy is used to stream recordings
to tsrecorder as they pass through the proxy to the kubernetes api
server. The connection to the recorder was using the client's
(e.g., kubectl) context, rather than a dedicated one. This was causing
the recording stream to get cut off in scenarios where the client
cancelled the context before streaming could be completed.

By using a dedicated context, we can continue streaming even if the
client cancels the context (for example if the client request
completes).

Fixes #17404

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
4 months ago
Tom Meadows cd2a3425cb
cmd/tsrecorder: adds sending api level logging to tsrecorder (#16960)
Updates #17141

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
4 months ago
Mike O'Driscoll f25e47cdeb
flake.nix: use tailscale go fork (#17486)
Move our nix flake to use Tailscale's go toolchain instead
of upstream go.

Fixes #17494

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
4 months ago
M. J. Fromberger ad6cf2f8f3
util/eventbus: add a function-based subscriber type (#17432)
Originally proposed by @bradfitz in #17413.

In practice, a lot of subscribers have only one event type of interest, or a
small number of mostly independent ones. In that case, the overhead of running
and maintaining a goroutine to select on multiple channels winds up being more
noisy than we'd like for the user of the API.

For this common case, add a new SubscriberFunc[T] type that delivers events to
a callback owned by the subscriber, directly on the goroutine belonging to the
client itself. This frees the consumer from the need to maintain their own
goroutine to pull events from the channel, and to watch for closure of the
subscriber.

Before:

     s := eventbus.Subscribe[T](eventClient)
     go func() {
       for {
          select {
          case <-s.Done():
            return
          case e := <-s.Events():
            doSomethingWith(e)
          }
       }
     }()
     // ...
     s.Close()

After:

     func doSomethingWithT(e T) { ... }
     s := eventbus.SubscribeFunc(eventClient, doSomethingWithT)
     // ...
     s.Close()

Moreover, unless the caller wants to explicitly stop the subscriber separately
from its governing client, it need not capture the SubscriberFunc value at all.

One downside of this approach is that a slow or deadlocked callback could block
client's service routine and thus stall all other subscriptions on that client,
However, this can already happen more broadly if a subscriber fails to service
its delivery channel in a timely manner, it just feeds back more immediately.

Updates #17487

Change-Id: I64592d786005177aa9fd445c263178ed415784d5
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Tom Proctor 98a0ccc18a
cmd/tailscaled: default state encryption off for incompatible args (#17480)
Since #17376, containerboot crashes on startup in k8s because state
encryption is enabled by default without first checking that it's
compatible with the selected state store. Make sure we only default
state encryption to enabled if it's not going to immediately clash with
other bits of tailscaled config.

Updates tailscale/corp#32909

Change-Id: I76c586772750d6da188cc97b647c6e0c1a8734f0

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
4 months ago
Brad Fitzpatrick 5c1e26b42f ipn/localapi: dead code eliminate unreachable/useless LocalAPI handlers when disabled
Saves ~94 KB from the min build.

Updates #12614

Change-Id: I3b0b8a47f80b9fd3b1038c2834b60afa55bf02c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan a9334576ea ipn/ipnlocal: use named arguments for `mockControl.send()`
Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick 232b928974 feature/linkspeed: move cosmetic tstun netlink code out to modular feature
Part of making all netlink monitoring code optional.

Updates #17311 (how I got started down this path)
Updates #12614

Change-Id: Ic80d8a7a44dc261c4b8678b3c2241c3b3778370d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Claus Lensbøl 63f7a400a8
wgengine/{magicsock,userspace,router}: move portupdates to the eventbus (#17423)
Also pull out interface method only needed in Linux.

Instead of having userspace do the call into the router, just let the
router pick up the change itself.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
James 'zofrex' Sanderson eabc62a9dd
ipn/ipnlocal: don't send LoginFinished unless auth was in progress (#17266)
Before we introduced seamless, the "blocked" state was used to track:

* Whether a login was required for connectivity, and therefore we should
  keep the engine deconfigured until that happened
* Whether authentication was in progress

"blocked" would stop authReconfig from running. We want this when a login is
required: if your key has expired we want to deconfigure the engine and keep
it down, so that you don't keep using exit nodes (which won't work because
your key has expired).

Taking the engine down while auth was in progress was undesirable, so we
don't do that with seamless renewal. However, not entering the "blocked"
state meant that we needed to change the logic for when to send
LoginFinished on the IPN bus after seeing StateAuthenticated from the
controlclient. Initially we changed the "if blocked" check to "if blocked or
seamless is enabled" which was correct in other places.

In this place however, it introduced a bug: we are sending LoginFinished
every time we see StateAuthenticated, which happens even on a down & up, or
a profile switch. This in turn made it harder for UI clients to track when
authentication is complete.

Instead we should only send it out if we were blocked (i.e. seamless is
disabled, or our key expired) or an auth was in progress.

Updates tailscale/corp#31476

Updates tailscale/corp#32645

Fixes #17363

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
4 months ago
Brad Fitzpatrick 316afe7d02 util/checkchange: stop using deephash everywhere
Saves 45 KB from the min build, no longer pulling in deephash or
util/hashx, both with unsafe code.

It can actually be more efficient to not use deephash, as you don't
have to walk all bytes of all fields recursively to answer that two
things are not equal. Instead, you can just return false at the first
difference you see. And then with views (as we use ~everywhere
nowadays), the cloning the old value isn't expensive, as it's just a
pointer under the hood.

Updates #12614

Change-Id: I7b08616b8a09b3ade454bb5e0ac5672086fe8aec
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 28b1b4c3c1 cmd/tailscaled: guard some flag work with buildfeatures checks
Updates #12614

Change-Id: Iec6f15d33a6500e7b0b7e8f5c098f7c00334460f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 10cb59fa87 build_dist.sh: keep --extra-small making a usable build, add --min
Historically, and until recently, --extra-small produced a usable build.

When I recently made osrouter be modular in 39e35379d4 (which is
useful in, say, tsnet builds) after also making netstack modular, that
meant --min now lacked both netstack support for routing and system
support for routing, making no way to get packets into
wireguard. That's not a nice default to users.  (we've documented
build_dist.sh in our KB)

Restore --extra-small to making a usable build, and add --min for
benchmarking purposes.

Updates #12614

Change-Id: I649e41e324a36a0ca94953229c9914046b5dc497
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 0415a56b6c
ipn/ipnlocal: fix another racy test (#17472)
Some of the test cases access fields of the backend that are supposed to be
locked while the test is running, which can trigger the race detector.  I fixed
a few of these in #17411, but I missed these two cases.

Updates #15160
Updates #17192

Change-Id: I45664d5e34320ecdccd2844e0f8b228145aaf603
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Brad Fitzpatrick 059f53e67a feature/condlite/expvar: add expvar stub package when metrics not needed
Saves ~53 KB from the min build.

Updates #12614

Change-Id: I73f9544a9feea06027c6ebdd222d712ada851299
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Jordan Whited 192f8d2804
wgengine/magicsock: add more handleNewServerEndpointRunLoop tests (#17469)
Updates tailscale/corp#32978

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
M. J. Fromberger e0f222b686
appc,ipn/ipnlocal: receive AppConnector updates via the event bus (#17411)
Add subscribers for AppConnector events

Make the RouteAdvertiser interface optional We cannot yet remove it because
the tests still depend on it to verify correctness. We will need to separately
update the test fixtures to remove that dependency.

Publish RouteInfo via the event bus, so we do not need a callback to do that. 
Replace it with a flag that indicates whether to treat the route info the connector 
has as "definitive" for filtering purposes.

Update the tests to simplify the construction of AppConnector values now that a
store callback is no longer required. Also fix a couple of pre-existing racy tests that 
were hidden by not being concurrent in the same way production is.

Updates #15160
Updates #17192

Change-Id: Id39525c0f02184e88feaf0d8a3c05504850e47ee
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
James 'zofrex' Sanderson 7407f404d9
ipn/ipnlocal: fix setAuthURL / setWgengineStatus race condition (#17408)
If we received a wg engine status while processing an auth URL, there was a
race condition where the authURL could be reset to "" immediately after we
set it.

To fix this we need to check that we are moving from a non-Running state to
a Running state rather than always resetting the URL when we "move" into a
Running state even if that is the current state.

We also need to make sure that we do not return from stopEngineAndWait until
the engine is stopped: before, we would return as soon as we received any
engine status update, but that might have been an update already in-flight
before we asked the engine to stop. Now we wait until we see an update that
is indicative of a stopped engine, or we see that the engine is unblocked
again, which indicates that the engine stopped and then started again while
we were waiting before we checked the state.

Updates #17388

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
Co-authored-by: Nick Khyl <nickk@tailscale.com>
4 months ago
Brad Fitzpatrick d816454a88 feature/featuretags: make usermetrics modular
Saves ~102 KB from the min build.

Updates #12614

Change-Id: Ie1d4f439321267b9f98046593cb289ee3c4d6249
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
License Updater ea8e991d69 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
4 months ago
Brad Fitzpatrick 525f9921fe cmd/testwrapper/flakytest: use t.Attr annotation on flaky tests
Updates #17460

Change-Id: I7381e9a6dd73514c73deb6b863749eef1a87efdc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 541a4ed5b4 all: use buildfeatures consts in a few more places
Saves ~25 KB.

Updates #12614

Change-Id: I7b976e57819a0d2692824d779c8cc98033df0d30
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Jordan Whited 44e1d735c3
tailcfg: bump CapVer for magicsock deadlock fix (#17450)
The fix that was applied in e44e28efcd.

Updates tailscale/corp#32978

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Alex Chan 6db8957744 tstest/integration: mark TestPeerRelayPing as flaky
Updates #17251

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick f208bf8cb1 types/lazy: document difference from sync.OnceValue
Updates #8419
Updates github.com/golang#62202

Change-Id: I0c082c4258fb7a95a17054f270dc32019bcc7581
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick cf520a3371 feature/featuretags: add LazyWG modular feature
Due to iOS memory limitations in 2020 (see
https://tailscale.com/blog/go-linker, etc) and wireguard-go using
multiple goroutines per peer, commit 16a9cfe2f4 introduced some
convoluted pathsways through Tailscale to look at packets before
they're delivered to wireguard-go and lazily reconfigure wireguard on
the fly before delivering a packet, only telling wireguard about peers
that are active.

We eventually want to remove that code and integrate wireguard-go's
configuration with Tailscale's existing netmap tracking.

To make it easier to find that code later, this makes it modular. It
saves 12 KB (of disk) to turn it off (at the expense of lots of RAM),
but that's not really the point. The point is rather making it obvious
(via the new constants) where this code even is.

Updates #12614

Change-Id: I113b040f3e35f7d861c457eaa710d35f47cee1cb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
kscooo f80c7e7c23 net/wsconn: clarify package comment
Explain that this file stays forked from coder/websocket until we can
depend on an upstream release for the helper.

Updates #cleanup

Signed-off-by: kscooo <kscowork@gmail.com>
4 months ago
Brad Fitzpatrick 6820ec5bbb wgengine: stop importing flowtrack when unused
Updates #12614

Change-Id: I42b5c4d623d356af4bee5bbdabaaf0f6822f2bf4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Jordan Whited e44e28efcd
wgengine/magicsock: fix relayManager deadlock (#17449)
Updates tailscale/corp#32978

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Jordan Whited 3aa8b6d683
wgengine/magicsock: remove misleading unexpected log message (#17445)
Switching to a Geneve-encapsulated (peer relay) path in
endpoint.handlePongConnLocked is expected around port rebinds, which end
up clearing endpoint.bestAddr.

Fixes tailscale/corp#33036

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 3c7e351671 net/connstats: make it modular (omittable)
Saves only 12 KB, but notably removes some deps on packages that future
changes can then eliminate entirely.

Updates #12614

Change-Id: Ibf830d3ee08f621d0a2011b1d4cd175427ef50df
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 2e381557b8 feature/c2n: move answerC2N code + deps out of control/controlclient
c2n was already a conditional feature, but it didn't have a
feature/c2n directory before (rather, it was using consts + DCE). This
adds it, and moves some code, which removes the httprec dependency.

Also, remove some unnecessary code from our httprec fork.

Updates #12614

Change-Id: I2fbe538e09794c517038e35a694a363312c426a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick db65f3fcf8 ipn/ipnlocal: use buildfeature consts in a few more places
Updates #12614

Change-Id: I561d434d9829172a3d7f6933399237924ff80490
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 223ced84b5 feature/ace: make ACE modular
Updates #12614

Change-Id: Iaee75d8831c4ba5c9705d7877bb78044424c6da1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 141eb64d3f wgengine/router/osrouter: fix data race in magicsock port update callback
As found by @cmol in #17423.

Updates #17423

Change-Id: I1492501f74ca7b57a8c5278ea6cb87a56a4086b9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 447cbdd1d0 health: make it omittable
Saves 86 KB.

And stop depending on expvar and usermetrics when disabled,
in prep to removing all the expvar/metrics/tsweb stuff.

Updates #12614

Change-Id: I35d2479ddd1d39b615bab32b1fa940ae8cbf9b11
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Simon Law 9c3aec58ba
ipn/ipnlocal: remove junk from suggestExitNodeUsingTrafficSteering (#17436)
This patch removes some code that didn’t get removed before merging
the changes in #16580.

Updates #cleanup
Updates #16551

Signed-off-by: Simon Law <sfllaw@tailscale.com>
4 months ago
Brad Fitzpatrick f42be719de all: use buildfeature constants in a few more places
Saves 21 KB.

Updates #12614

Change-Id: I0cd3e735937b0f5c0fcc9f09a24476b1c4ac9a15
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan 59a39841c3 tstest/integration: mark TestClientSideJailing as flaky
Updates #17419

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Tom Meadows 8d4ea55cc1
cmd/k8s-proxy: switching to using ipn/store/kubestore (#17402)
kubestore init function has now been moved to a more explicit path of
ipn/store/kubestore meaning we can now avoid the generic import of
feature/condregister.

Updates #12614

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
4 months ago
Alex Chan 304dabce17 ipn/ipnauth: fix a null pointer panic in GetConnIdentity
When running integration tests on macOS, we get a panic from a nil
pointer dereference when calling `ci.creds.PID()`.

This panic occurs because the `ci.creds != nil` check is insufficient
after a recent refactoring (c45f881) that changed `ci.creds` from a
pointer to the `PeerCreds` interface. Now `ci.creds` always compares as
non-nil, so we enter this block even when the underlying value is nil.

The integration tests fail on macOS when `peercred.Get()` returns the
error `unix.GetsockoptInt: socket is not connected`. This error isn't
new, and the previous code was ignoring it correctly.

Since we trust that `peercred` returns either a usable value or an error,
checking for a nil error is a sufficient and correct gate to prevent the
method call and avoid the panic.

Fixes #17421

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick 206d98e84b control/controlclient: restore aggressive Direct.Close teardown
In the earlier http2 package migration (1d93bdce20, #17394) I had
removed Direct.Close's tracking of the connPool, thinking it wasn't
necessary.

Some tests (in another repo) are strict and like it to tear down the
world and wait, to check for leaked goroutines. And they caught this
letting some goroutines idle past Close, even if they'd eventually
close down on their own.

This restores the connPool accounting and the aggressife close.

Updates #17305
Updates #17394

Change-Id: I5fed283a179ff7c3e2be104836bbe58b05130cc7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Simon Law cd523eae52
ipn/ipnlocal: introduce the concept of client-side-reachability (#17367)
The control plane will sometimes determine that a node is not online,
while the node is still able to connect to its peers. This patch
doesn’t solve this problem, but it does mitigate it.

This PR introduces the `client-side-reachability` node attribute that
switches the node to completely ignore the online signal from control.

In the future, the client itself should collect reachability data from
active Wireguard flows and Tailscale pings.

Updates #17366
Updates tailscale/corp#30379
Updates tailscale/corp#32686

Signed-off-by: Simon Law <sfllaw@tailscale.com>
4 months ago
Brad Fitzpatrick 24e38eb729 control/controlclient,health,ipn/ipnlocal,health: fix deadlock by deleting health reporting
A recent change (009d702adf) introduced a deadlock where the
/machine/update-health network request to report the client's health
status update to the control plane was moved to being synchronous
within the eventbus's pump machinery.

I started to instead make the health reporting be async, but then we
realized in the three years since we added that, it's barely been used
and doesn't pay for itself, for how many HTTP requests it makes.

Instead, delete it all and replace it with a c2n handler, which
provides much more helpful information.

Fixes tailscale/corp#32952

Change-Id: I9e8a5458269ebfdda1c752d7bbb8af2780d71b04
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick a208cb9fd5 feature/featuretags: add features for c2n, peerapi, advertise/use routes/exit nodes
Saves 262 KB so far. I'm sure I missed some places, but shotizam says
these were the low hanging fruit.

Updates #12614

Change-Id: Ia31c01b454f627e6d0470229aae4e19d615e45e3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 2cd518a8b6 control/controlclient: optimize zstd decode of KeepAlive messages
Maybe it matters? At least globally across all nodes?

Fixes #17343

Change-Id: I3f61758ea37de527e16602ec1a6e453d913b3195
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 3ae7a351b4 feature/featuretags: make clientmetrics optional
Saves 57 KB

Updates #12614

Change-Id: If7eebec12b3cb30ae6264171d36a258c04b05a70
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 127a967207
appc,*: publish events for route updates and storage (#17392)
Add and wire up event publishers for these two event types in the AppConnector.
Nothing currently subscribes to them, so this is harmless. Subscribers for
these events will be added in a near-future commit.

As part of this, move the appc.RouteInfo type to the types/appctype package.
It does not contain any package-specific details from appc. Beside it, add
appctype.RouteUpdate to carry route update event state, likewise not specific
to appc.  Update all usage of the appc.* types throughout to use appctype.*
instead, and update depaware files to reflect these changes.

Add a Close method to the AppConnector to make sure the client gets cleaned up
when the connector is dropped (we re-create connectors).

Update the unit tests in the appc package to also check the events published
alongside calls to the RouteAdvertiser.

For now the tests still rely on the RouteAdvertiser for correctness; this is OK
for now as the two methods are always performed together.  In the near future,
we need to rework the tests so not require that, but that will require building
some more test fixtures that we can handle separately.

Updates #15160
Updates #17192

Change-Id: I184670ba2fb920e0d2cb2be7c6816259bca77afe
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger 3c32f87624
feature/relayserver: use eventbus.Monitor to simplify lifecycle management (#17234)
Instead of using separate channels to manage the lifecycle of the eventbus
client, use the recently-added eventbus.Monitor, which handles signaling the
processing loop to stop and waiting for it to complete.  This allows us to
simplify some of the setup and cleanup code in the relay server.

Updates #15160

Change-Id: Ia1a47ce2e5a31bc8f546dca4c56c3141a40d67af
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Brad Fitzpatrick 1d93bdce20 control/controlclient: remove x/net/http2, use net/http
Saves 352 KB, removing one of our two HTTP/2 implementations linked
into the binary.

Fixes #17305
Updates #15015

Change-Id: I53a04b1f2687dca73c8541949465038b69aa6ade
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick c45f8813b4 feature/featuretags, all: add build features, use existing ones in more places
Saves 270 KB.

Updates #12614

Change-Id: I4c3fe06d32c49edb3a4bb0758a8617d83f291cf5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Tom Proctor aa5b2ce83b
cmd/k8s-operator: add .gitignore for generated chart CRDs (#17406)
Add a .gitignore for the chart version of the CRDs that we never commit,
because the static manifest CRD files are the canonical version. This
makes it easier to deploy the CRDs via the helm chart in a way that
reflects the production workflow without making the git checkout
"dirty".

Given that the chart CRDs are ignored, we can also now safely generate
them for the kube-generate-all Makefile target without being a nuisance
to the state of the git checkout. Added a slightly more robust repo root
detection to the generation logic to make sure the command works from
the context of both the Makefile and the image builder command we run
for releases in corp.

Updates tailscale/corp#32085

Change-Id: Id44a4707c183bfaf95a160911ec7a42ffb1a1287

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
4 months ago
Tom Proctor 16e0abe031
build_docker.sh: support including extra files (#17405)
mkctr already has support for including extra files in the built
container image. Wire up a new optional environment variable to thread
that through to mkctr. The operator e2e tests will use this to bake
additional trusted CAs into the test image without significantly
departing from the normal build or deployment process for our
containers.

Updates tailscale/corp#32085

Change-Id: Ica94ed270da13782c4f5524fdc949f9218f79477

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
4 months ago
Alex Chan 7dfa26778e derp/derphttp: de-flake DERP HTTP clients tests with memnet and synctest
Using memnet and synctest removes flakiness caused by real networking
and subtle timing differences.

Additionally, remove the `t.Logf` call inside the server's shutdown
goroutine that was causing a false positive data race detection.

The race detector is flagging a double write during this `t.Logf` call.
This is a common pattern, noted in golang/go#40343 and elsehwere in
this file, where using `t.Logf` after a test has finished can interact
poorly with the test runner.

This is a long-standing issue which became more common after rewriting
this test to use memnet and synctest.

Fixed #17355

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Andrew Lytvynov cca70ddbfc
cmd/tailscaled: default --encrypt-state to true if TPM is available (#17376)
Whenever running on a platform that has a TPM (and tailscaled can access
it), default to encrypting the state. The user can still explicitly set
this flag to disable encryption.

Updates https://github.com/tailscale/corp/issues/32909

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
4 months ago
Brad Fitzpatrick 78af49dd1a control/ts2021: rename from internal/noiseconn in prep for controlclient split
A following change will split out the controlclient.NoiseClient type
out, away from the rest of the controlclient package which is
relatively dependency heavy.

A question was where to move it, and whether to make a new (a fifth!)
package in the ts2021 dependency chain.

@creachadair and I brainstormed and decided to merge
internal/noiseconn and controlclient.NoiseClient into one package,
with names ts2021.Conn and ts2021.Client.

For ease of reviewing the subsequent PR, this is the first step that
just renames the internal/noiseconn package to control/ts2021.

Updates #17305

Change-Id: Ib5ea162dc1d336c1d805bdd9548d1702dd6e1468
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 801aac59db Makefile, cmd/*/depaware.txt: split out vendor packages explicitly
depaware was merging golang.org/x/foo and std's
vendor/golang.org/x/foo packages (which could both be in the binary!),
leading to confusing output, especially when I was working on
eliminating duplicate packages imported under different names.

This makes the depaware output longer and grosser, but doesn't hide
reality from us.

Updates #17305

Change-Id: I21cc3418014e127f6c1a81caf4e84213ce84ab57
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 67f1081269
appc,ipn/ipnlocal: add a required event bus to the AppConnector type (#17390)
Require the presence of the bus, but do not use it yet.  Check for required
fields and update tests and production use to plumb the necessary arguments.

Updates #15160
Updates #17192

Change-Id: I8cefd2fdb314ca9945317d3320bd5ea6a92e8dcb
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Claus Lensbøl ce752b8a88
net/netmon: remove usage of direct callbacks from netmon (#17292)
The callback itself is not removed as it is used in other repos, making
it simpler for those to slowly transition to the eventbus.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
M. J. Fromberger 6f7ce5eb5d
appc: factor app connector arguments into a Config type (#17389)
Replace the positional arguments to NewAppConnector with a Config struct.
Update the existing uses. Other than the API change, there are no functional
changes in this commit.

Updates #15160
Updates #17192

Change-Id: Ibf37f021372155a4db8aaf738f4b4f2c746bf623
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Brad Fitzpatrick 05a4c8e839 tsnet: remove AuthenticatedAPITransport (API-over-noise) support
It never launched and I've lost hope of it launching and it's in my
way now, so I guess it's time to say goodbye.

Updates tailscale/corp#4383
Updates #17305

Change-Id: I2eb551d49f2fb062979cc307f284df4b3dfa5956
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick c2f37c891c all: use Go 1.20's errors.Join instead of our multierr package
Updates #7123

Change-Id: Ie9be6814831f661ad5636afcd51d063a0d7a907d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 91fa51ca15 ipn/store, feature/condregister: permit callers to empty import optonal ipn stores
This permits other programs (in other repos) to conditionally
import ipn/store/awsstore and/or ipn/store/kubestore and have them
register themselves, rather than feature/condregister doing it.

Updates tailscale/corp#32922

Change-Id: I2936229ce37fd2acf9be5bf5254d4a262d090ec1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
James Sanderson ebc370e517 ipn/ipnlocal: fail test if more notifies are put than expected
The `put` callback runs on a different goroutine to the test, so calling
t.Fatalf in put had no effect. `drain` is always called when checking what
was put and is called from the test goroutine, so that's a good place to
fail the test if the channel was too full.

Updates #17363

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
4 months ago
Tom Meadows af1114e896
cmd/k8s-proxy: importing feature/condregister on cmd/k8s-proxy (#17383)
https://github.com/tailscale/tailscale/pull/17346 moved the kube and aws
arn store initializations to feature/condregister, under the assumption
that anything using it would use kubestore.New. Unfortunately,
cmd/k8s-proxy makes use of store.New, which compares the `<prefix>:`
supplied in the provided `path string` argument against known stores. If
it doesn't find it, it fallsback to using a FileStore.

Since cmd/k8s-proxy uses store.New to try and initialize a kube store in
some cases (without importing feature/condregister), it silently creates
a FileStore and that leads to misleading errors further along in
execution.

This fixes this issue by importing condregister, and successfully
initializes a kube store.

Updates #12614

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
4 months ago
Nick Khyl 9781b7c25c ipn/ipnlocal: plumb logf into nodeBackend
Updates #cleanup

Signed-off-by: Nick Khyl <nickk@tailscale.com>
4 months ago
Brad Fitzpatrick 5b09913d64 ipn/ipnlocal, engine: avoid runtime/pprof with two usages of ts_omit_debug
Saves 258 KB.

Updates #12614

Change-Id: I37c2f7f916480e3534883f338de4c64d08f7ef2b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick f7afb9b6ca feature/featuretags, ipn/conffile: make HuJSON support in config files optional
Saves 33 KB.

Updates #12614

Change-Id: Ie701c230e0765281f409f29ed263910b9be9cc77
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 6c6a1d8341 feature/appconnectors: start making it modular
Saves 45 KB.

Updates #12614

Change-Id: Iaeb73e69633878ce0a0f58c986024784bbe218f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 9386a101d8 cmd/tailscaled, ipn/localapi, util/eventbus: don't link in regexp when debug is omitted
Saves 442 KB. Lock it with a new min test.

Updates #12614

Change-Id: Ia7bf6f797b6cbf08ea65419ade2f359d390f8e91
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Andrew Lytvynov 840c7668e2
types/key: add IsZero method to HardwareAttestationKey (#17370)
We will need this for unmarshaling node prefs: use the zero
HardwareAttestationKey implementation when parsing and later check
`IsZero` to see if anything was loaded.

Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
4 months ago
Brad Fitzpatrick be6cfa00cb util/eventbus: when ts_omit_debugeventbus is set, don't import tsweb
I'm trying to remove the "regexp" and "regexp/syntax" packages from
our minimal builds. But tsweb pulls in regexp (via net/http/pprof etc)
and util/eventbus was importing the tsweb for no reason.

Updates #12614

Change-Id: Ifa8c371ece348f1dbf80d6b251381f3ed39d5fbd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick ee034d48fc feature/featuretags: add a catch-all "Debug" feature flag
Saves 168 KB.

Updates #12614

Change-Id: Iaab3ae3efc6ddc7da39629ef13e5ec44976952ba
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick bbb16e4e72 drive: don't use regexp package in leaf types package
Even with ts_omit_drive, the drive package is currently still imported
for some types. So it should be light. But it was depending on the
"regexp" packge, which I'd like to remove from our minimal builds.

Updates #12614

Change-Id: I5bf85d8eb15a739793723b1da11c370d3fcd2f32
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
James Tucker b9cdef18c0 util/prompt: add a default and take default in non-interactive cases
The Tailscale CLI is the primary configuration interface and as such it
is used in scripts, container setups, and many other places that do not
have a terminal available and should not be made to respond to prompts.

The default is set to false where the "risky" API is being used by the
CLI and true otherwise, this means that the `--yes` flags are only
required under interactive runs and scripts do not need to be concerned
with prompts or extra flags.

Updates #19445

Signed-off-by: James Tucker <james@tailscale.com>
4 months ago
Brad Fitzpatrick 442a3a779d feature, net/tshttpproxy: pull out support for using proxies as a feature
Saves 139 KB.

Also Synology support, which I saw had its own large-ish proxy parsing
support on Linux, but support for proxies without Synology proxy
support is reasonable, so I pulled that out as its own thing.

Updates #12614

Change-Id: I22de285a3def7be77fdcf23e2bec7c83c9655593
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 9b997c8f2f feature/tpm: don't log to stderr in tests
Fixes #17336

Change-Id: I7d2be4e8acf59116c57ce26049a6a5baa8f32436
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 1803226945 net/tstun: fix typo in doc
Updates #cleanup

Change-Id: Icaca974237cf678f3e036b1dfdd2f2e5082483db
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 2c956e30be ipn/ipnlocal: proxy h2c grpc using net/http.Transport instead of x/net/http2
(Kinda related: #17351)

Updates #17305

Change-Id: I47df2612732a5713577164e74652bc9fa3cd14b3
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 3f5c560fd4 ipn/ipnlocal: drop h2c package, use net/http's support
In Dec 2021 in d3d503d997 I had grand plans to make exit node DNS
cheaper by using HTTP/2 over PeerAPI, at least on some platforms. I
only did server-side support though and never made it to the client.

In the ~4 years since, some things have happened:

* Go 1.24 got support for http.Protocols (https://pkg.go.dev/net/http#Protocols)
  and doing UnencryptedHTTP2 ("HTTP2 with prior knowledge")
* The old h2c upgrade mechanism was deprecated; see https://github.com/golang/go/issues/63565
  and https://github.com/golang/go/issues/67816
* Go plans to deprecate x/net/http2 and move everything to the standard library.

So this drops our use of the x/net/http2/h2c package and instead
enables h2c (on all platforms now) using the standard library.

This does mean we lose the deprecated h2c Upgrade support, but that's
fine.

If/when we do the h2c client support for ExitDNS, we'll have to probe
the peer to see whether it supports it. Or have it reply with a header
saying that future requests can us h2c. (It's tempting to use capver,
but maybe people will disable that support anyway, so we should
discover it at runtime instead.)

Also do the same in the sessionrecording package.

Updates #17305

Change-Id: If323f5ef32486effb18ed836888aa05c0efb701e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick bcd79b161a feature/featuretags: add option to turn off DNS
Saves 328 KB (2.5%) off the minimal binary.

For IoT devices that don't need MagicDNS (e.g. they don't make
outbound connections), this provides a knob to disable all the DNS
functionality.

Rather than a massive refactor today, this uses constant false values
as a deadcode sledgehammer, guided by shotizam to find the largest DNS
functions which survived deadcode.

A future refactor could make it so that the net/dns/resolver and
publicdns packages don't even show up in the import graph (along with
their imports) but really it's already pretty good looking with just
these consts, so it's not at the top of my list to refactor it more
soon.

Also do the same in a few places with the ACME (cert) functionality,
as I saw those while searching for DNS stuff.

Updates #12614

Change-Id: I8e459f595c2fde68ca16503ff61c8ab339871f97
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Raj Singh a45473c4c5
cmd/k8s-operator: add DNS policy and config support to ProxyClass (#16887)
DNS configuration support to ProxyClass, allowing users to customize DNS resolution for Tailscale proxy pods.

Fixes #16886

Signed-off-by: Raj Singh <raj@tailscale.com>
4 months ago
Brad Fitzpatrick 9aa16bf97b feature/featuretags, Makefile: fix bug with CLI build tag and depaware, add variant
When I added dependency support to featuretag, I broke the handling of
the non-omit build tags (as used by the "box" support for bundling the
CLI into tailscaled). That then affected depaware. The
depaware-minbox.txt this whole time recently has not included the CLI.

So fix that, and also add a new depaware variant that's only the
daemon, without the CLI.

Updates #12614
Updates #17139

Change-Id: I4a4591942aa8c66ad8e3242052e3d9baa42902ca
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick ba76578447 ipn/ipnlocal, feature/posture: pull posture out into a modular feature
Updates #12614

Change-Id: I9d08a1330b9c55e1a23e7979a707e11d8e090d79
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 038cdb4640 feature/clientupdate: move clientupdate to a modular feature, disabled for tsnet
Updates #12614

Change-Id: I5f685dec84a5396b7c2b66f2788ae3d286e1ddc6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 69c79cb9f3 ipn/store, feature/condregister: move AWS + Kube store registration to condregister
Otherwise they're uselessly imported by tsnet applications, even
though they do nothing. tsnet applications wanting to use these
already had to explicitly import them and use kubestore.New or
awsstore.New and assign those to their tsnet.Server.Store fields.

Updates #12614

Change-Id: I358e3923686ddf43a85e6923c3828ba2198991d4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brian Palmer 54e50230a1
net/memnet: allow listener address reuse (#17342)
Listen address reuse is allowed as soon as the previous listener is
closed. There is no attempt made to emulate more complex address reuse
logic.

Updates tailscale/corp#28078

Change-Id: I56be1c4848e7b3f9fc97fd4ef13a2de9dcfab0f2

Signed-off-by: Brian Palmer <brianp@tailscale.com>
4 months ago
Brad Fitzpatrick bdb69d1b1f net/dns/resolver: fix data race in test
Fixes #17339

Change-Id: I486d2a0e0931d701923c1e0f8efbda99510ab19b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan 1aaa1648c4 README: update the version of Go in the README
Updates #17064

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick 72bc7334fb net/speedtest: mark flaky test, and skip it by default as it's slow
Updates #17338

Change-Id: I1f3dbc154ba274f615cc77d2aa76f6ff9d40137c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 39e35379d4 wgengine/router{,/osrouter}: split OS router implementations into subpackage
So wgengine/router is just the docs + entrypoint + types, and then
underscore importing wgengine/router/osrouter registers the constructors
with the wgengine/router package.

Then tsnet can not pull those in.

Updates #17313

Change-Id: If313226f6987d709ea9193c8f16a909326ceefe7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Fran Bull 65d6c80695 cmd/tailscale/cli,client,ipn: add appc-routes cli command
Allow the user to access information about routes an app connector has
learned, such as how many routes for each domain.

Fixes tailscale/corp#32624

Signed-off-by: Fran Bull <fran@tailscale.com>
4 months ago
Brad Fitzpatrick 976389c0f7 feature/sdnotify: move util/systemd to a modular feature
Updates #12614

Change-Id: I08e714c83b455df7f538cc99cafe940db936b480
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 7bcab4ab28 feature/featuretags: make CLI connection error diagnostics modular
Updates #12614

Change-Id: I09b8944166ee00910b402bcd5725cd7969e2c82c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 11b770fbc9 feature/logtail: pull logtail + netlog out to modular features
Removes 434 KB from the minimal Linux binary, or ~3%.

Primarily this comes from not linking in the zstd encoding code.

Fixes #17323

Change-Id: I0a90de307dfa1ad7422db7aa8b1b46c782bfaaf7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
David Bond e466488a2a
cmd/k8s-operator: add replica support to nameserver (#17246)
This commit modifies the `DNSConfig` custom resource to allow specifying
a replica count when deploying a nameserver. This allows deploying
nameservers in a HA configuration.

Updates https://github.com/tailscale/corp/issues/32589

Signed-off-by: David Bond <davidsbond93@gmail.com>
4 months ago
Brad Fitzpatrick a32102f741 smallzstd: delete unused package
As of the earlier 85febda86d, our new preferred zstd API of choice
is zstdframe.

Updates #cleanup
Updates tailscale/corp#18514

Change-Id: I5a6164d3162bf2513c3673b6d1e34cfae84cb104
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 01e645fae1 util/backoff: rename logtail/backoff package to util/backoff
It has nothing to do with logtail and is confusing named like that.

Updates #cleanup
Updates #17323

Change-Id: Idd34587ba186a2416725f72ffc4c5778b0b9db4a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 475b520aa2 tsconst, util/linuxfw, wgengine/router: move Linux fw consts to tsconst
Now cmd/derper doesn't depend on iptables, nftables, and netlink code :)

But this is really just a cleanup step I noticed on the way to making
tsnet applications able to not link all the OS router code which they
don't use.

Updates #17313

Change-Id: Ic7b4e04e3a9639fd198e9dbeb0f7bae22a4a47a9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Joe Tsai f19409482d logtail: delete AppendTextOrJSONLocked
This was accidentally added in #11671 for testing.
Nothing uses it.

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
4 months ago
Brad Fitzpatrick dd615c8fdd util/linuxfw, feature/buildfeatures: add ts_omit_iptables to make IPTables optional
Updates #12614

Change-Id: Ic0eba982aa8468a55c63e1b763345f032a55b4e2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Irbe Krumina 7df7e01d0f
tstest/integration/vms,.github/workflows: bump Ubuntu and NixOS for VM tests + cleanup (#16098)
This PR cleans up a bunch of things in ./tstest/integration/vms:

- Bumps version of Ubuntu that's actually run from CI 20.04 -> 24.04
- Removes Ubuntu 18.04 test
- Bumps NixOS 21.05 -> 25.05

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
4 months ago
Brad Fitzpatrick d01a0adfa6 types/dnstype: delete unused func, move other one to its sole caller
The dnstype package is used by tailcfg, which tries to be light and
leafy. But it brings in dnstype. So dnstype shouldn't bring in
x/net/dns/dnsmessage.

Updates #12614

Change-Id: I043637a7ce7fed097e648001f13ca1927a781def
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick e9dae5441e tka: use ts_omit_tailnetlock in another spot, for ed25519consensus
I noticed this while modularizing clientupdate. With this in first,
moving clientupdate to be modular removes a bunch more stuff from
the minimal build + tsnet.

Updates #17115

Change-Id: I44bd055fca65808633fd3a848b0bbc09b00ad4fa
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Claus Lensbøl 9ae8155bab
cmol/pprof health (#17303)
health: ensure timers are cleaned up

Updates tailscale/corp#32696

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
Brad Fitzpatrick 832e94607e doctor: add ts_omit_doctor support
Updates #12614

Change-Id: I84c166c4b99ca75d70abe4087e5ff3f7d90d4bcc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 87ee0f4e98 ipn/ipnlocal: move last unconditional gvisor import, complete ts_omit_netstack support
Fixes #17283

Change-Id: Ia84d269683e4a68d7d10562561204934eeaf53bb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Mahyar Mirrashed eaecc0be54
cmd/tailscale/cli: use tabwriter for tailscale status (#16596)
Fixes #17238

Signed-off-by: Mahyar Mirrashed <mah.mirr@gmail.com>
4 months ago
Brad Fitzpatrick c95fdb0f8a net/packet/checksum: copy the gvisor checksum, remove the dep
As part of making Tailscale's gvisor dependency optional for small builds,
this was one of the last places left that depended on gvisor. Just copy
the couple functions were were using.

Updates #17283

Change-Id: Id2bc07ba12039afe4c8a3f0b68f4d76d1863bbfe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick afe909664b types/opt: de-weird the API a bit with new True and False consts
Updates #cleanup

Change-Id: I15d8d840877d43e2b884d42354b4eb156094df7d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick e766adf71f net/tstun: use ts_omit_gro in another place I missed earlier
I didn't notice this GRO code during b3ae1cb0cc.

Updates #17283

Change-Id: I95c06c19e489097fc8d61180dc57ae4b8a69c58c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick e7a79ef5f1 tstest/integration: deflake TestC2NDebugNetmap, disable service collection
Fixes #17298

Change-Id: I83459fa1dad583c32395a80548510bc7ec035c41
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 09a33b9262 net/tstun: support ts_omit_netstack
Updates #17283

Change-Id: I1134bb15b3e39a3fa26c0621512aae9181de2210
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Nick Khyl 0b994ef2fe docs/windows/policy: add ADMX policy definition for AllowTailscaledRestart
Updates tailscale/corp#32675

Signed-off-by: Nick Khyl <nickk@tailscale.com>
4 months ago
Brad Fitzpatrick f715ee2be9 cmd/tailscaled: start implementing ts_omit_netstack
Baby steps. This permits building without much of gvisor, but not all of it.

Updates #17283

Change-Id: I8433146e259918cc901fe86b4ea29be22075b32c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick b3ae1cb0cc wgengine/netstack/gro: permit building without GRO
This only saves ~32KB in the minimal linux/amd64 binary, but it's a
step towards permitting not depending on gvisor for small builds.

Updates #17283

Change-Id: Iae8da5e9465127de354dbcaf25e794a6832d891b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Andrew Lytvynov f2b8d37436
feature/tpm: only register HardwareAttestationKey on linux/windows (#17293)
We can only register one key implementation per process. When running on
macOS or Android, trying to register a separate key implementation from
feature/tpm causes a panic.

Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
4 months ago
Alex Chan 002ecb78d0 all: don't rebind variables in for loops
See https://tip.golang.org/wiki/LoopvarExperiment#does-this-mean-i-dont-have-to-write-x--x-in-my-loops-anymore

Updates https://github.com/tailscale/tailscale/issues/11058

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
James Tucker 8b3e88cd09
wgengine/magicsock: fix rebind debouncing (#17282)
On platforms that are causing EPIPE at a high frequency this is
resulting in non-working connections, for example when Apple decides to
forcefully close UDP sockets due to an unsoliced packet rejection in the
firewall.

Too frequent rebinds cause a failure to solicit the endpoints triggering
the rebinds, that would normally happen via CallMeMaybe.

Updates #14551
Updates tailscale/corp#25648

Signed-off-by: James Tucker <james@tailscale.com>
4 months ago
Alex Chan 41a2aaf1da cmd/tailscale/cli: fix race condition in `up --force-reauth`
This commit fixes a race condition where `tailscale up --force-reauth` would
exit prematurely on an already-logged in device.

Previously, the CLI would wait for IPN to report the "Running" state and then
exit. However, this could happen before the new auth URL was printed, leading
to two distinct issues:

*   **Without seamless key renewal:** The CLI could exit immediately after
    the `StartLoginInteractive` call, before IPN has time to switch into
    the "Starting" state or send a new auth URL back to the CLI.
*   **With seamless key renewal:** IPN stays in the "Running" state
    throughout the process, so the CLI exits immediately without performing
    any reauthentication.

The fix is to change the CLI's exit condition.

Instead of waiting for the "Running" state, if we're doing a `--force-reauth`
we now wait to see the node key change, which is a more reliable indicator
that a successful authentication has occurred.

Updates tailscale/corp#31476
Updates tailscale/tailscale#17108

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Alex Chan c011369de2 cmd/tailscale/cli: start WatchIPNBus before initial Start
This partially reverts f3d2fd2.

When that patch was written, the goroutine that responds to IPN notifications
could call `StartLoginInteractive`, creating a race condition that led to
flaky integration tests. We no longer call `StartLoginInteractive` in that
goroutine, so the race is now impossible.

Moving the `WatchIPNBus` call earlier ensures the CLI gets all necessary
IPN notifications, preventing a reauth from hanging.

Updates tailscale/corp#31476

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick 260fe38ad8 Makefile, cmd/tailscaled: add minimal tailscale+cli binary depaware
Updates #12614

Change-Id: I593ed30f620556c6503d80c0ccbbe242567fd5cf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan 9154bc10f0 tstest/integration: skip this test rather than commenting it out
Updates #17108

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Nick Khyl 892f8a9582 various: allow tailscaled shutdown via LocalAPI
A customer wants to allow their employees to restart tailscaled at will, when access rights and MDM policy allow it,
as a way to fully reset client state and re-create the tunnel in case of connectivity issues.

On Windows, the main tailscaled process runs as a child of a service process. The service restarts the child
when it exits (or crashes) until the service itself is stopped. Regular (non-admin) users can't stop the service,
and allowing them to do so isn't ideal, especially in managed or multi-user environments.

In this PR, we add a LocalAPI endpoint that instructs ipnserver.Server, and by extension the tailscaled process,
to shut down. The service then restarts the child tailscaled. Shutting down tailscaled requires LocalAPI write access
and an enabled policy setting.

Updates tailscale/corp#32674
Updates tailscale/corp#32675

Signed-off-by: Nick Khyl <nickk@tailscale.com>
4 months ago
Brad Fitzpatrick 45d635cc98 feature/portlist: pull portlist service porting into extension, use eventbus
And yay: tsnet (and thus k8s-operator etc) no longer depends on
portlist! And LocalBackend is smaller.

Removes 50 KB from the minimal binary.

Updates #12614

Change-Id: Iee04057053dc39305303e8bd1d9599db8368d926
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Nick Khyl bbc5107d7d ipn/ipnlocal: do not reset extHost on (*LocalBackend).Shutdown
We made changes to ipnext callback registration/unregistration/invocation in #15780
that made resetting b.exthost to a nil, no-op host in (*LocalBackend).Shutdown() unnecessary.

But resetting it is also racy: b.exthost must be safe for concurrent use with or without b.mu held,
so it shouldn't be written after NewLocalBackend returns. This PR removes it.

Fixes #17279

Signed-off-by: Nick Khyl <nickk@tailscale.com>
4 months ago
Andrew Lytvynov c49ed5dd5a
feature/tpm: implement key.HardwareAttestationKey (#17256)
Updates #15830

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
4 months ago
Brad Fitzpatrick a40f23ad4a util/eventbus: flesh out docs a bit
Updates #cleanup

Change-Id: Ia6b0e4b0426be1dd10a777aff0a81d4dd6b69b01
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Raj Singh 0b27871860
k8s-operator: add IPv6 support for DNS records (#16691)
This change adds full IPv6 support to the Kubernetes operator's DNS functionality,
enabling dual-stack and IPv6-only cluster support.

Fixes #16633

Signed-off-by: Raj Singh <raj@tailscale.com>
4 months ago
Alex Chan e0a77cf41a tstest/integration: expand the tests for `tailscale up`
Expand the integration tests to cover a wider range of scenarios, including:

*   Before and after a successful initial login
*   Auth URLs and auth keys
*   With and without the `--force-reauth` flag
*   With and without seamless key renewal

These tests expose a race condition when using `--force-reauth` on an
already-logged in device. The command completes too quickly, preventing
the auth URL from being displayed. This issue is identified and will be
fixed in a separate commit.

Updates #17108

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
James Sanderson 70400cb75f cmd/tailscale/cli: reduce strength of lose-ssh risk warning
Ideally we would remove this warning entirely, as it is now possible to
reauthenticate without losing connectivty. However, it is still possible to
lose SSH connectivity if the user changes the ownership of the machine when
they do a force-reauth, and we have no way of knowing if they are going to
do that before they do it.

For now, let's just reduce the strength of the warning to warn them that
they "may" lose their connection, rather than they "will".

Updates tailscale/corp#32429

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
4 months ago
Brad Fitzpatrick 0bd4f4729b ipn/ipnlocal: rename misnamed DisablePortMapperForTest to DisablePortPollerForTest
I think this was originally a brain-o in 9380e2dfc6. It's
disabling the port _poller_, listing what open ports (i.e. services)
are open, not PMP/PCP/UPnP port mapping.

While there, drop in some more testenv.AssertInTest() in a few places.

Updates #cleanup

Change-Id: Ia6f755ad3544f855883b8a7bdcfc066e8649547b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Simon Law 34242df51b
derp/derpserver: clean up extraction of derp.Server (#17264)
PR #17258 extracted `derp.Server` into `derp/derpserver.Server`.

This followup patch adds the following cleanups:
1. Rename `derp_server*.go` files to `derpserver*.go` to match
   the package name.
2. Rename the `derpserver.NewServer` constructor to `derpserver.New`
   to reduce stuttering.
3. Remove the unnecessary `derpserver.Conn` type alias.

Updates #17257
Updates #cleanup

Signed-off-by: Simon Law <sfllaw@tailscale.com>
4 months ago
Patrick O'Doherty db02a46645
types/key: Update HardwareAttestationPublic representation (#17233)
Sidestep cmd/viewer incompatibility hiccups with
HardwareAttestationPublic type due to its *ecdsa.PublicKey inner member
by serializing the key to a byte slice instead.

Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
4 months ago
Brad Fitzpatrick 21dc5f4e21 derp/derpserver: split off derp.Server out of derp into its own package
This exports a number of things from the derp (generic + client) package
to be used by the new derpserver package, as now used by cmd/derper.

And then enough other misc changes to lock in that cmd/tailscaled can
be configured to not bring in tailscale.com/client/local. (The webclient
in particular, even when disabled, was bringing it in, so that's now fixed)

Fixes #17257

Change-Id: I88b6c7958643fb54f386dd900bddf73d2d4d96d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger df747f1c1b
util/eventbus: add a Done method to the Monitor type (#17263)
Some systems need to tell whether the monitored goroutine has finished
alongside other channel operations (notably in this case the relay server, but
there seem likely to be others similarly situated).

Updates #15160

Change-Id: I5f0f3fae827b07f9b7102a3b08f60cda9737fe28
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Brad Fitzpatrick b3e9a128af net/dns, feature/featuretags: make NetworkManager, systemd-resolved, and DBus modular
Saves 360 KB (19951800 => 19591352 on linux/amd64 --extra-small --box binary)

Updates #12614
Updates #17206

Change-Id: Iafd5b2536dd735111b447546cba335a7a64379ed
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick b54cdf9f38 all: use buildfeatures.HasCapture const in a handful of places
Help out the linker's dead code elimination.

Updates #12614

Change-Id: I6c13cb44d3250bf1e3a01ad393c637da4613affb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 8fe575409f feature/featuretags: add build tag to remove captive portal detection
This doesn't yet fully pull it out into a feature/captiveportal package.
This is the usual first step, moving the code to its own files within
the same packages.

Updates #17254

Change-Id: Idfaec839debf7c96f51ca6520ce36ccf2f8eec92
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Jordan Whited 4657cbdb11
client, cmd/tailscale/cli, feature/relayserver, net/udprelay: implement tailscale debug peer-relay-sessions (#17239)
Fixes tailscale/corp#30035

Signed-off-by: Dylan Bargatze <dylan@tailscale.com>
Signed-off-by: Jordan Whited <jordan@tailscale.com>
Co-authored-by: Dylan Bargatze <dylan@tailscale.com>
4 months ago
Brad Fitzpatrick 87ccfbd250 ipn/ipnlocal: fix eventbus data race
Fixes #17252

Change-Id: Id969fca750a48fb43431c53f3e0631bd9bd496d1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Jonathan Nobels 4af15a1148
magicsock: fix deadlock in SetStaticEndpoints (#17247)
updates tailscale/corp#32600

A localAPI/cli call to reload-config can end up leaving magicsock's mutex
locked.   We were missing an unlock for the early exit where there's no change in
the static endpoints when the disk-based config is loaded.  This is not likely
the root cause of the linked issue - just noted during investigation.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
4 months ago
License Updater 1791f87870 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
4 months ago
Percy Wegmann e3307fbce1 cmd/tailscale: omit the `drive` subcommand in MacOS GUI apps
In MacOS GUI apps, users have to select folders to share via the GUI. This is both because
the GUI app keeps its own record of shares, and because the sandboxed version of the GUI
app needs to gain access to the shared folders by having the user pick them in a file
selector.

The new build tag `ts_mac_gui` allows the MacOS GUI app build to signal that this
is a MacOS GUI app, which causes the `drive` subcommand to be omitted so that people
do not mistakenly attempt to use it.

Updates tailscale/tailscale#17210

Signed-off-by: Percy Wegmann <percy@tailscale.com>
4 months ago
Will Norris e582fb9b53 client/web: use network profile for displaying tailnet info
Also update to use the new DisplayNameOrDefault.

Updates tailscale/corp#30456

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
4 months ago
Will Norris 15b3876c2c client/systray: use new tailnet display name is profile title
Updates tailscale/corp#30456

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
4 months ago
M. J. Fromberger daad5c2b5c
wgengine/router: use eventbus.Monitor in linuxRouter (#17232)
This commit does not change the order or meaning of any eventbus activity, it
only updates the way the plumbing is set up.

Updates #15160

Change-Id: I61b863f9c05459d530a4c34063a8bad9046c0e27
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Mahyar Mirrashed 5e79e497d3
cmd/tailscale/cli: show last seen time on status command (#16588)
Add a last seen time on the cli's status command, similar to the web
portal.

Before:
```
100.xxx.xxx.xxx    tailscale-operator   tagged-devices linux   offline
```

After:
```
100.xxx.xxx.xxx    tailscale-operator   tagged-devices linux   offline, last seen 20d ago
```

Fixes #16584

Signed-off-by: Mahyar Mirrashed <mah.mirr@gmail.com>
4 months ago
Claus Lensbøl f67ad67c6f
control/controlclient: switch ID to be incrementing instead of random (#17230)
Also cleans up a a few comments.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
M. J. Fromberger 1b5201023f
ipn/ipnlocal: use eventbus.Monitor in LocalBackend (#17225)
This commit does not change the order or meaning of any eventbus activity, it
only updates the way the plumbing is set up.

Updates #15160

Change-Id: I06860ac4e43952a9bb4d85366138c9d9a17fd9cd
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger e59fbaab64
util/eventbus: give a nicer error when attempting to use a closed client (#17208)
It is a programming error to Publish or Subscribe on a closed Client, but now
the way you discover that is by getting a panic from down in the machinery of
the bus after the client state has been cleaned up.

To provide a more helpful error, let's panic explicitly when that happens and
say what went wrong ("the client is closed"), by preventing subscriptions from
interleaving with closure of the client. With this change, either an attachment
fails outright (because the client is already closed) or completes and then
shuts down in good order in the normal course.

This does not change the semantics of the client, publishers, or subscribers,
it's just making the failure more eager so we can attach explanatory text.

Updates #15160

Change-Id: Ia492f4c1dea7535aec2cdcc2e5ea5410ed5218d2
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Claus Lensbøl 6e128498a7
controlclient/auto: switch eventbus to using a monitor (#17205)
Only changes how the go routine consuming the events starts and stops,
not what it does.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
David Bond cc1761e8d2
cmd/k8s-operator: send operator logs to tailscale (#17110)
This commit modifies the k8s operator to wrap its logger using the logtail
logger provided via the tsnet server. This causes any logs written by
the operator to make their way to Tailscale in the same fashion as
wireguard logs to be used by support.

This functionality can also be opted-out of entirely using the
"TS_NO_LOGS_NO_SUPPORT" environment variable.

Updates https://github.com/tailscale/corp/issues/32037

Signed-off-by: David Bond <davidsbond93@gmail.com>
4 months ago
Kristoffer Dalby 986b4d1b0b control/controlclient: fix tka godoc
Updates #cleanup

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
4 months ago
Brad Fitzpatrick 8ec07b5f7f ipn/ipnauth: don't crash on OpenBSD trying to log username of unknown peer
We never implemented the peercred package on OpenBSD (and I just tried
again and failed), but we've always documented that the creds pointer
can be nil for operating systems where we can't map the unix socket
back to its UID. On those platforms, we set the default unix socket
permissions such that only the admin can open it anyway and we don't
have a read-only vs read-write distinction. OpenBSD was always in that
camp, where any access to Tailscale's unix socket meant full access.

But during some refactoring, we broke OpenBSD in that we started
assuming during one logging path (during login) that Creds was non-nil
when looking up an ipnauth.Actor's username, which wasn't relevant (it
was called from a function "maybeUsernameOf" anyway, which threw away
errors).

Verified on an OpenBSD VM. We don't have any OpenBSD integration tests yet.

Fixes #17209
Updates #17221

Change-Id: I473c5903dfaa645694bcc75e7f5d484f3dd6044d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick db048e905d control/controlhttp: simplify, fix race dialing, remove priority concept
controlhttp has the responsibility of dialing a set of candidate control
endpoints in a way that minimizes user facing latency. If one control
endpoint is unavailable we promptly dial another, racing across the
dimensions of: IPv6, IPv4, port 80, and port 443, over multiple server
endpoints.

In the case that the top priority endpoint was not available, the prior
implementation would hang waiting for other results, so as to try to
return the highest priority successful connection to the rest of the
client code. This hang would take too long with a large dialplan and
sufficient client to endpoint latency as to cause the server to timeout
the connection due to inactivity in the intermediate state.

Instead of trying to prioritize non-ideal candidate connections, the
first successful connection is now used unconditionally, improving user
facing latency and avoiding any delays that would encroach on the
server-side timeout.

The tests are converted to memnet and synctest, running on all
platforms.

Fixes #8442
Fixes tailscale/corp#32534

Co-authored-by: James Tucker <james@tailscale.com>
Change-Id: I4eb57f046d8b40403220e40eb67a31c41adb3a38
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
4 months ago
Brad Fitzpatrick 1b6bc37f28 net/dnscache: fix case where Resolver could return zero IP with single IPv6 address
The controlhttp dialer with a ControlDialPlan IPv6 entry was hitting a
case where the dnscache Resolver was returning an netip.Addr zero
value, where it should've been returning the IPv6 address.

We then tried to dial "invalid IP:80", which would immediately fail,
at least locally.

Mostly this was causing spammy logs when debugging other stuff.

Updates tailscale/corp#32534

Change-Id: If8b9a20f10c1a6aa8a662c324151d987fe9bd2f8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick d7ec043306 cmd/tailscale/cli: add ts2021 debug flag to set a dial plan
Updates tailscale/corp#32534

Change-Id: Ief4ee0a263ea1edbf652b74d8c335c1e5ee209d7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 798fddbe5c feature/linuxdnsfight: move inotify watching of /etc/resolv.conf out to a feature
tsnet apps in particular never use the Linux DNS OSManagers, so they don't need
DBus, etc. I started to pull that all out into separate features so tsnet doesn't
need to bring in DBus, but hit this first.

Here you can see that tsnet (and the k8s-operator) no longer pulls in inotify.

Updates #17206

Change-Id: I7af0f391f60c5e7dbeed7a080346f83262346591
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger f9c699812a
ipn/ipnlocal: use eventbus.Monitor in expiryManager (#17204)
This commit does not change the order or meaning of any eventbus activity, it
only updates the way the plumbing is set up.

Updates #15160

Change-Id: I0a175e67e867459daaedba0731bf68bd331e5ebc
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger 2b6bc11586
wgengine: use eventbus.Client.Monitor to simplify subscriber maintenance (#17203)
This commit does not change the order or meaning of any eventbus activity, it
only updates the way the plumbing is set up.

Updates #15160

Change-Id: I40c23b183c2a6a6ea3feec7767c8e5417019fc07
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
M. J. Fromberger ca9d795006
util/eventbus: add a Monitor type to manage subscriber goroutines (#17127)
A common pattern in event bus usage is to run a goroutine to service a
collection of subscribers on a single bus client. To have an orderly shutdown,
however, we need a way to wait for such a goroutine to be finished.

This commit adds a Monitor type that makes this pattern easier to wire up:
rather than having to track all the subscribers and an extra channel, the
component need only track the client and the monitor.  For example:

   cli := bus.Client("example")
   m := cli.Monitor(func(c *eventbus.Client) {
     s1 := eventbus.Subscribe[T](cli)
     s2 := eventbus.Subscribe[U](cli)
     for {
       select {
       case <-c.Done():
         return
       case t := <-s1.Events():
          processT(t)
       case u := <-s2.Events():
          processU(u)
       }
     }
   })

To shut down the client and wait for the goroutine, the caller can write:

   m.Close()

which closes cli and waits for the goroutine to finish. Or, separately:

   cli.Close()
   // do other stuff
   m.Wait()

While the goroutine management is not explicitly tied to subscriptions, it is a
common enough pattern that this seems like a useful simplification in use.

Updates #15160

Change-Id: I657afda1cfaf03465a9dce1336e9fd518a968bca
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Claus Lensbøl 009d702adf
health: remove direct callback and replace with eventbus (#17199)
Pulls out the last callback logic and ensures timers are still running.

The eventbustest package is updated support the absence of events.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
Brad Fitzpatrick d559a21418 util/eventbus/eventbustest: fix typo of test name
And another case of the same typo in a comment elsewhere.

Updates #cleanup

Change-Id: Iaa9d865a1cf83318d4a30263c691451b5d708c9c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 5e698a81b6 cmd/tailscaled: make the outbound HTTP/SOCKS5 proxy modular
Updates #12614

Change-Id: Icba6f1c0838dce6ee13aa2dc662fb551813262e4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick ecfdd86fc9 net/ace, control/controlhttp: start adding ACE dialing support
Updates tailscale/corp#32227

Change-Id: I38afc668f99eb1d6f7632e82554b82922f3ebb9f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Naman Sood b9cda4bca5
tsnet,internal/client/tailscale: resolve OAuth into authkeys in tsnet (#17191)
* tsnet,internal/client/tailscale: resolve OAuth into authkeys in tsnet

Updates #8403.

* internal/client/tailscale: omit OAuth library via build tag

Updates #12614.

Signed-off-by: Naman Sood <mail@nsood.in>
4 months ago
Anton Tolchanov 2351cc0d0e ipn/ipnlocal: make the redactNetmapPrivateKeys test recursive
Expand TestRedactNetmapPrivateKeys to cover all sub-structs of
NetworkMap and confirm that a) all fields are annotated as private or
public, and b) all private fields are getting redacted.

Updates tailscale/corp#32095

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
4 months ago
Anton Tolchanov 4a04161828 ipn/ipnlocal: add a C2N endpoint for fetching a netmap
For debugging purposes, add a new C2N endpoint returning the current
netmap. Optionally, coordination server can send a new "candidate" map
response, which the client will generate a separate netmap for.
Coordination server can later compare two netmaps, detecting unexpected
changes to the client state.

Updates tailscale/corp#32095

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
4 months ago
Anton Tolchanov 394718a4ca tstest/integration: support multiple C2N handlers in testcontrol
Instead of a single hard-coded C2N handler, add support for calling
arbitrary C2N endpoints via a node roundtripper.

Updates tailscale/corp#32095

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
4 months ago
M. J. Fromberger fc9a74a405
util/eventbus: fix flakes in eventbustest tests (#17198)
When tests run in parallel, events from multiple tests on the same bus can
intercede with each other. This is working as intended, but for the test cases
we want to control exactly what goes through the bus.

To fix that, allocate a fresh bus for each subtest.

Fixes #17197

Change-Id: I53f285ebed8da82e72a2ed136a61884667ef9a5e
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Brad Fitzpatrick 78035fb9d2 feature/featuretags,cmd/omitsize: support feature dependencies
This produces the following omitsizes output:

    Starting with everything and removing a feature...

    tailscaled tailscale combined (linux/amd64)
     27005112  18153656  39727288
    - 7696384 - 7282688 -19607552 .. remove *
    -  167936 -  110592 -  245760 .. remove acme
    - 1925120 -       0 - 7340032 .. remove aws
    -    4096 -       0 -    8192 .. remove bird
    -   20480 -   12288 -   32768 .. remove capture
    -       0 -   57344 -   61440 .. remove completion
    -  249856 -  696320 -  692224 .. remove debugeventbus
    -   12288 -    4096 -   24576 .. remove debugportmapper
    -       0 -       0 -       0 .. remove desktop_sessions
    -  815104 -    8192 -  544768 .. remove drive
    -   65536 -  356352 -  425984 .. remove kube
    -  233472 -  286720 -  311296 .. remove portmapper (and debugportmapper)
    -   90112 -       0 -  110592 .. remove relayserver
    -  655360 -  712704 -  598016 .. remove serve (and webclient)
    -  937984 -       0 -  950272 .. remove ssh
    -  708608 -  401408 -  344064 .. remove syspolicy
    -       0 - 4071424 -11132928 .. remove systray
    -  159744 -   61440 -  225280 .. remove taildrop
    -  618496 -  454656 -  757760 .. remove tailnetlock
    -  122880 -       0 -  131072 .. remove tap
    -  442368 -       0 -  483328 .. remove tpm
    -   16384 -       0 -   20480 .. remove wakeonlan
    -  278528 -  368640 -  286720 .. remove webclient

    Starting at a minimal binary and adding one feature back...

    tailscaled tailscale combined (linux/amd64)
     19308728  10870968  20119736 omitting everything
    +  352256 +  454656 +  643072 .. add acme
    + 2035712 +       0 + 2035712 .. add aws
    +    8192 +       0 +    8192 .. add bird
    +   20480 +   12288 +   36864 .. add capture
    +       0 +   57344 +   61440 .. add completion
    +  262144 +  274432 +  266240 .. add debugeventbus
    +  344064 +  118784 +  360448 .. add debugportmapper (and portmapper)
    +       0 +       0 +       0 .. add desktop_sessions
    +  978944 +    8192 +  991232 .. add drive
    +   61440 +  364544 +  425984 .. add kube
    +  331776 +  110592 +  335872 .. add portmapper
    +  122880 +       0 +  102400 .. add relayserver
    +  598016 +  155648 +  737280 .. add serve
    + 1142784 +       0 + 1142784 .. add ssh
    +  708608 +  860160 +  720896 .. add syspolicy
    +       0 + 4079616 + 6221824 .. add systray
    +  180224 +   65536 +  237568 .. add taildrop
    +  647168 +  393216 +  720896 .. add tailnetlock
    +  122880 +       0 +  126976 .. add tap
    +  446464 +       0 +  454656 .. add tpm
    +   20480 +       0 +   24576 .. add wakeonlan
    + 1011712 + 1011712 + 1138688 .. add webclient (and serve)

Fixes #17139

Change-Id: Ia91be2da00de8481a893243d577d20e988a0920a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 4f211ea5c5
util/eventbus: add a LogAllEvents helper for testing (#17187)
When developing (and debugging) tests, it is useful to be able to see all the
traffic that transits the event bus during the execution of a test.

Updates #15160

Change-Id: I929aee62ccf13bdd4bd07d786924ce9a74acd17a
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Andrew Lytvynov 70dfdac609
prober: allow custom tls.Config for TLS probes (#17186)
Updates https://github.com/tailscale/corp/issues/28569

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
4 months ago
Brad Fitzpatrick 73bbd7caca build_dist.sh: add -trimpath
Saves 81KB (20320440 to 20238520 bytes for linux/amd64)

Updates #1278

Change-Id: Id607480c76220c74c8854ef1a2459aee650ad7b6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Erisa A 61751a0c9a
scripts/installer.sh: add Siemens Industrial OS (#17185)
Fixes #17179

Signed-off-by: Erisa A <erisa@tailscale.com>
4 months ago
Alex Chan cd153aa644 control, ipn, tailcfg: enable seamless key renewal by default
Previously, seamless key renewal was an opt-in feature.  Customers had
to set a `seamless-key-renewal` node attribute in their policy file.

This patch enables seamless key renewal by default for all clients.

It includes a `disable-seamless-key-renewal` node attribute we can set
in Control, so we can manage the rollout and disable the feature for
clients with known bugs.  This new attribute makes the feature opt-out.

Updates tailscale/corp#31479

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Esteban-Bermudez 1c9aaa444d cmd/tailscale/cli: use helper function for matching profiles
This makes the `switch` command use the helper `matchProfile` function
that was introduced in the `remove` sub command.

Signed-off-by: Esteban-Bermudez <esteban@bermudezaguirre.com>
4 months ago
Esteban-Bermudez 5e3e536c2d cmd/tailscale/cli: add `remove` subcommand
Fixes #12255

Add a new subcommand to `switch` for removing a profile from the local
client. This does not delete the profile from the Tailscale account, but
removes it from the local machine. This functionality is available on
the GUI's, but not yet on the CLI.

Signed-off-by: Esteban-Bermudez <esteban@bermudezaguirre.com>
4 months ago
Brad Fitzpatrick 55d0e6d3a8 net/dns/recursive: remove recursive DNS resolver
It doesn't really pull its weight: it adds 577 KB to the binary and
is rarely useful.

Also, we now have static IPs and other connectivity paths coming
soon enough.

Updates #5853
Updates #1278
Updates tailscale/corp#32168

Change-Id: If336fed00a9c9ae9745419e6d81f7de6da6f7275
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 73c371f784 cmd/derper: permit port 80 in ACE targets
Updates tailscale/corp#32168
Updates tailscale/corp#32226

Change-Id: Iddc017b060c76e6eab8f6d0c989a775bcaae3518
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Tom Proctor bb38bf7414
docker: bump alpine v3.19 -> 3.22 (#17155)
Updates #15328

Change-Id: Ib33baf8756b648176dce461b25169e079cbd5533

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
4 months ago
Remy Guercio 9d661663f3
cmd/tsidp: update README with new repo location warning
Fixes: #17170

Signed-off-by: Remy Guercio <remy@tailscale.com>
4 months ago
M. J. Fromberger 6992f958fc
util/eventbus: add an EqualTo helper for testing (#17178)
For a common case of events being simple struct types with some exported
fields, add a helper to check (reflectively) for equal values using cmp.Diff so
that a failed comparison gives a useful diff in the test output.

More complex uses will still want to provide their own comparisons; this
(intentionally) does not export diff options or other hooks from the cmp
package.

Updates #15160

Change-Id: I86bee1771cad7debd9e3491aa6713afe6fd577a6
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Elliot Blackburn 8a4b1eb6a3
words: add some more (#17177)
Updates #words

Signed-off-by: Elliot Blackburn <elliot@sunbeam.cx>
4 months ago
Claus Lensbøl df362d0a08
net/netmon: make ChangeDelta event not a pointer (#17112)
This makes things work slightly better over the eventbus.

Also switches ipnlocal to use the event over the eventbus instead of the
direct callback.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
M. J. Fromberger 48029a897d
util/eventbus: allow test expectations reporting only an error (#17146)
Extend the Expect method of a Watcher to allow filter functions that report
only an error value, and which "pass" when the reported error is nil.

Updates #15160

Change-Id: I582d804554bd1066a9e499c1f3992d068c9e8148
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Alex Chan db0b9a361c net/dns: don't timeout if inotify sends multiple events
This fixes a flaky test which has been occasionally timing out in CI.

In particular, this test times out if `watchFile` receives multiple
notifications from inotify before we cancel the test context. We block
processing the second notification, because we've stopped listening to
the `callbackDone` channel.

This patch changes the test so we only send on the first notification.

Testing this locally with `stress` confirms that the test is no longer
flaky.

Fixes #17172
Updates #14699

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
James Sanderson ddc0cd7e1e ipn/ipnlocal: disconnect and block when key expires even when using seamless
Updates tailscale/corp#31478

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
4 months ago
Alex Chan 312582bdbf ssh/tailssh: mark TestSSHRecordingCancelsSessionsOnUploadFailure as flaky
Updates https://github.com/tailscale/tailscale/issues/7707

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Brad Fitzpatrick 697098ed6c ipn/ipnlocal: fix a case where ts_omit_ssh was still linking in x/crypto/ssh
And add a test.

Updates #12614

Change-Id: Icb1c77f5890def794a4938583725c1a0886b197d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Simon Law 6db30a10f7
cmd/tailscale: shrink QR codes using half blocks (#17084)
When running `tailscale up --qr`, the QR code is rendered using
two full blocks ██ to form a square pixel. This is a problem for
people with smaller terminals, because the output is 37 lines high.
All modern terminals support half block characters, like ▀ and ▄,
which only takes 19 lines and can easily fit in a regular terminal
window.

For example, https://login.tailscale.com/a/0123456789 is now rendered:

```
user@host:~$ tailscale up --qr
█████████████████████████████████████
█████████████████████████████████████
████ ▄▄▄▄▄ █ ▀▀   █▄▀▀ ▄ █ ▄▄▄▄▄ ████
████ █   █ █▀ ▄▄▄█▀█▄▀  ▄█ █   █ ████
████ █▄▄▄█ ██▄ ▄▀▀▄▄ ▀▀ ▀█ █▄▄▄█ ████
████▄▄▄▄▄▄▄█ ▀▄▀ █▄▀▄▀▄█ █▄▄▄▄▄▄▄████
████▄█▄ ▀▄▄▄█▀▄█▀ ▀▄ ▄  ▀▀ ▀▀▄█▄ ████
████▄▀▄▀▄█▄ █ ▄▄▄▄█▀██▀██▄▄█▀█▄▄▀████
████▄█▀ ▀ ▄█▄▄▀▄▀█ ▄ ▄█▀█▄▀██▄ ▀▀████
█████▀ ▀  ▄▀▀▀▀▄▀▄▀▀ ▄▄ ▄ ▀  █▄ ▄████
██████ ▄▄█▄▄▄▄▄▀ █ ▄▀▀▄█▀ █ ▄ ▀ █████
████▄█▄▄  ▄▀ ▀██▀  ▄█▀▀████▄▀█ ██████
█████▄▄▄█▄▄▄▀▀ █▄▄▄▄▄ ▀█ ▄▄▄   ▀▀████
████ ▄▄▄▄▄ █ ██▄ ▀ █▀█ ▄ █▄█  █▄█████
████ █   █ █▀  █ ▀█▄▄ █▀  ▄  ▀▄▀▄████
████ █▄▄▄█ █▄█▀█▄▀██▀██▄ ▀█▄▀▀▄▀▄████
████▄▄▄▄▄▄▄█▄▄███▄▄▄███▄▄▄██▄██▄█████
█████████████████████████████████████
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
```

To render a QR code with full blocks, like we did in the past, use the
new `--qr-format` flag:

```
user@host:~$ tailscale up --qr --qr-format=large
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
████████              ██  ████      ██  ████      ██              ████████
████████  ██████████  ██            ████      ██  ██  ██████████  ████████
████████  ██      ██  ████        ██████  ██      ██  ██      ██  ████████
████████  ██      ██  ██    ████████  ████      ████  ██      ██  ████████
████████  ██      ██  ████      ████      ████  ████  ██      ██  ████████
████████  ██████████  ██████  ██    ████          ██  ██████████  ████████
████████              ██  ██  ██  ██  ██  ██  ██  ██              ████████
████████████████████████    ██    ████  ██  ████  ████████████████████████
████████  ██    ██      ████  ████  ██          ████  ████  ██    ████████
██████████████    ████████  ████      ██  ██              ██████  ████████
████████  ██  ██  ██    ██          ██████████████    ██████    ██████████
██████████  ██  ██████  ██  ██████████  ████  ██████████  ██████  ████████
████████  ████  ██    ██    ██  ████        ██████  ██████    ████████████
████████████        ████████  ██  ██  ██  ████  ████  ██████      ████████
████████████  ██      ████████  ██  ████            ██    ██      ████████
██████████          ██        ██  ██      ████  ██        ████  ██████████
████████████      ██          ██  ██    ████  ████  ██      ██  ██████████
████████████  ████████████████    ██  ██    ████    ██  ██      ██████████
████████  ██          ██  ████████      ██████████████  ████  ████████████
████████████████    ██      ████      ████    ██████████  ██  ████████████
██████████      ██      ████  ██            ████              ████████████
████████████████████████      ████████████    ██  ██████          ████████
████████              ██  ████    ██  ██████      ██  ██    ██  ██████████
████████  ██████████  ██  ██████      ██  ██  ██  ██████    ██████████████
████████  ██      ██  ████    ██  ████      ████          ██  ██  ████████
████████  ██      ██  ██      ██    ██████  ██      ██      ██  ██████████
████████  ██      ██  ██  ██████  ████████████    ████  ████  ██  ████████
████████  ██████████  ██████  ████  ████  ██████    ████    ██  ██████████
████████              ██    ██████      ██████      ████  ████  ██████████
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
██████████████████████████████████████████████████████████████████████████
```

Fixes #17083

Signed-off-by: Simon Law <sfllaw@tailscale.com>
4 months ago
Brad Fitzpatrick e180fc267b feature/featuretags, all: add ts_omit_acme to disable TLS cert support
I'd started to do this in the earlier ts_omit_server PR but
decided to split it into this separate PR.

Updates #17128

Change-Id: Ief8823a78d1f7bbb79e64a5cab30a7d0a5d6ff4b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Brad Fitzpatrick 99b3f69126 feature/portmapper: make the portmapper & its debugging tools modular
Starting at a minimal binary and adding one feature back...
    tailscaled tailscale combined (linux/amd64)
     30073135  17451704  31543692 omitting everything
    +  480302 +   10258 +  493896 .. add debugportmapper
    +  475317 +  151943 +  467660 .. add portmapper
    +  500086 +  162873 +  510511 .. add portmapper+debugportmapper

Fixes #17148

Change-Id: I90bd0e9d1bd8cbe64fa2e885e9afef8fb5ee74b1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan 2b0f59cd38 logpolicy: remove the deprecated and now-unused `NewWithConfigPath`
Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
M. J. Fromberger 8608e42103
feature,ipn/ipnlocal,wgengine: improve how eventbus shutdown is handled (#17156)
Instead of waiting for a designated subscription to close as a canary for the
bus being stopped, use the bus Client's own signal for closure added in #17118.

Updates #cleanup

Change-Id: I384ea39f3f1f6a030a6282356f7b5bdcdf8d7102
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Paweł Bojanowski b63f5d7e7d logpolicy/logpolicy: use noopPretendSuccessTransport if NoLogsNoSupport envknob is set
Signed-off-by: Paweł Bojanowski <pawel.bojanowski@loft.sh>
4 months ago
Alex Chan 84659b1dc6 ipn: fix the string representation of an empty ipn.Notify
Before: `ipn.Notify}`
After:  `ipn.Notify{}`

Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Claus Lensbøl 2015ce4081
health,ipn/ipnlocal: introduce eventbus in heath.Tracker (#17085)
The Tracker was using direct callbacks to ipnlocal. This PR moves those
to be triggered via the eventbus.

Additionally, the eventbus is now closed on exit from tailscaled
explicitly, and health is now a SubSystem in tsd.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
4 months ago
Brad Fitzpatrick 4cca9f7c67 all: add ts_omit_serve, start making tailscale serve/funnel be modular
tailscaled tailscale combined (linux/amd64)
     29853147  17384418  31412596 omitting everything
    +  621570 +  219277 +  554256 .. add serve

Updates #17128

Change-Id: I87c2c6c3d3fc2dc026c3de8ef7000a813b41d31c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
M. J. Fromberger 5b5ae2b2ee
util/eventbus: add a Done channel to the Client (#17118)
Subscribers already have a Done channel that the caller can use to detect when
the subscriber has been closed. Typically this happens when the governing
Client closes, which in turn is typically because the Bus closed.

But clients and subscribers can stop at other times too, and a caller has no
good way to tell the difference between "this subscriber closed but the rest
are OK" and "the client closed and all these subscribers are finished".

We've worked around this in practice by knowing the closure of one subscriber
implies the fate of the rest, but we can do better: Add a Done method to the
Client that allows us to tell when that has been closed explicitly, after all
the publishers and subscribers associated with that client have been closed.
This allows the caller to be sure that, by the time that occurs, no further
pending events are forthcoming on that client.

Updates #15160

Change-Id: Id601a79ba043365ecdb47dd035f1fdadd984f303
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
4 months ago
Mike O'Driscoll 5ad3bd9f47
flake.nix: fix go version (#17152)
Bump to 1.25.1 to match go.mod

Fixes #17150

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
4 months ago
Jordan Whited 24dd19c9a0
tstest/integration{/testcontrol}: add peer relay integration test (#17103)
Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 8b48f3847d net/netmon, wgengine/magicsock: simplify LinkChangeLogLimiter signature
Remove the need for the caller to hold on to and call an unregister
function. Both two callers (one real, one test) already have a context
they can use. Use context.AfterFunc instead. There are no observable
side effects from scheduling too late if the goroutine doesn't run sync.

Updates #17148

Change-Id: Ie697dae0e797494fa8ef27fbafa193bfe5ceb307
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
4 months ago
Alex Chan 5c24f0ed80 wgengine/magicsock: send a valid payload in TestNetworkDownSendErrors
This test ostensibly checks whether we record an error metric if a packet
is dropped because the network is down, but the network connectivity is
irrelevant -- the send error is actually because the arguments to Send()
are invalid:

    RebindingUDPConn.WriteWireGuardBatchTo:
    [unexpected] offset (0) != Geneve header length (8)

This patch changes the test so we try to send a valid packet, and we
verify this by sending it once before taking the network down.  The new
error is:

    magicsock: network down

which is what we're trying to test.

We then test sending an invalid payload as a separate test case.

Updates tailscale/corp#22075

Signed-off-by: Alex Chan <alexc@tailscale.com>
4 months ago
Jordan Whited 998a667cd5
wgengine/magicsock: don't add DERP addrs to endpointState (#17147)
endpointState is used for tracking UDP direct connection candidate
addresses. If it contains a DERP addr, then direct connection path
discovery will always send a wasteful disco ping over it. Additionally,
CLI "tailscale ping" via peer relay will race over DERP, leading to a
misleading result if pong arrives via DERP first.

Disco pongs arriving via DERP never influence path selection. Disco
ping/pong via DERP only serves "tailscale ping" reporting.

Updates #17121

Signed-off-by: Jordan Whited <jordan@tailscale.com>
4 months ago
Brad Fitzpatrick 09dfd94613 cmd/omitsize: fix the --features flag
When you say --features=foo,bar, that was supposed to mean
to only show features "foo" and "bar" in the table.

But it was also being used as the set of all features that are
omittable, which was wrong, leading to misleading numbers
when --features was non-empty.

Updates #12614

Change-Id: Idad2fa67fb49c39454032e84a3dede967890fdf5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Aaron Klotz 4bb03609bc tool/gocross: ensure child process error codes are propagated on non-Unix
The Unix implementation of doExec propagates error codes by virtue of
the fact that it does an execve; the replacement binary will return the
exit code.

On non-Unix, we need to simulate these semantics by checking for an
ExitError and, when present, passing that value on to os.Exit.

We also add error handling to the doExec call for the benefit of
handling any errors where doExec fails before being able to execute
the desired binary.

Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
5 months ago
Brad Fitzpatrick 6fb316f5ed feature/buildfeatures: split const bools out of the featuretags package, add Has prefix
This renames the package+symbols in the earlier 17ffa80138 to be
in their own package ("buildfeatures") and start with the word "Has"
like "if buildfeatures.HasFoo {".

Updates #12614

Change-Id: I510e5f65993e5b76a0e163e3aa4543755213cbf6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Patrick O'Doherty 510830ca7a
tailcfg: add HardwareAttestationKey to MapRequest (#17102)
Extend the client state management to generate a hardware attestation
key if none exists.

Extend MapRequest with HardwareAttestationKey{,Signature} fields that
optionally contain the public component of the hardware attestation key
and a signature of the node's node key using it. This will be used by
control to associate hardware attesation keys with node identities on a
TOFU basis.

Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
5 months ago
Brad Fitzpatrick 17ffa80138 feature/featuretags: add auto-generated constants for all modular features
So code (in upcoming PRs) can test for the build tags with consts and
get dead code elimination from the compiler+linker.

Updates #12614

Change-Id: If6160453ffd01b798f09894141e7631a93385941
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Will Norris 082c6a25b0 client/systray: only send clipboard notification on success
Fixes #14430

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
5 months ago
Claus Lensbøl b816fd7117
control/controlclient: introduce eventbus messages instead of callbacks (#16956)
This is a small introduction of the eventbus into controlclient that
communicates with mainly ipnlocal. While ipnlocal is a complicated part
of the codebase, the subscribers here are from the perspective of
ipnlocal already called async.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
5 months ago
David Bond 782c16c513
k8s-operator: reset service status before append (#17120)
This commit fixes an issue within the service reconciler where we end
up in a constant reconciliation loop. When reconciling, the loadbalancer
status is appended to but not reset between each reconciliation, leading
to an ever growing slice of duplicate statuses.

Fixes https://github.com/tailscale/tailscale/issues/17105
Fixes https://github.com/tailscale/tailscale/issues/17107

Signed-off-by: David Bond <davidsbond93@gmail.com>
5 months ago
Brad Fitzpatrick 7d2101f352 cmd/omitsize: add flag to disable the removal table
And remove a bogus omit feature from feature/featuretags.

Updates #12614

Change-Id: I0a08183fb75c73ae75b6fd4216d134e352dcf5a0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 0cc1b2ff76 cmd/derper: add start of ACE support
Updates tailscale/corp#32168
Updates tailscale/corp#32226

Change-Id: Ia46abcaa09dcfd53bf8d4699909537bacf84d57a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 3a49b7464c all: add ts_omit_tailnetlock as a start of making it build-time modular
Updates #17115

Change-Id: I6b083c0db4c4d359e49eb129d626b7f128f0a9d2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 0e3d942e39 feature/featuretags: move list of omit-able features to a Go package
Updates #12614

Change-Id: I4012c33095c6a7ccf80ad36dbab5cedbae5b3d47
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
David Bond cfb2ca724b
tsnet: expose logtail's Logf method (#17057)
This commit adds a new method to the tsnet.Server type named `Logger`
that returns the underlying logtail instance's Logf method.

This is intended to be used within the Kubernetes operator to wrap its
existing logger in a way such that operator specific logs can also be
sent to control for support & debugging purposes.

Updates https://github.com/tailscale/corp/issues/32037

Signed-off-by: David Bond <davidsbond93@gmail.com>
5 months ago
Brad Fitzpatrick a1dcf12b67 feature/drive: start factoring out Taildrive, add ts_omit_drive build tag
As of this commit (per the issue), the Taildrive code remains where it
was, but in new files that are protected by the new ts_omit_drive
build tag. Future commits will move it.

Updates #17058

Change-Id: Idf0a51db59e41ae8da6ea2b11d238aefc48b219e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 82c5024f03 net/netns: fix controlLogf doc
Its doc said its signature matched a std signature, but it used
Tailscale-specific types.

Nowadays it's the caller (func control) that curries the logf/netmon
and returns the std-matching signature.

Updates #cleanup (while answering a question on Slack)

Change-Id: Ic99de41fc6a1c720575a7f33c564d0bcfd9a2c30
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 921d77062e cmd/omitsize: add tool to dump build sizes
Updates #12614

Change-Id: I8f85d7275bc8eecedbabe6631b50e1cf70791d2d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Will Hannah 1be9c6b23e
VERSION.txt: this is v1.89.0 (#17099)
Signed-off-by: Will Hannah <willh@tailscale.com>
5 months ago
Will Hannah 49aa798d18
VERSION.txt: this is v1.88.0 (#17098)
Signed-off-by: Will Hannah <willh@tailscale.com>
5 months ago
Jordan Whited fb9d9ba86e
wgengine/magicsock: add TS_DEBUG_NEVER_DIRECT_UDP debug knob (#17094)
Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Jordan Whited 32bfd72752
tstest/integration/testcontrol: propagate CapVer (#17093)
To support integration testing of client features that rely on it, e.g.
peer relay.

Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Jordan Whited 6feb6f3c75
wgengine/magicsock: add relayManager event logs (#17091)
These are gated behind magicsock component debug logging.

Updates tailscale/corp#30818

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Tom Proctor 1ec3d20d10
cmd/k8s-operator: simplify scope of e2e tests (#17076)
Removes ACL edits from e2e tests in favour of trying to simplify the
tests and separate the actual test logic from the environment setup
logic as much as possible. Also aims to fit in with the requirements
that will generally be filled anyway for most devs working on the
operator; in particular using tags that fit in with our documentation.

Updates tailscale/corp#32085

Change-Id: I7659246e39ec0b7bcc4ec0a00c6310f25fe6fac2

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
5 months ago
Jordan Whited 2d9d869d3d
wgengine/magicsock: fix debug disco printing of alloc resp disco keys (#17087)
Updates tailscale/corp#30818

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Jordan Whited 09bfee2e06
disco: add missing message types to MessageSummary (#17081)
Updates tailscale/corp#30818

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
nikiUppal-TS 88d7db33da
cmd/tailscale: use tailnet display name on cli (#17079)
Updates cli to use tailnet display name

Updates tailscale/corp#32108

Signed-off-by: nikiUppal-TS <nikita@tailscale.com>
5 months ago
Nick O'Neill 77250a301a
ipn/ipnlocal, types: plumb tailnet display name cap through to network profile (#17045)
Updates tailscale/corp#30456

Signed-off-by: Nick O'Neill <nick@tailscale.com>
5 months ago
Brad Fitzpatrick f1ded84454 cmd/tailscaled: add disabled debug file to force reflect for binary size experiments
This adds a file that's not compiled by default that exists just to
make it easier to do binary size checks, probing what a binary would
be like if it included reflect methods (as used by html/template, etc).

As an example, once tailscaled uses reflect.Type.MethodByName(non-const-string) anywhere,
the build jumps up by 14.5 MB:

    $ GOOS=linux GOARCH=amd64 ./tool/go build -tags=ts_include_cli,ts_omit_webclient,ts_omit_systray,ts_omit_debugeventbus -o before ./cmd/tailscaled

    $ GOOS=linux GOARCH=amd64 ./tool/go build -tags=ts_include_cli,ts_omit_webclient,ts_omit_systray,ts_omit_debugeventbus,ts_debug_forcereflect -o after ./cmd/tailscaled

    $ ls -l before after
    -rwxr-xr-x@ 1 bradfitz  staff  41011861 Sep  9 07:28 before
    -rwxr-xr-x@ 1 bradfitz  staff  55610948 Sep  9 07:29 after

This is particularly pronounced with large deps like the AWS SDK. If you compare using ts_omit_aws:

    -rwxr-xr-x@ 1 bradfitz  staff  38284771 Sep  9 07:40 no-aws-no-reflect
    -rwxr-xr-x@ 1 bradfitz  staff  45546491 Sep  9 07:41 no-aws-with-reflect

That means adding AWS to a non-reflect binary adds 2.7 MB but adding
AWS to a reflect binary adds 10 MB.

Updates #17063
Updates #12614

Change-Id: I18e9b77c9cf33565ce5bba65ac5584fa9433f7fb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Alex Chan f4ae81e015
tsnet: remove APIClient() which is deprecated and now unused (#17073)
Updates tailscale/corp#22748

Signed-off-by: Alex Chan <alexc@tailscale.com>
5 months ago
Brad Fitzpatrick 3e4b0c1516 cmd/tailscale, ipn/ipnlocal: add ts_omit_webclient
Fixes #17063
Updates #12614

Change-Id: I0a189f6a4d1c4558351e3195839867725774fa96
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick ffc82ad820 util/eventbus: add ts_omit_debugeventbus
Updates #17063

Change-Id: Ibc98dd2088f82c829effa71f72f3e2a5abda5038
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 6f9f190f4d go.toolchain.rev: bump to Go 1.25.1
Updates #17064

Change-Id: Ibbca837e0921fe9f82fc931dde8bb51b017e4e48
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
License Updater 2da52dce7a licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
5 months ago
Alex Chan 71cb6d4cbd
cmd/tailscale/cli, derp: use client/local instead of deprecated client/tailscale (#17061)
* cmd/tailscale/cli: use client/local instead of deprecated client/tailscale

Updates tailscale/corp#22748

Signed-off-by: Alex Chan <alexc@tailscale.com>

* derp: use client/local instead of deprecated client/tailscale

Updates tailscale/corp#22748

Signed-off-by: Alex Chan <alexc@tailscale.com>

---------

Signed-off-by: Alex Chan <alexc@tailscale.com>
5 months ago
Brad Fitzpatrick 1cb855fb36 util/expvarx: deflake TestSafeFuncHappyPath with synctest
I probably could've deflaked this without synctest, but might as well use
it now that Go 1.25 has it.

Fixes #15348

Change-Id: I81c9253fcb7eada079f3e943ab5f1e29ba8e8e31
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Alex Chan 14adf5b717
utils/expvarx, tstest/integration: mark two tests as known flaky (#17052)
* utils/expvarx: mark TestSafeFuncHappyPath as known flaky

Updates #15348

Signed-off-by: Alex Chan <alexc@tailscale.com>

* tstest/integration: mark TestCollectPanic as known flaky

Updates #15865

Signed-off-by: Alex Chan <alexc@tailscale.com>

---------

Signed-off-by: Alex Chan <alexc@tailscale.com>
5 months ago
Alex Chan ff8900583c
cmd/tailscale/cli: fix the spelling of "routes" (#17039)
Updates #cleanup

Signed-off-by: Alex Chan <alexc@tailscale.com>
5 months ago
Anton Tolchanov ed6aa50bd5 prober: include current probe results in run-probe text response
It was a bit confusing that provided history did not include the
current probe results.

Updates tailscale/corp#20583

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
5 months ago
James Tucker a29545e9cc wgengine/magicsock: log the peer failing disco writes are intended for
Updates tailscale/corp#31762

Signed-off-by: James Tucker <james@tailscale.com>
5 months ago
Mike O'Driscoll 23297da10d
cmd/tailscale/cli: add new line for set --webclient (#17043)
Fixes #17042

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
5 months ago
James Sanderson 046b8830c7 ipn/ipnlocal: add state change test for key expiry
Updates tailscale/corp#31478

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
5 months ago
Brad Fitzpatrick 46369f06af util/syspolicy/policyclient: always use no-op policyclient in tests by default
We should never use the real syspolicy implementation in tests by
default. (the machine's configuration shouldn't affect tests)

You either specify a test policy, or you get a no-op one.

Updates #16998

Change-Id: I3350d392aad11573a5ad7caab919bb3bbaecb225
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick b034f7cca9 ipn/ipnlocal, util/syspolicy: convert last RegisterWellKnownSettingsForTest caller, remove
Updates #16998

Change-Id: I735d75129a97a929092e9075107e41cdade18944
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
David Bond 624cdd2961
cmd/containerboot: do not reset state on non-existant secret (#17021)
This commit modifies containerboot's state reset process to handle the
state secret not existing. During other parts of the boot process we
gracefully handle the state secret not being created yet, but missed
that check within `resetContainerbootState`

Fixes https://github.com/tailscale/tailscale/issues/16804

Signed-off-by: David Bond <davidsbond93@gmail.com>
5 months ago
Brad Fitzpatrick d8ac539bf9 util/syspolicy: remove handler, other dead code
Fixes #17022

Change-Id: I6a0f6488ae3ea75c5844dfcba68e1e8024e930be
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
David Bond 04f00339b6
cmd/k8s-operator: update connector example (#17020)
This commit modifies the connector example to use the new hostname prefix
and replicas fields

Signed-off-by: David Bond <davidsbond93@gmail.com>
5 months ago
Jonathan Nobels a2f2ac6ba1
ipn/local: fix deadlock in initial suggested exit node query (#17025)
updates tailscale/corp#26369

b.mu is locked here.  We need to use suggestExitNodeLocked.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
5 months ago
Craig Hesling 2b9d055101 drive: fix StatCache mishandling of paths with spaces
Fix "file not found" errors when WebDAV clients access files/dirs inside
directories with spaces.

The issue occurred because StatCache was mixing URL-escaped and
unescaped paths, causing cache key mismatches.
Specifically, StatCache.set() parsed WebDAV responses containing
URL-escaped paths (ex. "Dir%20Space/file1.txt") and stored them
alongside unescaped cache keys (ex. "Dir Space/file1.txt").
This mismatch prevented StatCache.get() from correctly determining whether
a child file existed.

See https://github.com/tailscale/tailscale/issues/13632#issuecomment-3243522449
for the full explanation of the issue.

The decision to keep all paths references unescaped inside the StatCache
is consistent with net/http.Request.URL.Path and rewrite.go (sole consumer)

Update unit test to detect this directory space mishandling.

Fixes tailscale#13632

Signed-off-by: Craig Hesling <craig@hesling.com>
5 months ago
Brad Fitzpatrick 0f3598b467 util/syspolicy: delete some unused code in handler.go
There's a TODO to delete all of handler.go, but part of it's
still used in another repo.

But this deletes some.

Updates #17022

Change-Id: Ic5a8a5a694ca258440307436731cd92b45ee2d21
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Alex Chan c9f214e503
ipn: warn about self as the exit node if backend is running (#17018)
Before:

    $ tailscale ip -4
    1.2.3.4

    $ tailscale set --exit-node=1.2.3.4
    no node found in netmap with IP 1.2.3.4

After:

    $ tailscale set --exit-node=1.2.3.4
    cannot use 1.2.3.4 as an exit node as it is a local IP address to this machine; did you mean --advertise-exit-node?

The new error message already existed in the code, but would only be
triggered if the backend wasn't running -- which means, in practice,
it would almost never be triggered.

The old error message is technically true, but could be confusing if you
don't know the distinction between "netmap" and "tailnet" -- it could
sound like the exit node isn't part of your tailnet. A node is never in
its own netmap, but it is part of your tailnet.

This error confused me when I was doing some local dev work, and it's
confused customers before (e.g. #7513). Using the more specific error
message should reduce confusion.

Updates #7513
Updates https://github.com/tailscale/corp/issues/23596

Signed-off-by: Alex Chan <alexc@tailscale.com>
5 months ago
Brad Fitzpatrick d06d9007a6 ipn/ipnlocal: convert more tests to use policytest, de-global-ify
Now that we have policytest and the policyclient.Client interface, we
can de-global-ify many of the tests, letting them run concurrently
with each other, and just removing global variable complexity.

This does ~half of the LocalBackend ones.

Updates #16998

Change-Id: Iece754e1ef4e49744ccd967fa83629d0dca6f66a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 21f21bd2a2 util/syspolicy: finish adding ts_omit_syspolicy build tags, tests
Fixes #16998
Updates #12614

Change-Id: Idf2b1657898111df4be31f356091b2376d0d7f0b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 24b8a57b1e util/syspolicy/policytest: move policy test helper to its own package
Updates #16998
Updates #12614

Change-Id: I9fd27d653ebee547951705dc5597481e85b60747
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 2b3e533048 util/syspolicy: finish plumbing policyclient, add feature/syspolicy, move global impl
This is step 4 of making syspolicy a build-time feature.

This adds a policyclient.Get() accessor to return the correct
implementation to use: either the real one, or the no-op one. (A third
type, a static one for testing, also exists, so in general a
policyclient.Client should be plumbed around and not always fetched
via policyclient.Get whenever possible, especially if tests need to use
alternate syspolicy)

Updates #16998
Updates #12614

Change-Id: Iaf19670744a596d5918acfa744f5db4564272978
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
M. J. Fromberger 9e9bf13063
ipn/ipnlocal: revert some locking changes ahead of release branch cut (#17011) 5 months ago
Brad Fitzpatrick 0d23490e1a ipn/ipnlocal: simplify a test with a new simpler syspolicy client test type
Less indirection.

Updates #16998
Updates #12614

Change-Id: I5a3a3c3f3b195486b2731ec002d2532337b3d211
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 1ca4ae598a ipn/ipnlocal: use policyclient.Client always, stop using global syspolicy funcs
Step 4 of N. See earlier commits in the series (via the issue) for the
plan.

This adds the missing methods to policyclient.Client and then uses it
everywhere in ipn/ipnlocal and locks it in with a new dep test.

Still plenty of users of the global syspolicy elsewhere in the tree,
but this is a lot of them.

Updates #16998
Updates #12614

Change-Id: I25b136539ae1eedbcba80124de842970db0ca314
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Brad Fitzpatrick 2434bc69fc util/syspolicy/{setting,ptype}: move PreferenceOption and Visibility to new leaf package
Step 3 in the series. See earlier cc532efc20 and d05e6dc09e.

This step moves some types into a new leaf "ptype" package out of the
big "settings" package. The policyclient.Client will later get new
methods to return those things (as well as Duration and Uint64, which
weren't done at the time of the earlier prototype).

Updates #16998
Updates #12614

Change-Id: I4d72d8079de3b5351ed602eaa72863372bd474a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Percy Wegmann 42a215e12a cmd/tailscale/cli: prompt for y/n when attempting risky action
Previously, when attempting a risky action, the CLI printed a 5 second countdown saying
"Continuing in 5 seconds...". When the countdown finished, the CLI aborted rather than
continuing.

To avoid confusion, but also avoid accidentally continuing if someone (or an automated
process) fails to manually abort within the countdown, we now explicitly prompt for a
y/n response on whether or not to continue.

Updates #15445

Co-authored-by: Kot C <kot@kot.pink>
Signed-off-by: Percy Wegmann <percy@tailscale.com>
5 months ago
License Updater dbc54addd0 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
5 months ago
nikiUppal-TS 0f5d3969ca
tailcfg: add tailnet display name field (#16907)
Updates the NodeCapabilities to contain Tailnet Display Name

Updates tailscale/corp#30462

Signed-off-by: nikiUppal-TS <nikita@tailscale.com>
5 months ago
Brad Fitzpatrick 61d3693e61 cmd/tailscale/cli: add a debug command to force a risky action
For testing risky action flows.

Updates #15445

Change-Id: Id81e54678a1fe5ccedb5dd9c6542ff48c162b349
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
David Bond 12ad630128
cmd/k8s-operator: allow specifying replicas for connectors (#16721)
This commit adds a `replicas` field to the `Connector` custom resource that
allows users to specify the number of desired replicas deployed for their
connectors.

This allows users to deploy exit nodes, subnet routers and app connectors
in a highly available fashion.

Fixes #14020

Signed-off-by: David Bond <davidsbond93@gmail.com>
5 months ago
Brad Fitzpatrick d05e6dc09e util/syspolicy/policyclient: add policyclient.Client interface, start plumbing
This is step 2 of ~4, breaking up #14720 into reviewable chunks, with
the aim to make syspolicy be a build-time configurable feature.

Step 1 was #16984.

In this second step, the util/syspolicy/policyclient package is added
with the policyclient.Client interface.  This is the interface that's
always present (regardless of build tags), and is what code around the
tree uses to ask syspolicy/MDM questions.

There are two implementations of policyclient.Client for now:

1) NoPolicyClient, which only returns default values.
2) the unexported, temporary 'globalSyspolicy', which is implemented
   in terms of the global functions we wish to later eliminate.

This then starts to plumb around the policyclient.Client to most callers.

Future changes will plumb it more. When the last of the global func
callers are gone, then we can unexport the global functions and make a
proper policyclient.Client type and constructor in the syspolicy
package, removing the globalSyspolicy impl out of tsd.

The final change will sprinkle build tags in a few more places and
lock it in with dependency tests to make sure the dependencies don't
later creep back in.

Updates #16998
Updates #12614

Change-Id: Ib2c93d15c15c1f2b981464099177cd492d50391c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Erisa A 921d53904c
CODE_OF_CONDUCT.md: fix duplicate entry (#16814)
Remove duplicate entry not present on approved wording
Updates #cleanup

Signed-off-by: Erisa A <erisa@tailscale.com>
5 months ago
Brad Fitzpatrick cc532efc20 util/syspolicy/*: move syspolicy keys to new const leaf "pkey" package
This is step 1 of ~3, breaking up #14720 into reviewable chunks, with
the aim to make syspolicy be a build-time configurable feature.

In this first (very noisy) step, all the syspolicy string key
constants move to a new constant-only (code-free) package. This will
make future steps more reviewable, without this movement noise.

There are no code or behavior changes here.

The future steps of this series can be seen in #14720: removing global
funcs from syspolicy resolution and using an interface that's plumbed
around instead. Then adding build tags.

Updates #12614

Change-Id: If73bf2c28b9c9b1a408fe868b0b6a25b03eeabd1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Simon Law 6d45fcfc93
.github/workflows: reviewing depaware.txt is unnecessary (#16990)
Apparently, #16989 introduced a bug in request-dataplane-review.yml:

> you may only define one of `paths` and `paths-ignore` for a single event

Related #16372
Updates #cleanup

Signed-off-by: Simon Law <sfllaw@tailscale.com>
5 months ago
Simon Law 442f4758a9
.github/workflows: reviewing depaware.txt is unnecessary (#16989)
@tailscale/dataplane almost never needs to review depaware.txt, when
it is the only change to the DERP implementation.

Related #16372
Updates #cleanup

Signed-off-by: Simon Law <sfllaw@tailscale.com>
5 months ago
James Tucker 3b68d607be wgengine/magicsock: drop DERP queue from head rather than tail
If the DERP queue is full, drop the oldest item first, rather than the
youngest, on the assumption that older data is more likely to be
unanswerable.

Updates tailscale/corp#31762

Signed-off-by: James Tucker <james@tailscale.com>
5 months ago
Remy Guercio 89fe2e1f12
cmd/tsidp: add allow-insecure-no-client-registration and JSON file migration (#16881)
Add a ternary flag that unless set explicitly to false keeps the
insecure behavior of TSIDP.

If the flag is false, add functionality on startup to migrate
oidc-funnel-clients.json to oauth-clients.json if it doesn’t exist.
If the flag is false, modify endpoints to behave similarly regardless
of funnel, tailnet, or localhost. They will all verify client ID & secret
when appropriate per RFC 6749. The authorize endpoint will no longer change
based on funnel status or nodeID.

Add extra tests verifying TSIDP endpoints behave as expected
with the new flag.

Safely create the redirect URL from what's passed into the
authorize endpoint.

Fixes #16880

Signed-off-by: Remy Guercio <remy@tailscale.com>
5 months ago
Naman Sood 76fc02be09
words: just an ordinary commit, nothing fishy at all (#16982)
* words: just an ordinary commit, nothing fishy at all

Updates #words

Signed-off-by: Naman Sood <mail@nsood.in>
5 months ago
Joe Tsai 7cbcc10eb1
syncs: add Semaphore.Len (#16981)
The Len reports the number of acquired tokens for metrics.

Updates tailscale/corp#31252

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
5 months ago
Brad Fitzpatrick 1a98943204 go.mod: bump github.com/ulikunitz/xz for security warning
Doesn't look to affect us, but pacifies security scanners.

See 88ddf1d0d9

It's for decoding. We only use this package for encoding (via
github.com/google/rpmpack / github.com/goreleaser/nfpm/v2).

Updates #8043

Change-Id: I87631aa5048f9514bb83baf1424f6abb34329c46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
5 months ago
Joe Tsai 3aea0e095a syncs: delete WaitGroup and use sync.WaitGroup.Go in Go 1.25
Our own WaitGroup wrapper type was a prototype implementation
for the Go method on the standard sync.WaitGroup type.
Now that there is first-class support for Go,
we should migrate over to using it and delete syncs.WaitGroup.

Updates #cleanup
Updates tailscale/tailscale#16330

Change-Id: Ib52b10f9847341ce29b4ca0da927dc9321691235
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
5 months ago
James Tucker f5d3c59a92 wgengine/magicsock: shorten process internal DERP queue
DERP writes go via TCP and the host OS will have plenty of buffer space.
We've observed in the wild with a backed up TCP socket kernel side
buffers of >2.4MB. The DERP internal queue being larger causes an
increase in the probability that the contents of the backbuffer are
"dead letters" - packets that were assumed to be lost.

A first step to improvement is to size this queue only large enough to
avoid some of the initial connect stall problem, but not large enough
that it is contributing in a substantial way to buffer bloat /
dead-letter retention.

Updates tailscale/corp#31762

Signed-off-by: James Tucker <james@tailscale.com>
5 months ago
James Tucker d42f0b6a21 util/ringbuffer: rename to ringlog
I need a ringbuffer in the more traditional sense, one that has a notion
of item removal as well as tail loss on overrun. This implementation is
really a clearable log window, and is used as such where it is used.

Updates #cleanup
Updates tailscale/corp#31762

Signed-off-by: James Tucker <james@tailscale.com>
5 months ago
License Updater 4b9a1a0087 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
5 months ago
Patrick O'Doherty 48dbe70b54
go.mod: bump Go 1.25 release (#16969)
Bump Go 1.25 release to include a go/types patch and resolve govulncheck
CI exceptions.

Updates tailscale/corp#31755

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
5 months ago
Maisem Ali 882b05fff9 cmd/viewer: add field comments to generated view methods
Extract field comments from AST and include them in generated view
methods. Comments are preserved from the original struct fields to
provide documentation for the view accessors.

Fixes #16958

Signed-off-by: Maisem Ali <3953239+maisem@users.noreply.github.com>
5 months ago
Jonathan Nobels 80f5a00e76
ipn/local: add the suggested exit node to the ipn bus (#16748)
fixes tailscale/corp#26369

The suggested exit node is currently only calculated during a localAPI request.
For older UIs, this wasn't a bad choice - we could just fetch it on-demand when a menu
presented itself.  For newer incarnations however, this is an always-visible field
that needs to react to changes in the suggested exit node's value.

This change recalculates the suggested exit node ID on netmap updates and
broadcasts it on the IPN bus.   The localAPI version of this remains intact for the
time being.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
5 months ago
Jonathan Nobels 6542a00ab0
tailcfg: add mac-ui-v2 node capability (#16940)
updates tailscale/corp#29841

Adds a  node cap macOS UIs can query to determine
whether then should enable the new windowed UI.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
5 months ago
Jordan Whited 575664b263
wgengine/magicsock: make endpoint.discoPing peer relay aware (#16946)
Updates tailscale/corp#30333

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Patrick O'Doherty c5429cd49c
go.toolchain.branch: bump to go1.25 (#16954)
go.toolchain.rev: bump go1.25 version
flake.nix: bump Go to 1.25

Updates #16330

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
5 months ago
M. J. Fromberger b411ffb52f ipn/ipnlocal: remove UnlockEarly from doSetHostinfoFilterServices
Pull the lock-bearing code into a closure, and use a clone rather than a
shallow copy of the hostinfo record.

Updates #11649

Change-Id: I4f1d42c42ce45e493b204baae0d50b1cbf82b102
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
M. J. Fromberger 9002e5fd6b ipn/ipnlocal: remove an unnecessary unlock shortcut
The early unlock on this branch was required because the "send" method goes on
to acquire the mutex itself. Rather than release the lock just to acquire it
again, call the underlying locked helper directly.

Updates #11649

Change-Id: I50d81864a00150fc41460b7486a9c65655f282f5
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
M. J. Fromberger 2fb9472990 ipn/ipnlocal: remove unnecessary usees of lockAndGetUnlock
In places where we are locking the LocakBackend and immediately deferring an
unlock, and where there is no shortcut path in the control flow below the
deferral, we do not need the unlockOnce helper. Replace all these with use of
the lock directly.

Updates #11649

Change-Id: I3e6a7110dfc9ec6c1d38d2585c5367a0d4e76514
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
Jordan Whited 9403ba8c69
wgengine/magicsock: trigger peer relay path discovery on CallMeMaybe RX (#16929)
Updates tailscale/corp#30333

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Aaron Klotz b5f834aef8 cmd/tailscaled: add Dnscache as a service dependency
Updates https://github.com/tailscale/corp/issues/30961

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
5 months ago
Claus Lensbøl fafb514538
client/systray: go back to using upstream library (#16938)
We had a fix in a local branch, but upstream has merged it now.

Updates #1708

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
5 months ago
Kot C 4236a759f3
cmd/tsidp: Add Docker image to README (#16915)
Signed-off-by: Kot C <kot@kot.pink>
5 months ago
M. J. Fromberger 16bd60f9ca
ipn,tsnet: update AdvertiseTags documentation (#16931)
Instead of referring to groups, which is a term of art for a different entity,
update the doc comments to more accurately describe what tags are in reference
to the policy document.

Updates #cleanup

Change-Id: Iefff6f84981985f834bae7c6a6c34044f53f2ea2
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
M. J. Fromberger 6c8fef961e
ipn/ipnlocal: replace the LockedOnEntry pattern with conventional lock/unlock discipline (#16925)
There are several methods within the LocalBackend that used an unusual and
error-prone lock discipline whereby they require the caller to hold the backend
mutex on entry, but release it on the way out.

In #11650 we added some support code to make this pattern more visible.
Now it is time to eliminate the pattern (at least within this package).
This is intended to produce no semantic changes, though I am relying on
integration tests and careful inspection to achieve that.

To the extent possible I preserved the existing control flow. In a few places,
however, I replaced this with an unlock/lock closure. This means we will
sometimes reacquire a lock only to release it again one frame up the stack, but
these operations are not performance sensitive and the legibility gain seems
worthwhile.

We can probably also pull some of these out into separate methods, but I did
not do that here so as to avoid other variable scope changes that might be hard
to see. I would like to do some more cleanup separately.

As a follow-up, we could also remove the unlockOnce helper, but I did not do
that here either.

Updates #11649

Change-Id: I4c92d4536eca629cfcd6187528381c33f4d64e20
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
Mohammed Naser fa0e83ab4f
tsnet: add Server.AdvertiseTags option (#15840)
Updates #8531

Change-Id: I9b6653872c66929e692bd592ef3f438430c657b5
Signed-off-by: Valentin Alekseev <valentin.alekseev@gmail.com>
Co-authored-by: Valentin Alekseev <valentin.alekseev@gmail.com>
5 months ago
Jordan Whited 86a5292c03
ipn/localapi: make tailscale debug derp STUNOnly-aware (#16927)
Fixes #16926

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Need-an-AwP b558f81a82
fix: invalid memory address or nil pointer dereference (#16922)
Signed-off-by: Need-an-AwP <113933967+Need-an-AwP@users.noreply.github.com>
5 months ago
Tom Proctor 3eeecb4c7f
cmd/k8s-proxy,k8s-operator: fix serve config for userspace mode (#16919)
The serve code leaves it up to the system's DNS resolver and netstack to
figure out how to reach the proxy destination. Combined with k8s-proxy
running in userspace mode, this means we can't rely on MagicDNS being
available or tailnet IPs being routable. I'd like to implement that as a
feature for serve in userspace mode, but for now the safer fix to get
kube-apiserver ProxyGroups consistently working in all environments is to
switch to using localhost as the proxy target instead.

This has a small knock-on in the code that does WhoIs lookups, which now
needs to check the X-Forwarded-For header that serve populates to get
the correct tailnet IP to look up, because the request's remote address
will be loopback.

Fixes #16920

Change-Id: I869ddcaf93102da50e66071bb00114cc1acc1288

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
5 months ago
Jordan Whited c85cdabdfc
net/udprelay: set ICMP err immunity sock opt (#16918)
Updates tailscale/corp#31506

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Jordan Whited b17cfe4aed
wgengine/magicsock,net/sockopts: export Windows ICMP suppression logic (#16917)
For eventual use by net/udprelay.Server.

Updates tailscale/corp#31506

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Jordan Whited cf739256ca
net/udprelay: increase socket buffer size (#16910)
This increases throughput over long fat networks, and in the presence
of crypto/syscall-induced delay.

Updates tailscale/corp#31164

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Mike O'Driscoll e296a6be8d
cmd/tsidp: update oidc-funnel-clients.json store path (#16845)
Update odic-funnel-clients.json to take a path, this
allows setting the location of the file and prevents
it from landing in the root directory or users home directory.

Move setting of rootPath until after tsnet has started.
Previously this was added for the lazy creation of the
oidc-key.json. It's now needed earlier in the flow.

Updates #16734
Fixes #16844

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
5 months ago
dependabot[bot] 3e198f6d5f
.github: Bump github/codeql-action from 3.29.7 to 3.29.8 (#16828)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.7 to 3.29.8.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](51f77329af...76621b61de)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5 months ago
Jordan Whited 641a90ea33
net/sockopts,wgengine/magicsock: export socket buffer sizing logic (#16909)
For eventual use by net/udprelay.Server

Updates tailscale/corp#31164

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Fran Bull b48d2de6ab cmd/natc,tsconsensus: add cluster config admin
Add the ability for operators of natc in consensus mode to remove
servers from the raft cluster config, without losing other state.

Updates #14667

Signed-off-by: Fran Bull <fran@tailscale.com>
5 months ago
Fran Bull d986baa18f tsconsensus,cmd/natc: add 'follower only' bootstrap option
Currently consensus has a bootstrap routine where a tsnet node tries to
join each other node with the cluster tag, and if it is not able to join
any other node it starts its own cluster.

That algorithm is racy, and can result in split brain (more than one
leader/cluster) if all the nodes for a cluster are started at the same
time.

Add a FollowOnly argument to the bootstrap function. If provided this
tsnet node will never lead, it will try (and retry with exponential back
off) to follow any node it can contact.

Add a --follow-only flag to cmd/natc that uses this new tsconsensus
functionality.

Also slightly reorganize some arguments into opts structs.

Updates #14667

Signed-off-by: Fran Bull <fran@tailscale.com>
5 months ago
Jordan Whited d4b7200129
net/udprelay: use batching.Conn (#16866)
This significantly improves throughput of a peer relay server on Linux.

Server.packetReadLoop no longer passes sockets down the stack. Instead,
packet handling methods return a netip.AddrPort and []byte, which
packetReadLoop gathers together for eventual batched writes on the
appropriate socket(s).

Updates tailscale/corp#31164

Signed-off-by: Jordan Whited <jordan@tailscale.com>
5 months ago
Fran Bull 5c560d7489 tsconsensus: check for bootstrap error
We have been unintentionally ignoring errors from calling bootstrap.
bootstrap sometimes calls raft.BootstrapCluster which sometimes returns
a safe to ignore error, handle that case appropriately.

Updates #14667

Signed-off-by: Fran Bull <fran@tailscale.com>
5 months ago
Adrian Dewhurst b28699cd31 types/views: add min/max helpers to views.Slice
This has come up in a few situations recently and adding these helpers
is much better than copying the slice (calling AsSlice()) in order to
use slices.Max and friends.

Updates #cleanup

Change-Id: Ib289a07d23c3687220c72c4ce341b9695cd875bf
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
5 months ago
Mike O'Driscoll 2581e38789
prober: update runall handler to be generic (#16895)
Update the runall handler to be more generic with an
exclude param to exclude multiple probes as the requesters
definition.

Updates tailscale/corp#27370

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
5 months ago
Mike O'Driscoll e4031daa08
.github/Makefile/flake: update nix flake support (#16636)
Cleanup nix support, make flake easier to read with nix-systems.
This also harmonizes with golinks flake setup and reduces an input
dependency by 1.

Update deps test to ensure the vendor hash stays harmonized
with go.mod.

Update make tidy to ensure vendor hash stays current.

Overlay the current version of golang, tailscale runs
recent releases faster than nixpkgs can update them into
the unstable branch.

Updates #16637

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
5 months ago
Aaron Klotz 84472167dd tool/gocross: fix environment variable clearing in gocross-wrapper.ps1
The -Environment argument to Start-Process is essentially being treated
as a delta; removing a particular variable from the argument's hash
table does not indicate to delete. Instead we must set the value of each
unwanted variable to $null.

Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
5 months ago
Aaron Klotz d92789affa tool/gocross: don't set executable bits on PowerShell script
Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
5 months ago
M. J. Fromberger 55698c8511
ipn/localapi: plumb an event bus through the localapi.Handler (#16892)
Some of the operations of the local API need an event bus to correctly
instantiate other components (notably including the portmapper).

This commit adds that, and as the parameter list is starting to get a bit long
and hard to read, I took the opportunity to move the arguments to a config
type. Only a few call sites needed to be updated and this API is not intended
for general use, so I did not bother to stage the change.

Updates #15160
Updates #16842

Change-Id: I7b057d71161bd859f5acb96e2f878a34c85be0ef
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
5 months ago
Aaron Klotz 02f6030dbd tool, tool/gocross: update gocross to support building natively on Windows and add a PowerShell Core wrapper script
gocross-wrapper.ps1 is a PowerShell core script that is essentially a
straight port of gocross-wrapper.sh. It requires PowerShell 7.4, which
is the latest LTS release of PSCore.

Why use PowerShell Core instead of Windows PowerShell? Essentially
because the former is much better to script with and is the edition
that is currently maintained.

Because we're using PowerShell Core, but many people will be running
scripts from a machine that only has Windows PowerShell, go.cmd has
been updated to prompt the user for PowerShell core installation if
necessary.

gocross-wrapper.sh has also been updated to utilize the PSCore script
when running under cygwin or msys.

gocross itself required a couple of updates:

We update gocross to output the PowerShell Core wrapper alongside the
bash wrapper, which will propagate the revised scripts to other repos
as necessary.

We also fix a couple of things in gocross that didn't work on Windows:
we change the toolchain resolution code to use os.UserHomeDir instead
of directly referencing the HOME environment variable, and we fix a
bug in the way arguments were being passed into exec.Command on
non-Unix systems.

Updates https://github.com/tailscale/corp/issues/29940

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
5 months ago
Will Norris 9d9a70d81d client/systray: disable 'more settings' menu if backend not running
Updates #1708

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
5 months ago
Mike O'Driscoll 6d45663dd4
cmd/derpprobe,prober: add run all probes handler (#16875)
Add a Run all probes handler that executes all
probes except those that are continuous or the derpmap
probe.

This is leveraged by other tooling to confirm DERP
stability after a deploy.

Updates tailscale/corp#27370

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
6 months ago
Percy Wegmann 192fa6f05d {cmd/dist,release/dist}: add support for intermediary QNAP signing certificates
Updates #23528

Signed-off-by: Percy Wegmann <percy@tailscale.com>
6 months ago
Jonathan Nobels 6006bc92b5
net/{netns, netmon}: use LastKnownDefaultInterface if set and check for utun (#16873)
fixes tailscale/corp#31299

Fixes two issues:
getInterfaceIndex would occasionally race with netmon's state, returning
the cached default interface index after it had be changed by NWNetworkMonitor.
This had the potential to cause connections to bind to the prior default.  The fix
here is to preferentially use the interface index provided by NWNetworkMonitor
preferentially.

When no interfaces are available, macOS will set the tunnel as the default
interface when an exit node is enabled, potentially causing getInterfaceIndex
to return utun's index.  We now guard against this when taking the
defaultIdx path.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
6 months ago
Percy Wegmann 9c39296ab5 release/dist/qnap: verify code signing
This pulls in a change from github.com/tailscale/QDK to verify code signing
when using QNAP_SIGNING_SCRIPT.

It also upgrades to the latest Google Cloud PKCS#11 library, and reorders
the Dockerfile to allow for more efficient future upgrades to the included QDK.

Updates tailscale/corp#23528

Signed-off-by: Percy Wegmann <percy@tailscale.com>
6 months ago
Patrick O'Doherty 55beba4094
types/key: init HardwareAttestionKey implementation (#16867)
Define the HardwareAttestionKey interface describing a platform-specific
hardware backed node identity attestation key. Clients will register the
key type implementations for their platform.

Updates tailscale/corp#31269

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
6 months ago
Will Hannah 5b6c64b187
net/tshttpproxy: use errors.New for error creation (#16860)
Updates tailscale/corp#30668

Signed-off-by: Will Hannah <willh@tailscale.com>
6 months ago
Michael Ben-Ami 3f1851a6d9 types/dnstype, ipn/ipnlocal: allow other DNS resolvers with exit nodes
dnstype.Resolver adds a boolean UseWithExitNode that controls
whether the resolver should be used in tailscale exit node contexts
(not wireguard exit nodes). If UseWithExitNode resolvers are found,
they are installed as the global resolvers. If no UseWithExitNode resolvers
are found, the exit node resolver continues to be installed as the global
resolver. Split DNS Routes referencing UseWithExitNode resolvers are also
installed.

Updates #8237

Fixes tailscale/corp#30906
Fixes tailscale/corp#30907

Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
6 months ago
Will Norris b8c45a6a8f client/systray: add CLI error output if operator is missing
We already show a message in the menu itself, this just adds it to the
CLI output as well.

Updates #1708

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
6 months ago
Joe Tsai fbb91758ac
cmd/viewer, types/views: implement support for json/v2 (#16852)
This adds support for having every viewer type implement
jsonv2.MarshalerTo and jsonv2.UnmarshalerFrom.

This provides a significant boost in performance
as the json package no longer needs to validate
the entirety of the JSON value outputted by MarshalJSON,
nor does it need to identify the boundaries of a JSON value
in order to call UnmarshalJSON.

For deeply nested and recursive MarshalJSON or UnmarshalJSON calls,
this can improve runtime from O(N²) to O(N).

This still references "github.com/go-json-experiment/json"
instead of the experimental "encoding/json/v2" package
now available in Go 1.25 under goexperiment.jsonv2
so that code still builds without the experiment tag.
Of note, the "github.com/go-json-experiment/json" package
aliases the standard library under the right build conditions.

Updates tailscale/corp#791

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
6 months ago
Jordan Whited c083a9b053
net/batching: fix compile-time assert (#16864)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Anton Tolchanov 819db6759c tka: block key addition when the max number of keys is reached
Updates #16607

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
6 months ago
Will Hannah 5402620db8
net/tshttpproxy: add macOS support for system proxy (#16826)
Adds a setter for proxyFunc to allow macOS to pull defined
system proxies. Disallows overriding if proxyFunc is set via config.

Updates tailscale/corp#30668

Signed-off-by: Will Hannah <willh@tailscale.com>
6 months ago
Brad Fitzpatrick e4d2822afc go.toolchain.rev: bump Go for data race in Go http client
Updates golang/go#73522
Updates tailscale/go#131
Updates tailscale/corp#31133

Change-Id: Ibb7a98944ef287d455ce4f5d202b2e2bd6d8742b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
6 months ago
Andrew Lytvynov 0f7facfeee
control/controlclient: fix data race on tkaHead (#16855)
Grab a copy under mutex in sendMapRequest.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
6 months ago
Jordan Whited 16bc0a5558
net/{batching,packet},wgengine/magicsock: export batchingConn (#16848)
For eventual use by net/udprelay.Server.

Updates tailscale/corp#31164

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Andrew Lytvynov f22c7657e5
cmd/tailscale: add --json-docs flag (#16851)
This prints all command and flag docs as JSON. To be used for generating
the contents of https://tailscale.com/kb/1080/cli.

Updates https://github.com/tailscale/tailscale-www/issues/4722

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
6 months ago
dependabot[bot] d07166b87d
.github: Bump actions/cache from 4.2.3 to 4.2.4 (#16829)
Bumps [actions/cache](https://github.com/actions/cache) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](5a3ec84eff...0400d5f644)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
M. J. Fromberger ee0c7b05a5
cmd/tailscale: fix a panic in netcheck portmapper construction (#16843)
This affects the 1.87.33 unstable release.

Updates #16842
Updates #15160

Change-Id: Ie6d1b2c094d1a6059fbd1023760567900f06e0ad
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
6 months ago
Jordan Whited cde65dba16
wgengine/magicsock: add clientmetric for Peer Relay challenge reception (#16834)
Updates tailscale/corp#30527

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Jordan Whited 4fa27db8dd
wgengine/magicsock: add clientmetrics for locally delivered Peer Relay alloc disco (#16833)
Expected when Peer Relay'ing via self. These disco messages never get
sealed, and never leave the process.

Updates tailscale/corp#30527

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Jordan Whited 36397f1794
wgengine/magicsock: add clientmetrics for TX direction Peer Relay disco messages (#16831)
Updates tailscale/corp#30527

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Mike O'Driscoll 03c4b2a0d0
derp/derphttp: test improvements (#16723)
Update some logging to help future failures.
Improve test shutdown concurrency issues.

Fixes #16722

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
6 months ago
Jordan Whited d122f0350e
control/controlknobs,tailcfg,wgengine/magicsock: deprecate NodeAttrDisableMagicSockCryptoRouting (#16818)
Peer Relay is dependent on crypto routing, therefore crypto routing is
now mandatory.

Updates tailscale/corp#20732
Updates tailscale/corp#31083

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
dependabot[bot] 71d51eb8db
.github: bump github/codeql-action from 3.29.3 to 3.29.5 (#16765)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.3 to 3.29.5.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](d6bbdef45e...51f77329af)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
Brad Fitzpatrick 796eb21204 go.toolchain.rev: bump tsgo toolchain
Updates tailscale/go#129

Change-Id: I94debd1d0b7080c5b012f200ad98d22c3048f350
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
6 months ago
Claus Lensbøl 5297dc3baf
cmd/tailscale/cli: move systray configuration to tailscale configure (#16817)
Updates #1708

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
6 months ago
Andrew Lytvynov b5283ab13a
go.toolchain.rev: bump to 1.24.6 (#16811)
Updates https://github.com/tailscale/corp/issues/31103

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
6 months ago
Claus Lensbøl 3fe022877a
client/systray: temporarily replace systray module (#16807)
We are waiting for a PR to be reviewed upstream.

https://github.com/fyne-io/systray/pull/100

Updates #1708

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
6 months ago
Erisa A d4060f1a39
CODE_OF_CONDUCT.md: update Code of Conduct (#16806)
Updates #cleanup

Signed-off-by: Erisa A <erisa@tailscale.com>
6 months ago
Claus Lensbøl 89954fbceb
client/systray: add startup script generator for systemd (#16801)
Updates #1708

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
6 months ago
Jordan Whited 4666d4ca2a
wgengine/magicsock: fix missing Conn.hasPeerRelayServers.Store() call (#16792)
This commit also extends the updateRelayServersSet unit tests to cover
onNodeViewsUpdate.

Fixes tailscale/corp#31080

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Jordan Whited 0374e6d906
wgengine/magicsock: add lazyEndpoint.FromPeer tests (#16791)
Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Jordan Whited 02967ffcf2
wgengine/magicsock: add lazyEndpoint.InitiationMessagePublicKey tests (#16790)
Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Jordan Whited 908f20e0a5
wgengine/magicsock: add receiveIP() unit tests (#16781)
One of these tests highlighted a Geneve encap bug, which is also fixed
in this commit.

looksLikeInitMsg was passed a packet post Geneve header stripping with
slice offsets that had not been updated to account for the stripping.

Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
TheBigBear 57d653014b
scripts/installer.sh: add FreeBSD 15 (#16741)
* Update installer.sh add FreeBSD ver 15

this should fix the issue on https://github.com/tailscale/tailscale/issues/16740

Signed-off-by: TheBigBear <471105+TheBigBear@users.noreply.github.com>

* scripts/installer.sh: small indentation change

Signed-off-by: Erisa A <erisa@tailscale.com>
Fixes #16740

---------

Signed-off-by: TheBigBear <471105+TheBigBear@users.noreply.github.com>
Signed-off-by: Erisa A <erisa@tailscale.com>
Co-authored-by: Erisa A <erisa@tailscale.com>
6 months ago
Andrew Lytvynov f80ea92030
.github/workflows: enforce github action version pinning (#16768)
Use https://github.com/stacklok/frizbee via the new `go tool` support
from Go 1.24.

Updates https://github.com/tailscale/corp/issues/31017

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
6 months ago
Erisa A ad273d75b7
scripts/installer.sh: add bazzite handling (#16779)
Fixes #14540

Signed-off-by: Erisa A <erisa@tailscale.com>
6 months ago
Will Norris 9f29c428f4 client/systray: allow specifying tailscaled socket
Pass a local.Client to systray.Run, so we can use the existing global
localClient in the cmd/tailscale CLI.  Add socket flag to cmd/systray.

Updates #1708

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
6 months ago
Claus Lensbøl 5bb42e3018
wgengine/router: rely on events for deleted IP rules (#16744)
Adds the eventbus to the router subsystem.

The event is currently only used on linux.

Also includes facilities to inject events into the bus.

Updates #15160

Signed-off-by: Claus Lensbøl <claus@tailscale.com>
6 months ago
Jordan Whited b0018f1e7d
wgengine/magicsock: fix looksLikeInitiationMsg endianness (#16771)
WireGuard message type is little-endian encoded.

Updates tailscale/corp#30903

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
Will Norris 834630fedf cmd/tailscale: add systray subcommand on Linux builds
This will start including the sytray app in unstable builds for Linux,
unless the `ts_omit_systray` build flag is specified.

If we decide not to include it in the v1.88 release, we can pull it
back out or restrict it to unstable builds.

Updates #1708

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
6 months ago
Will Norris 0f15e44196 Makefile: sort make commands and fix printing newlines
Updates #cleanup

Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d
Signed-off-by: Will Norris <will@tailscale.com>
6 months ago
kari-ts d897d809d6
feature/taildrop: do not use m.opts.Dir for Android (#16316)
In Android, we are prompting the user to select a Taildrop directory when they first receive a Taildrop: we block writes on Taildrop dir selection. This means that we cannot use Dir inside managerOptions, since the http request would not get the new Taildrop extension. This PR removes, in the Android case, the reliance on m.opts.Dir, and instead has FileOps hold the correct directory.

This expands FileOps to be the Taildrop interface for all file system operations.

Updates tailscale/corp#29211

Signed-off-by: kari-ts <kari@tailscale.com>

restore tstest
6 months ago
mzbenami 5865d0a61a
Makefile: 'generate' target (#16746)
Signed-off-by: Michael Ben-Ami <mzb@tailscale.com>
6 months ago
Lee Briggs f2fd7a0514
cmd/k8s-operator,k8s-operator: allow setting a `priorityClassName` (#16685)
* cmd/k8s-operator,k8s-operator: allow setting a `priorityClassName`

Fixes #16682

Signed-off-by: Lee Briggs <lee@leebriggs.co.uk>

* Update k8s-operator/apis/v1alpha1/types_proxyclass.go

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
Signed-off-by: Lee Briggs <jaxxstorm@users.noreply.github.com>

* run make kube-generate-all

Change-Id: I5f8f16694fdc181b048217b9f05ec2ee2aa04def
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>

---------

Signed-off-by: Lee Briggs <lee@leebriggs.co.uk>
Signed-off-by: Lee Briggs <jaxxstorm@users.noreply.github.com>
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
6 months ago
jishudashu 23a0398136
ipn/ipnlocal, net/dns: use slices.Equal to simplify code (#16641)
Signed-off-by: jishudashu <979260390@qq.com>
6 months ago
Mike O'Driscoll 47b5f10165
cmd/tsidp,tsnet: update tsidp oidc-key store path (#16735)
The tsidp oidc-key.json ended up in the root directory
or home dir of the user process running it.

Update this to store it in a known location respecting
the TS_STATE_DIR and flagDir options.

Fixes #16734

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
6 months ago
Joe Tsai 1cc842b389
util/set: add more functionality to IntSet (#16640) 6 months ago
Tom Proctor eed3e5dc61
ipn/store/kubestore,kube: fix cert error in admin UI (#16717)
Also adds a test to kube/kubeclient to defend against the error type
returned by the client changing in future.

Fixes tailscale/corp#30855

Change-Id: Id11d4295003e66ad5c29a687f1239333c21226a4

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
6 months ago
Percy Wegmann aa6a2d1e56 drive/driveimpl: use sudo or su to run file server
Some systems have `sudo`, some have `su`. This tries both, increasing
the chance that we can run the file server as an unprivileged user.

Updates #14629

Signed-off-by: Percy Wegmann <percy@tailscale.com>
6 months ago
Jordan Whited 3d1e4f147a
tstest/natlab: fix conn.Close race with conn.ReadFromUDPAddrPort (#16710)
If a conn.Close call raced conn.ReadFromUDPAddrPort before it could
"register" itself as an active read, the conn.ReadFromUDPAddrPort would
never return.

This commit replaces all the activeRead and breakActiveReads machinery
with a channel. These constructs were only depended upon by
SetReadDeadline, and SetReadDeadline was unused.

Updates #16707

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
KevinLiang10 e37432afb7
cmd/tailscale/cli: update message for disable service (#16705)
This commit update the message for recommanding clear command after running serve for service.
Instead of a flag, we pass the service name as a parameter.

Fixes tailscale/corp#30846

Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
6 months ago
M. J. Fromberger b34cdc9710
ipn,net,tsnet,wgengine: make an eventbus mandatory where it is used (#16594)
In the components where an event bus is already plumbed through, remove the
exceptions that allow it to be omitted, and update all the tests that relied on
those workarounds execute properly.

This change applies only to the places where we're already using the bus; it
does not enforce the existence of a bus in other components (yet),

Updates #15160

Change-Id: Iebb92243caba82b5eb420c49fc3e089a77454f65
Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
6 months ago
dependabot[bot] e5e4386f33
build(deps): bump @babel/runtime from 7.23.4 to 7.26.10 in /client/web (#15299)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.23.4 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
Nick Khyl 4df02bbb48 util/syspolicy/setting: use a custom marshaler for time.Duration
jsonv2 now returns an error when you marshal or unmarshal a time.Duration
without an explicit format flag. This is an intentional, temporary choice until
the default [time.Duration] representation is decided (see golang/go#71631).

setting.Snapshot can hold time.Duration values inside a map[string]any,
so the jsonv2 update breaks marshaling. In this PR, we start using
a custom marshaler until that decision is made or golang/go#71664
lets us specify the format explicitly.

This fixes `tailscale syspolicy list` failing when KeyExpirationNotice
or any other time.Duration policy setting is configured.

Fixes #16683

Signed-off-by: Nick Khyl <nickk@tailscale.com>
6 months ago
Jordan Whited a9f3fd1c67
wgengine/magicsock: fix magicsock deadlock around Conn.NoteRecvActivity (#16687)
Updates #16651
Updates tailscale/corp#30836

Signed-off-by: Jordan Whited <jordan@tailscale.com>
6 months ago
M. J. Fromberger 5ce3845a02
net/portmapper: avert a panic when a mapping is not available (#16686)
Ideally when we attempt to create a new port mapping, we should not return
without error when no mapping is available. We already log these cases as
unexpected, so this change is just to avoiding panicking dispatch on the
invalid result in those cases. We still separately need to fix the underlying
control flow.

Updates #16662

Change-Id: I51e8a116b922b49eda45e31cd27f6b89dd51abc8

Signed-off-by: M. J. Fromberger <fromberger@tailscale.com>
6 months ago
dependabot[bot] c962fefa3e
build(deps): bump form-data from 4.0.0 to 4.0.4 in /client/web (#16623)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
dependabot[bot] 4a435aedcb
.github: Bump github/codeql-action from 3.29.2 to 3.29.3 (#16615)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.29.2 to 3.29.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](181d5eefc2...d6bbdef45e)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6 months ago
Tom Proctor 61d42eb300
k8s-operator: fix test flake (#16680)
This occasionally panics waiting on a nil ctx, but was missed in the
previous PR because it's quite a rare flake as it needs to progress to a
specific point in the parser.

Updates #16678

Change-Id: Ifd36dfc915b153aede36b8ee39eff83750031f95

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
6 months ago
Tom Proctor 02084629e2
k8s-operator: handle multiple WebSocket frames per read (#16678)
When kubectl starts an interactive attach session, it sends 2 resize
messages in quick succession. It seems that particularly in HTTP mode,
we often receive both of these WebSocket frames from the underlying
connection in a single read. However, our parser currently assumes 0-1
frames per read, and leaves the second frame in the read buffer until
the next read from the underlying connection. It doesn't take long after
that before we end up failing to skip a control message as we normally
should, and then we parse a control message as though it will have a
stream ID (part of the Kubernetes protocol) and error out.

Instead, we should keep parsing frames from the read buffer for as long
as we're able to parse complete frames, so this commit refactors the
messages parsing logic into a loop based on the contents of the read
buffer being non-empty.

k/k staging/src/k8s.io/kubectl/pkg/cmd/attach/attach.go for full
details of the resize messages.

There are at least a couple more multiple-frame read edge cases we
should handle, but this commit is very conservatively fixing a single
observed issue to make it a low-risk candidate for cherry picking.

Updates #13358

Change-Id: Iafb91ad1cbeed9c5231a1525d4563164fc1f002f

Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
6 months ago
James Sanderson 5731869565 health: add an ETag to UnhealthyState for change detection
Updates tailscale/corp#30596

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
6 months ago
Tom Meadows 5154bbb0b3
k8s-operator: adding session type to cast header (#16660)
Updates #16490

Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
6 months ago
Raj Singh e300a00058
cmd/k8s-operator: Enhance DNS record handling for ProxyGroup egress services (#16181)
This update introduces support for DNS records associated with ProxyGroup egress services, ensuring that the ClusterIP Service IP is used instead of Pod IPs.

Fixes #15945

Signed-off-by: Raj Singh <raj@tailscale.com>
6 months ago
Aaron Klotz bfebf870ae cmd/tailscaled: update installSystemDaemonWindows to set the correct system service depndencies
Fixes #16658

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
6 months ago
Danni Popova c572442548
cmd/tailscale: allow SSH to IPs or DNS names without MagicDNS (#16591)
fixes #16381

Signed-off-by: Danni Popova <danni@tailscale.com>
6 months ago
Aaron Klotz 2a5d9c7269 VERSION.txt: this is v1.87.0
Signed-off-by: Aaron Klotz <aaron@tailscale.com>
6 months ago

@ -0,0 +1,54 @@
#!/usr/bin/env bash
#
# This script sets up cigocacher, but should never fail the build if unsuccessful.
# It expects to run on a GitHub-hosted runner, and connects to cigocached over a
# private Azure network that is configured at the runner group level in GitHub.
#
# Usage: ./action.sh
# Inputs:
# URL: The cigocached server URL.
# HOST: The cigocached server host to dial.
# Outputs:
# success: Whether cigocacher was set up successfully.
set -euo pipefail
if [ -z "${GITHUB_ACTIONS:-}" ]; then
echo "This script is intended to run within GitHub Actions"
exit 1
fi
if [ -z "${URL:-}" ]; then
echo "No cigocached URL is set, skipping cigocacher setup"
exit 0
fi
GOPATH=$(command -v go || true)
if [ -z "${GOPATH}" ]; then
if [ ! -f "tool/go" ]; then
echo "Go not available, unable to proceed"
exit 1
fi
GOPATH="./tool/go"
fi
BIN_PATH="${RUNNER_TEMP:-/tmp}/cigocacher$(${GOPATH} env GOEXE)"
if [ -d "cmd/cigocacher" ]; then
echo "cmd/cigocacher found locally, building from local source"
"${GOPATH}" build -o "${BIN_PATH}" ./cmd/cigocacher
else
echo "cmd/cigocacher not found locally, fetching from tailscale.com/cmd/cigocacher"
"${GOPATH}" build -o "${BIN_PATH}" tailscale.com/cmd/cigocacher
fi
CIGOCACHER_TOKEN="$("${BIN_PATH}" --auth --cigocached-url "${URL}" --cigocached-host "${HOST}" )"
if [ -z "${CIGOCACHER_TOKEN:-}" ]; then
echo "Failed to fetch cigocacher token, skipping cigocacher setup"
exit 0
fi
echo "Fetched cigocacher token successfully"
echo "::add-mask::${CIGOCACHER_TOKEN}"
echo "GOCACHEPROG=${BIN_PATH} --cache-dir ${CACHE_DIR} --cigocached-url ${URL} --cigocached-host ${HOST} --token ${CIGOCACHER_TOKEN}" >> "${GITHUB_ENV}"
echo "success=true" >> "${GITHUB_OUTPUT}"

@ -0,0 +1,35 @@
name: go-cache
description: Set up build to use cigocacher
inputs:
cigocached-url:
description: URL of the cigocached server
required: true
cigocached-host:
description: Host to dial for the cigocached server
required: true
checkout-path:
description: Path to cloned repository
required: true
cache-dir:
description: Directory to use for caching
required: true
outputs:
success:
description: Whether cigocacher was set up successfully
value: ${{ steps.setup.outputs.success }}
runs:
using: composite
steps:
- name: Setup cigocacher
id: setup
shell: bash
env:
URL: ${{ inputs.cigocached-url }}
HOST: ${{ inputs.cigocached-host }}
CACHE_DIR: ${{ inputs.cache-dir }}
working-directory: ${{ inputs.checkout-path }}
# https://github.com/orgs/community/discussions/25910
run: $GITHUB_ACTION_PATH/action.sh

@ -18,7 +18,7 @@ jobs:
runs-on: [ ubuntu-latest ] runs-on: [ ubuntu-latest ]
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build checklocks - name: Build checklocks
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks

@ -0,0 +1,73 @@
name: Build cigocacher
on:
# Released on-demand. The commit will be used as part of the tag, so generally
# prefer to release from main where the commit is stable in linear history.
workflow_dispatch:
jobs:
build:
strategy:
matrix:
GOOS: ["linux", "darwin", "windows"]
GOARCH: ["amd64", "arm64"]
runs-on: ubuntu-24.04
env:
GOOS: "${{ matrix.GOOS }}"
GOARCH: "${{ matrix.GOARCH }}"
CGO_ENABLED: "0"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build
run: |
OUT="cigocacher$(./tool/go env GOEXE)"
./tool/go build -o "${OUT}" ./cmd/cigocacher/
tar -zcf cigocacher-${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz "${OUT}"
- uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: cigocacher-${{ matrix.GOOS }}-${{ matrix.GOARCH }}
path: cigocacher-${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz
release:
runs-on: ubuntu-24.04
needs: build
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: 'cigocacher-*'
merge-multiple: true
# This step is a simplified version of actions/create-release and
# actions/upload-release-asset, which are archived and unmaintained.
- name: Create release
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const fs = require('fs');
const path = require('path');
const { data: release } = await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: `cmd/cigocacher/${{ github.sha }}`,
name: `cigocacher-${{ github.sha }}`,
draft: false,
prerelease: true,
target_commitish: `${{ github.sha }}`
});
const files = fs.readdirSync('.').filter(f => f.endsWith('.tar.gz'));
for (const file of files) {
await github.rest.repos.uploadReleaseAsset({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.id,
name: file,
data: fs.readFileSync(file)
});
console.log(`Uploaded ${file}`);
}

@ -45,7 +45,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# Install a more recent Go that understands modern go.mod content. # Install a more recent Go that understands modern go.mod content.
- name: Install Go - name: Install Go
@ -55,7 +55,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/init@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -66,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/autobuild@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -80,4 +80,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 uses: github/codeql-action/analyze@76621b61decf072c1cee8dd1ce2d2a82d33c17ed # v3.29.5

@ -0,0 +1,29 @@
name: "Validate Docker base image"
on:
workflow_dispatch:
pull_request:
paths:
- "Dockerfile.base"
- ".github/workflows/docker-base.yml"
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: "build and test"
run: |
set -e
IMG="test-base:$(head -c 8 /dev/urandom | xxd -p)"
docker build -t "$IMG" -f Dockerfile.base .
iptables_version=$(docker run --rm "$IMG" iptables --version)
if [[ "$iptables_version" != *"(legacy)"* ]]; then
echo "ERROR: Docker base image should contain legacy iptables; found ${iptables_version}"
exit 1
fi
ip6tables_version=$(docker run --rm "$IMG" ip6tables --version)
if [[ "$ip6tables_version" != *"(legacy)"* ]]; then
echo "ERROR: Docker base image should contain legacy ip6tables; found ${ip6tables_version}"
exit 1
fi

@ -4,12 +4,10 @@ on:
branches: branches:
- main - main
pull_request: pull_request:
branches:
- "*"
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: "Build Docker image" - name: "Build Docker image"
run: docker build . run: docker build .

@ -17,11 +17,11 @@ jobs:
id-token: "write" id-token: "write"
contents: "read" contents: "read"
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}" ref: "${{ (inputs.tag != null) && format('refs/tags/{0}', inputs.tag) || '' }}"
- uses: "DeterminateSystems/nix-installer-action@main" - uses: DeterminateSystems/nix-installer-action@786fff0690178f1234e4e1fe9b536e94f5433196 # v20
- uses: "DeterminateSystems/flakehub-push@main" - uses: DeterminateSystems/flakehub-push@71f57208810a5d299fc6545350981de98fdbc860 # v6
with: with:
visibility: "public" visibility: "public"
tag: "${{ inputs.tag }}" tag: "${{ inputs.tag }}"

@ -2,7 +2,11 @@ name: golangci-lint
on: on:
# For now, only lint pull requests, not the main branches. # For now, only lint pull requests, not the main branches.
pull_request: pull_request:
paths:
- ".github/workflows/golangci-lint.yml"
- "**.go"
- "go.mod"
- "go.sum"
# TODO(andrew): enable for main branch after an initial waiting period. # TODO(andrew): enable for main branch after an initial waiting period.
#push: #push:
# branches: # branches:
@ -23,17 +27,21 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 - uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with: with:
go-version-file: go.mod go-version-file: go.mod
cache: false cache: true
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0 uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with: with:
version: v2.0.2 version: v2.4.0
# Show only new issues if it's a pull request. # Show only new issues if it's a pull request.
only-new-issues: true only-new-issues: true
# Loading packages with a cold cache takes a while:
args: --timeout=10m

@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Check out code into the Go module directory - name: Check out code into the Go module directory
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install govulncheck - name: Install govulncheck
run: ./tool/go install golang.org/x/vuln/cmd/govulncheck@latest run: ./tool/go install golang.org/x/vuln/cmd/govulncheck@latest

@ -10,8 +10,6 @@ on:
- scripts/installer.sh - scripts/installer.sh
- .github/workflows/installer.yml - .github/workflows/installer.yml
pull_request: pull_request:
branches:
- "*"
paths: paths:
- scripts/installer.sh - scripts/installer.sh
- .github/workflows/installer.yml - .github/workflows/installer.yml
@ -60,6 +58,14 @@ jobs:
# Check a few images with wget rather than curl. # Check a few images with wget rather than curl.
- { image: "debian:oldstable-slim", deps: "wget" } - { image: "debian:oldstable-slim", deps: "wget" }
- { image: "debian:sid-slim", deps: "wget" } - { image: "debian:sid-slim", deps: "wget" }
- { image: "debian:stable-slim", deps: "curl" }
- { image: "ubuntu:24.04", deps: "curl" }
- { image: "fedora:latest", deps: "curl" }
# Test TAILSCALE_VERSION pinning on a subset of distros.
# Skip Alpine as community repos don't reliably keep old versions.
- { image: "debian:stable-slim", deps: "curl", version: "1.80.0" }
- { image: "ubuntu:24.04", deps: "curl", version: "1.80.0" }
- { image: "fedora:latest", deps: "curl", version: "1.80.0" }
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: ${{ matrix.image }} image: ${{ matrix.image }}
@ -93,15 +99,21 @@ jobs:
contains(matrix.image, 'parrotsec') || contains(matrix.image, 'parrotsec') ||
contains(matrix.image, 'kalilinux') contains(matrix.image, 'kalilinux')
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: run installer - name: run installer
run: scripts/installer.sh run: scripts/installer.sh
env:
TAILSCALE_VERSION: ${{ matrix.version }}
# Package installation can fail in docker because systemd is not running # Package installation can fail in docker because systemd is not running
# as PID 1, so ignore errors at this step. The real check is the # as PID 1, so ignore errors at this step. The real check is the
# `tailscale --version` command below. # `tailscale --version` command below.
continue-on-error: true continue-on-error: true
- name: check tailscale version - name: check tailscale version
run: tailscale --version run: |
tailscale --version
if [ -n "${{ matrix.version }}" ]; then
tailscale --version | grep -q "^${{ matrix.version }}" || { echo "Version mismatch!"; exit 1; }
fi
notify-slack: notify-slack:
needs: test needs: test
runs-on: ubuntu-latest runs-on: ubuntu-latest

@ -17,7 +17,7 @@ jobs:
runs-on: [ ubuntu-latest ] runs-on: [ ubuntu-latest ]
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Build and lint Helm chart - name: Build and lint Helm chart
run: | run: |
eval `./tool/go run ./cmd/mkversion` eval `./tool/go run ./cmd/mkversion`

@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install qemu - name: Install qemu
run: | run: |
sudo rm /var/lib/man-db/auto-update sudo rm /var/lib/man-db/auto-update

@ -0,0 +1,29 @@
# Pin images used in github actions to a hash instead of a version tag.
name: pin-github-actions
on:
pull_request:
branches:
- main
paths:
- ".github/workflows/**"
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
run:
name: pin-github-actions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: pin
run: make pin-github-actions
- name: check for changed workflow files
run: git diff --no-ext-diff --exit-code .github/workflows || (echo "Some github actions versions need pinning, run make pin-github-actions."; exit 1)

@ -2,20 +2,21 @@ name: request-dataplane-review
on: on:
pull_request: pull_request:
branches: types: [ opened, synchronize, reopened, ready_for_review ]
- "*"
paths: paths:
- ".github/workflows/request-dataplane-review.yml" - ".github/workflows/request-dataplane-review.yml"
- "**/*derp*" - "**/*derp*"
- "**/derp*/**" - "**/derp*/**"
- "!**/depaware.txt"
jobs: jobs:
request-dataplane-review: request-dataplane-review:
if: github.event.pull_request.draft == false
name: Request Dataplane Review name: Request Dataplane Review
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Get access token - name: Get access token
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6 uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
id: generate-token id: generate-token

@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run SSH integration tests - name: Run SSH integration tests
run: | run: |
make sshintegrationtest make sshintegrationtest

@ -48,7 +48,7 @@ jobs:
cache-key: ${{ steps.hash.outputs.key }} cache-key: ${{ steps.hash.outputs.key }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Compute cache key from go.{mod,sum} - name: Compute cache key from go.{mod,sum}
@ -57,7 +57,7 @@ jobs:
# See if the cache entry already exists to avoid downloading it # See if the cache entry already exists to avoid downloading it
# and doing the cache write again. # and doing the cache write again.
- id: check-cache - id: check-cache
uses: actions/cache/restore@v4 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4
with: with:
path: gomodcache # relative to workspace; see env note at top of file path: gomodcache # relative to workspace; see env note at top of file
key: ${{ steps.hash.outputs.key }} key: ${{ steps.hash.outputs.key }}
@ -69,7 +69,7 @@ jobs:
run: go mod download run: go mod download
- name: Cache Go modules - name: Cache Go modules
if: steps.check-cache.outputs.cache-hit != 'true' if: steps.check-cache.outputs.cache-hit != 'true'
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache # relative to workspace; see env note at top of file path: gomodcache # relative to workspace; see env note at top of file
key: ${{ steps.hash.outputs.key }} key: ${{ steps.hash.outputs.key }}
@ -88,11 +88,11 @@ jobs:
- shard: '4/4' - shard: '4/4'
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -126,31 +126,30 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true enableCrossOsArchive: true
- name: Restore Cache - name: Restore Cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: restore-cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
# Note: unlike the other setups, this is only grabbing the mod download # Note: this is only restoring the build cache. Mod cache is shared amongst
# cache, rather than the whole mod directory, as the download cache # all jobs in the workflow.
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: | path: |
~/.cache/go-build ~/.cache/go-build
~\AppData\Local\go-build ~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be key: ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: | restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }} ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2- ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-
${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-
- name: build all - name: build all
if: matrix.buildflags == '' # skip on race builder if: matrix.buildflags == '' # skip on race builder
working-directory: src working-directory: src
@ -177,7 +176,7 @@ jobs:
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all - name: test all
working-directory: src working-directory: src
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} run: NOBASHDEBUG=true NOPWSHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
env: env:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
TS_TEST_SHARD: ${{ matrix.shard }} TS_TEST_SHARD: ${{ matrix.shard }}
@ -206,12 +205,26 @@ jobs:
shell: bash shell: bash
run: | run: |
find $(go env GOCACHE) -type f -mmin +90 -delete find $(go env GOCACHE) -type f -mmin +90 -delete
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
path: |
~/.cache/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-${{ matrix.shard }}-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
windows: windows:
# windows-8vpu is a 2022 GitHub-managed runner in our permissions:
# org with 8 cores and 32 GB of RAM: id-token: write # This is required for requesting the GitHub action identity JWT that can auth to cigocached
# https://github.com/organizations/tailscale/settings/actions/github-hosted-runners/1 contents: read # This is required for actions/checkout
runs-on: windows-8vcpu # ci-windows-github-1 is a 2022 GitHub-managed runner in our org with 8 cores
# and 32 GB of RAM. It is connected to a private Azure VNet that hosts cigocached.
# https://github.com/organizations/tailscale/settings/actions/github-hosted-runners/5
runs-on: ci-windows-github-1
needs: gomod-cache needs: gomod-cache
name: Windows (${{ matrix.name || matrix.shard}}) name: Windows (${{ matrix.name || matrix.shard}})
strategy: strategy:
@ -220,54 +233,40 @@ jobs:
include: include:
- key: "win-bench" - key: "win-bench"
name: "benchmarks" name: "benchmarks"
- key: "win-tool-go"
name: "./tool/go"
- key: "win-shard-1-2" - key: "win-shard-1-2"
shard: "1/2" shard: "1/2"
- key: "win-shard-2-2" - key: "win-shard-2-2"
shard: "2/2" shard: "2/2"
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: ${{ github.workspace }}/src
- name: Install Go - name: Install Go
if: matrix.key != 'win-tool-go'
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with: with:
go-version-file: src/go.mod go-version-file: ${{ github.workspace }}/src/go.mod
cache: false cache: false
- name: Restore Go module cache - name: Restore Go module cache
if: matrix.key != 'win-tool-go' uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true enableCrossOsArchive: true
- name: Restore Cache - name: Set up cigocacher
if: matrix.key != 'win-tool-go' id: cigocacher-setup
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: ./src/.github/actions/go-cache
with: with:
path: | checkout-path: ${{ github.workspace }}/src
~/.cache/go-build cache-dir: ${{ github.workspace }}/cigocacher
~\AppData\Local\go-build cigocached-url: ${{ vars.CIGOCACHED_AZURE_URL }}
# The -2- here should be incremented when the scheme of data to be cigocached-host: ${{ vars.CIGOCACHED_AZURE_HOST }}
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ matrix.key }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ matrix.key }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ matrix.key }}-go-2-
- name: test-tool-go
if: matrix.key == 'win-tool-go'
working-directory: src
run: ./tool/go version
- name: test - name: test
if: matrix.key != 'win-bench' && matrix.key != 'win-tool-go' # skip on bench builder if: matrix.key != 'win-bench' # skip on bench builder
working-directory: src working-directory: src
run: go run ./cmd/testwrapper sharded:${{ matrix.shard }} run: go run ./cmd/testwrapper sharded:${{ matrix.shard }}
@ -279,12 +278,26 @@ jobs:
# the equals signs cause great confusion. # the equals signs cause great confusion.
run: go test ./... -bench . -benchtime 1x -run "^$" run: go test ./... -bench . -benchtime 1x -run "^$"
- name: Tidy cache - name: Print stats
if: matrix.key != 'win-tool-go' shell: pwsh
working-directory: src if: steps.cigocacher-setup.outputs.success == 'true'
shell: bash env:
GOCACHEPROG: ${{ env.GOCACHEPROG }}
run: | run: |
find $(go env GOCACHE) -type f -mmin +90 -delete Invoke-Expression "$env:GOCACHEPROG --stats" | jq .
win-tool-go:
runs-on: windows-latest
needs: gomod-cache
name: Windows (win-tool-go)
steps:
- name: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
path: src
- name: test-tool-go
working-directory: src
run: ./tool/go version
privileged: privileged:
needs: gomod-cache needs: gomod-cache
@ -294,11 +307,11 @@ jobs:
options: --privileged options: --privileged
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -317,18 +330,18 @@ jobs:
if: github.repository == 'tailscale/tailscale' if: github.repository == 'tailscale/tailscale'
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true enableCrossOsArchive: true
- name: Run VM tests - name: Run VM tests
working-directory: src working-directory: src
run: ./tool/go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004 run: ./tool/go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2404
env: env:
HOME: "/var/lib/ghrunner/home" HOME: "/var/lib/ghrunner/home"
TMPDIR: "/tmp" TMPDIR: "/tmp"
@ -373,31 +386,29 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: Restore Cache - name: Restore Cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: restore-cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
# Note: unlike the other setups, this is only grabbing the mod download # Note: this is only restoring the build cache. Mod cache is shared amongst
# cache, rather than the whole mod directory, as the download cache # all jobs in the workflow.
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: | path: |
~/.cache/go-build ~/.cache/go-build
~\AppData\Local\go-build ~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: | restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }} ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2- ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-
- name: Restore Go module cache ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: build all - name: build all
working-directory: src working-directory: src
run: ./tool/go build ./cmd/... run: ./tool/go build ./cmd/...
@ -418,6 +429,17 @@ jobs:
shell: bash shell: bash
run: | run: |
find $(go env GOCACHE) -type f -mmin +90 -delete find $(go env GOCACHE) -type f -mmin +90 -delete
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
path: |
~/.cache/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-${{ matrix.goarm }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
ios: # similar to cross above, but iOS can't build most of the repo. So, just ios: # similar to cross above, but iOS can't build most of the repo. So, just
# make it build a few smoke packages. # make it build a few smoke packages.
@ -425,11 +447,11 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -463,31 +485,29 @@ jobs:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: Restore Cache - name: Restore Cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: restore-cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
# Note: unlike the other setups, this is only grabbing the mod download # Note: this is only restoring the build cache. Mod cache is shared amongst
# cache, rather than the whole mod directory, as the download cache # all jobs in the workflow.
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: | path: |
~/.cache/go-build ~/.cache/go-build
~\AppData\Local\go-build ~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: | restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }} ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2- ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-
- name: Restore Go module cache ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: build core - name: build core
working-directory: src working-directory: src
run: ./tool/go build ./cmd/tailscale ./cmd/tailscaled run: ./tool/go build ./cmd/tailscale ./cmd/tailscaled
@ -501,6 +521,17 @@ jobs:
shell: bash shell: bash
run: | run: |
find $(go env GOCACHE) -type f -mmin +90 -delete find $(go env GOCACHE) -type f -mmin +90 -delete
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
path: |
~/.cache/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
android: android:
# similar to cross above, but android fails to build a few pieces of the # similar to cross above, but android fails to build a few pieces of the
@ -510,7 +541,7 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed # Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
@ -518,7 +549,7 @@ jobs:
# some Android breakages early. # some Android breakages early.
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482 # TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -535,31 +566,29 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: Restore Cache - name: Restore Cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: restore-cache
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
# Note: unlike the other setups, this is only grabbing the mod download # Note: this is only restoring the build cache. Mod cache is shared amongst
# cache, rather than the whole mod directory, as the download cache # all jobs in the workflow.
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: | path: |
~/.cache/go-build ~/.cache/go-build
~\AppData\Local\go-build ~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be key: ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: | restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }} ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-
${{ github.job }}-${{ runner.os }}-go-2- ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-
- name: Restore Go module cache ${{ runner.os }}-js-wasm-go-
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }}
enableCrossOsArchive: true
- name: build tsconnect client - name: build tsconnect client
working-directory: src working-directory: src
run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
@ -578,17 +607,28 @@ jobs:
shell: bash shell: bash
run: | run: |
find $(go env GOCACHE) -type f -mmin +90 -delete find $(go env GOCACHE) -type f -mmin +90 -delete
- name: Save Cache
# Save cache even on failure, but only on cache miss and main branch to avoid thrashing.
if: always() && steps.restore-cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/main'
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with:
# Note: this is only saving the build cache. Mod cache is shared amongst
# all jobs in the workflow.
path: |
~/.cache/go-build
~\AppData\Local\go-build
key: ${{ runner.os }}-js-wasm-go-${{ hashFiles('**/go.sum') }}-${{ github.job }}-${{ github.run_id }}
tailscale_go: # Subset of tests that depend on our custom Go toolchain. tailscale_go: # Subset of tests that depend on our custom Go toolchain.
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set GOMODCACHE env - name: Set GOMODCACHE env
run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -613,7 +653,9 @@ jobs:
steps: steps:
- name: build fuzzers - name: build fuzzers
id: build id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master # As of 21 October 2025, this repo doesn't tag releases, so this commit
# hash is just the tip of master.
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
# continue-on-error makes steps.build.conclusion be 'success' even if # continue-on-error makes steps.build.conclusion be 'success' even if
# steps.build.outcome is 'failure'. This means this step does not # steps.build.outcome is 'failure'. This means this step does not
# contribute to the job's overall pass/fail evaluation. # contribute to the job's overall pass/fail evaluation.
@ -643,7 +685,9 @@ jobs:
# report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong # report a failure because TS_FUZZ_CURRENTLY_BROKEN is set to the wrong
# value. # value.
if: steps.build.outcome == 'success' if: steps.build.outcome == 'success'
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master # As of 21 October 2025, this repo doesn't tag releases, so this commit
# hash is just the tip of master.
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@1242ccb5b6352601e73c00f189ac2ae397242264
with: with:
oss-fuzz-project-name: 'tailscale' oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 150 fuzz-seconds: 150
@ -665,13 +709,13 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Set GOMODCACHE env - name: Set GOMODCACHE env
run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV run: echo "GOMODCACHE=$HOME/.cache/go-mod" >> $GITHUB_ENV
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -685,11 +729,11 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -699,6 +743,7 @@ jobs:
run: | run: |
pkgs=$(./tool/go list ./... | grep -Ev 'dnsfallback|k8s-operator|xdp') pkgs=$(./tool/go list ./... | grep -Ev 'dnsfallback|k8s-operator|xdp')
./tool/go generate $pkgs ./tool/go generate $pkgs
git add -N . # ensure untracked files are noticed
echo echo
echo echo
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1) git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
@ -708,11 +753,11 @@ jobs:
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -720,21 +765,21 @@ jobs:
- name: check that 'go mod tidy' is clean - name: check that 'go mod tidy' is clean
working-directory: src working-directory: src
run: | run: |
./tool/go mod tidy make tidy
echo echo
echo echo
git diff --name-only --exit-code || (echo "Please run 'go mod tidy'."; exit 1) git diff --name-only --exit-code || (echo "Please run 'make tidy'"; exit 1)
licenses: licenses:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: gomod-cache needs: gomod-cache
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}
@ -784,11 +829,11 @@ jobs:
steps: steps:
- name: checkout - name: checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with: with:
path: src path: src
- name: Restore Go module cache - name: Restore Go module cache
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
with: with:
path: gomodcache path: gomodcache
key: ${{ needs.gomod-cache.outputs.cache-key }} key: ${{ needs.gomod-cache.outputs.cache-key }}

@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run update-flakes - name: Run update-flakes
run: ./update-flake.sh run: ./update-flake.sh
@ -35,7 +35,7 @@ jobs:
private-key: ${{ secrets.CODE_UPDATER_APP_PRIVATE_KEY }} private-key: ${{ secrets.CODE_UPDATER_APP_PRIVATE_KEY }}
- name: Send pull request - name: Send pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
author: Flakes Updater <noreply+flakes-updater@tailscale.com> author: Flakes Updater <noreply+flakes-updater@tailscale.com>

@ -14,7 +14,7 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run go get - name: Run go get
run: | run: |
@ -32,7 +32,7 @@ jobs:
- name: Send pull request - name: Send pull request
id: pull-request id: pull-request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e #v7.0.8 uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 #v8.0.0
with: with:
token: ${{ steps.generate-token.outputs.token }} token: ${{ steps.generate-token.outputs.token }}
author: OSS Updater <noreply+oss-updater@tailscale.com> author: OSS Updater <noreply+oss-updater@tailscale.com>

@ -0,0 +1,38 @@
name: tailscale.com/cmd/vet
env:
HOME: ${{ github.workspace }}
# GOMODCACHE is the same definition on all OSes. Within the workspace, we use
# toplevel directories "src" (for the checked out source code), and "gomodcache"
# and other caches as siblings to follow.
GOMODCACHE: ${{ github.workspace }}/gomodcache
on:
push:
branches:
- main
- "release-branch/*"
paths:
- "**.go"
pull_request:
paths:
- "**.go"
jobs:
vet:
runs-on: [ self-hosted, linux ]
timeout-minutes: 5
steps:
- name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
path: src
- name: Build 'go vet' tool
working-directory: src
run: ./tool/go build -o /tmp/vettool tailscale.com/cmd/vet
- name: Run 'go vet'
working-directory: src
run: ./tool/go vet -vettool=/tmp/vettool tailscale.com/...

@ -3,8 +3,6 @@ on:
workflow_dispatch: workflow_dispatch:
# For now, only run on requests, not the main branches. # For now, only run on requests, not the main branches.
pull_request: pull_request:
branches:
- "*"
paths: paths:
- "client/web/**" - "client/web/**"
- ".github/workflows/webclient.yml" - ".github/workflows/webclient.yml"
@ -24,7 +22,7 @@ jobs:
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install deps - name: Install deps
run: ./tool/yarn --cwd client/web run: ./tool/yarn --cwd client/web
- name: Run lint - name: Run lint

3
.gitignore vendored

@ -52,3 +52,6 @@ client/web/build/assets
# Ignore personal IntelliJ settings # Ignore personal IntelliJ settings
.idea/ .idea/
# Ignore syncthing state directory.
/.stfolder

@ -0,0 +1 @@
.gitignore

@ -1 +1 @@
3.19 3.22

@ -1,17 +0,0 @@
# This is the official list of Tailscale
# authors for copyright purposes.
#
# Names should be added to this file as one of
# Organization's name
# Individual's name <submission email address>
# Individual's name <submission email address> <email2> <emailN>
#
# Please keep the list sorted.
#
# You do not need to add entries to this list, and we don't actively
# populate this list. If you do want to be acknowledged explicitly as
# a copyright holder, though, then please send a PR referencing your
# earlier contributions and clarifying whether it's you or your
# company that owns the rights to your contribution.
Tailscale Inc.

@ -1,135 +1,103 @@
# Contributor Covenant Code of Conduct # Tailscale Community Code of Conduct
## Our Pledge ## Our Pledge
We as members, contributors, and leaders pledge to make participation We are committed to creating an open, welcoming, diverse, inclusive, healthy and respectful community.
in our community a harassment-free experience for everyone, regardless Unacceptable, harmful and inappropriate behavior will not be tolerated.
of age, body size, visible or invisible disability, ethnicity, sex
characteristics, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open,
welcoming, diverse, inclusive, and healthy community.
## Our Standards ## Our Standards
Examples of behavior that contributes to a positive environment for Examples of behavior that contributes to a positive environment for our community include:
our community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our
mistakes, and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or - Demonstrating empathy and kindness toward other people.
advances of any kind - Being respectful of differing opinions, viewpoints, and experiences.
* Trolling, insulting or derogatory comments, and personal or - Giving and gracefully accepting constructive feedback.
political attacks - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience.
* Public or private harassment - Focusing on what is best not just for us as individuals, but for the overall community.
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in
a professional setting
## Enforcement Responsibilities Examples of unacceptable behavior include without limitation:
Community leaders are responsible for clarifying and enforcing our - The use of language, imagery or emojis (collectively "content") that is racist, sexist, homophobic, transphobic, or otherwise harassing or discriminatory based on any protected characteristic.
standards of acceptable behavior and will take appropriate and fair - The use of sexualized content and sexual attention or advances of any kind.
corrective action in response to any behavior that they deem - The use of violent, intimidating or bullying content.
inappropriate, threatening, offensive, or harmful. - Trolling, concern trolling, insulting or derogatory comments, and personal or political attacks.
- Public or private harassment.
- Publishing others' personal information, such as a photo, physical address, email address, online profile information, or other personal information, without their explicit permission or with the intent to bully or harass the other person.
- Posting deep fake or other AI generated content about or involving another person without the explicit permission.
- Spamming community channels and members, such as sending repeat messages, low-effort content, or automated messages.
- Phishing or any similar activity.
- Distributing or promoting malware.
- The use of any coded or suggestive content to hide or provoke otherwise unacceptable behavior.
- Other conduct which could reasonably be considered harmful, illegal, or inappropriate in a professional setting.
Community leaders have the right and responsibility to remove, edit, Please also see the Tailscale Acceptable Use Policy, available at [tailscale.com/tailscale-aup](https://tailscale.com/tailscale-aup).
or reject comments, commits, code, wiki edits, issues, and other
contributions that are not aligned to this Code of Conduct, and will
communicate reasons for moderation decisions when appropriate.
## Scope ## Reporting Incidents
This Code of Conduct applies within all community spaces, and also Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to Tailscale directly via <info@tailscale.com>, or to the community leaders or moderators via DM or similar.
applies when an individual is officially representing the community in All complaints will be reviewed and investigated promptly and fairly.
public spaces. Examples of representing our community include using an We will respect the privacy and safety of the reporter of any issues.
official e-mail address, posting via an official social media account,
or acting as an appointed representative at an online or offline
event.
## Enforcement Please note that this community is not moderated by staff 24/7, and we do not have, and do not undertake, any obligation to prescreen, monitor, edit, or remove any content or data, or to actively seek facts or circumstances indicating illegal activity.
While we strive to keep the community safe and welcoming, moderation may not be immediate at all hours.
If you encounter any issues, report them using the appropriate channels.
Instances of abusive, harassing, or otherwise unacceptable behavior ## Enforcement Guidelines
may be reported to the community leaders responsible for enforcement
at [info@tailscale.com](mailto:info@tailscale.com). All complaints
will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and Community leaders and moderators are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
security of the reporter of any incident.
## Enforcement Guidelines Community leaders and moderators have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Community Code of Conduct.
Tailscale retains full discretion to take action (or not) in response to a violation of these guidelines with or without notice or liability to you.
We will interpret our policies and resolve disputes in favor of protecting users, customers, the public, our community and our company, as a whole.
Community leaders will follow these Community Impact Guidelines in Community leaders will follow these community enforcement guidelines in determining the consequences for any action they deem in violation of this Code of Conduct,
determining the consequences for any action they deem in violation of and retain full discretion to apply the enforcement guidelines as necessary depending on the circumstances:
this Code of Conduct:
### 1. Correction ### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
deemed unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate.
providing clarity around the nature of the violation and an A public apology may be requested.
explanation of why the behavior was inappropriate. A public apology
may be requested.
### 2. Warning ### 2. Warning
**Community Impact**: A violation through a single incident or series Community Impact: A violation through a single incident or series of actions.
of actions.
**Consequence**: A warning with consequences for continued Consequence: A warning with consequences for continued behavior.
behavior. No interaction with the people involved, including No interaction with the people involved, including unsolicited interaction with those enforcing this Community Code of Conduct, for a specified period of time.
unsolicited interaction with those enforcing the Code of Conduct, for This includes avoiding interactions in community spaces as well as external channels like social media.
a specified period of time. This includes avoiding interactions in Violating these terms may lead to a temporary or permanent ban.
community spaces as well as external channels like social
media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban ### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
public communication with the community for a specified period of No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
time. No public or private interaction with the people involved,
including unsolicited interaction with those enforcing the Code of
Conduct, is allowed during this period. Violating these terms may lead
to a permanent ban.
### 4. Permanent Ban ### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
community standards, including sustained inappropriate behavior,
harassment of an individual, or aggression toward or disparagement of
classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction Consequence: A permanent ban from any sort of public interaction within the community.
within the community.
## Acceptable Use Policy
Violation of this Community Code of Conduct may also violate the Tailscale Acceptable Use Policy, which may result in suspension or termination of your Tailscale account.
For more information, please see the Tailscale Acceptable Use Policy, available at [tailscale.com/tailscale-aup](https://tailscale.com/tailscale-aup).
## Privacy
Please see the Tailscale [Privacy Policy](https://tailscale.com/privacy-policy) for more information about how Tailscale collects, uses, discloses and protects information.
## Attribution ## Attribution
This Code of Conduct is adapted from the [Contributor This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at <https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.
Covenant][homepage], version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
conduct enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org [homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the For answers to common questions about this code of conduct, see the FAQ at <https://www.contributor-covenant.org/faq>.
FAQ at https://www.contributor-covenant.org/faq. Translations are Translations are available at <https://www.contributor-covenant.org/translations>.
available at https://www.contributor-covenant.org/translations.

@ -1,4 +1,4 @@
# Copyright (c) Tailscale Inc & AUTHORS # Copyright (c) Tailscale Inc & contributors
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
# Note that this Dockerfile is currently NOT used to build any of the published # Note that this Dockerfile is currently NOT used to build any of the published
@ -36,7 +36,7 @@
# $ docker exec tailscaled tailscale status # $ docker exec tailscaled tailscale status
FROM golang:1.24-alpine AS build-env FROM golang:1.25-alpine AS build-env
WORKDIR /go/src/tailscale WORKDIR /go/src/tailscale
@ -71,10 +71,15 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \ -X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot -v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.19 FROM alpine:3.22
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables # Alpine 3.19 replaced legacy iptables with nftables based implementation.
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables # Tailscale is used on some hosts that don't support nftables, such as Synology
# NAS, so link iptables back to legacy version. Hosts that don't require legacy
# iptables should be able to use Tailscale in nftables mode. See
# https://github.com/tailscale/tailscale/issues/17854
RUN rm /usr/sbin/iptables && ln -s /usr/sbin/iptables-legacy /usr/sbin/iptables
RUN rm /usr/sbin/ip6tables && ln -s /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/ COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be # For compat with the previous run.sh, although ideally you should be

@ -1,12 +1,12 @@
# Copyright (c) Tailscale Inc & AUTHORS # Copyright (c) Tailscale Inc & contributors
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.19 FROM alpine:3.22
RUN apk add --no-cache ca-certificates iptables iptables-legacy iproute2 ip6tables iputils RUN apk add --no-cache ca-certificates iptables iptables-legacy iproute2 ip6tables iputils
# Alpine 3.19 replaces legacy iptables with nftables based implementation. We # Alpine 3.19 replaced legacy iptables with nftables based implementation.
# can't be certain that all hosts that run Tailscale containers currently # Tailscale is used on some hosts that don't support nftables, such as Synology
# suppport nftables, so link back to legacy for backwards compatibility reasons. # NAS, so link iptables back to legacy version. Hosts that don't require legacy
# TODO(irbekrm): add some way how to determine if we still run on nodes that # iptables should be able to use Tailscale in nftables mode. See
# don't support nftables, so that we can eventually remove these symlinks. # https://github.com/tailscale/tailscale/issues/17854
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables RUN rm /usr/sbin/iptables && ln -s /usr/sbin/iptables-legacy /usr/sbin/iptables
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables RUN rm /usr/sbin/ip6tables && ln -s /usr/sbin/ip6tables-legacy /usr/sbin/ip6tables

@ -1,6 +1,6 @@
BSD 3-Clause License BSD 3-Clause License
Copyright (c) 2020 Tailscale Inc & AUTHORS. Copyright (c) 2020 Tailscale Inc & contributors.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met: modification, are permitted provided that the following conditions are met:

@ -8,8 +8,9 @@ PLATFORM ?= "flyio" ## flyio==linux/amd64. Set to "" to build all platforms.
vet: ## Run go vet vet: ## Run go vet
./tool/go vet ./... ./tool/go vet ./...
tidy: ## Run go mod tidy tidy: ## Run go mod tidy and update nix flake hashes
./tool/go mod tidy ./tool/go mod tidy
./update-flake.sh
lint: ## Run golangci-lint lint: ## Run golangci-lint
./tool/go run github.com/golangci/golangci-lint/cmd/golangci-lint run ./tool/go run github.com/golangci/golangci-lint/cmd/golangci-lint run
@ -17,28 +18,36 @@ lint: ## Run golangci-lint
updatedeps: ## Update depaware deps updatedeps: ## Update depaware deps
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go" # depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one. # it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --internal \ PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --vendor --internal \
tailscale.com/cmd/tailscaled \ tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \ tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper \ tailscale.com/cmd/derper \
tailscale.com/cmd/k8s-operator \ tailscale.com/cmd/k8s-operator \
tailscale.com/cmd/stund \ tailscale.com/cmd/stund \
tailscale.com/cmd/tsidp tailscale.com/cmd/tsidp
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update -goos=linux,darwin,windows,android,ios --internal \ PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --goos=linux,darwin,windows,android,ios --vendor --internal \
tailscale.com/tsnet tailscale.com/tsnet
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --file=depaware-minbox.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min --add=cli)" --vendor --internal \
tailscale.com/cmd/tailscaled
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --file=depaware-min.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min)" --vendor --internal \
tailscale.com/cmd/tailscaled
depaware: ## Run depaware checks depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go" # depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one. # it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --internal \ PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --vendor --internal \
tailscale.com/cmd/tailscaled \ tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \ tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper \ tailscale.com/cmd/derper \
tailscale.com/cmd/k8s-operator \ tailscale.com/cmd/k8s-operator \
tailscale.com/cmd/stund \ tailscale.com/cmd/stund \
tailscale.com/cmd/tsidp tailscale.com/cmd/tsidp
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --goos=linux,darwin,windows,android,ios --internal \ PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --goos=linux,darwin,windows,android,ios --vendor --internal \
tailscale.com/tsnet tailscale.com/tsnet
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --file=depaware-minbox.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min --add=cli)" --vendor --internal \
tailscale.com/cmd/tailscaled
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --file=depaware-min.txt --goos=linux --tags="$$(./tool/go run ./cmd/featuretags --min)" --vendor --internal \
tailscale.com/cmd/tailscaled
buildwindows: ## Build tailscale CLI for windows/amd64 buildwindows: ## Build tailscale CLI for windows/amd64
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
@ -133,9 +142,19 @@ sshintegrationtest: ## Run the SSH integration tests in various Docker container
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \ echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \
echo "Testing on alpine:latest" && docker build --build-arg="BASE=alpine:latest" -t ssh-alpine-latest ssh/tailssh/testcontainers echo "Testing on alpine:latest" && docker build --build-arg="BASE=alpine:latest" -t ssh-alpine-latest ssh/tailssh/testcontainers
.PHONY: generate
generate: ## Generate code
./tool/go generate ./...
.PHONY: pin-github-actions
pin-github-actions:
./tool/go tool github.com/stacklok/frizbee actions .github/workflows
help: ## Show this help help: ## Show this help
@echo "\nSpecify a command. The choices are:\n" @echo ""
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}' @echo "Specify a command. The choices are:"
@echo ""
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
@echo "" @echo ""
.PHONY: help .PHONY: help

@ -37,7 +37,7 @@ not open source.
## Building ## Building
We always require the latest Go release, currently Go 1.23. (While we build We always require the latest Go release, currently Go 1.25. (While we build
releases with our [Go fork](https://github.com/tailscale/go/), its use is not releases with our [Go fork](https://github.com/tailscale/go/), its use is not
required.) required.)

@ -1 +1 @@
1.85.0 1.95.0

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package appc implements App Connectors. // Package appc implements App Connectors.
@ -12,19 +12,20 @@ package appc
import ( import (
"context" "context"
"fmt" "fmt"
"maps"
"net/netip" "net/netip"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"golang.org/x/net/dns/dnsmessage" "tailscale.com/syncs"
"tailscale.com/types/appctype"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
"tailscale.com/util/execqueue" "tailscale.com/util/execqueue"
"tailscale.com/util/mak"
"tailscale.com/util/slicesx" "tailscale.com/util/slicesx"
) )
@ -115,19 +116,6 @@ func metricStoreRoutes(rate, nRoutes int64) {
recordMetric(nRoutes, metricStoreRoutesNBuckets, metricStoreRoutesN) recordMetric(nRoutes, metricStoreRoutesNBuckets, metricStoreRoutesN)
} }
// RouteInfo is a data structure used to persist the in memory state of an AppConnector
// so that we can know, even after a restart, which routes came from ACLs and which were
// learned from domains.
type RouteInfo struct {
// Control is the routes from the 'routes' section of an app connector acl.
Control []netip.Prefix `json:",omitempty"`
// Domains are the routes discovered by observing DNS lookups for configured domains.
Domains map[string][]netip.Addr `json:",omitempty"`
// Wildcards are the configured DNS lookup domains to observe. When a DNS query matches Wildcards,
// its result is added to Domains.
Wildcards []string `json:",omitempty"`
}
// AppConnector is an implementation of an AppConnector that performs // AppConnector is an implementation of an AppConnector that performs
// its function as a subsystem inside of a tailscale node. At the control plane // its function as a subsystem inside of a tailscale node. At the control plane
// side App Connector routing is configured in terms of domains rather than IP // side App Connector routing is configured in terms of domains rather than IP
@ -138,14 +126,20 @@ type RouteInfo struct {
// routes not yet served by the AppConnector the local node configuration is // routes not yet served by the AppConnector the local node configuration is
// updated to advertise the new route. // updated to advertise the new route.
type AppConnector struct { type AppConnector struct {
// These fields are immutable after initialization.
logf logger.Logf logf logger.Logf
eventBus *eventbus.Bus
routeAdvertiser RouteAdvertiser routeAdvertiser RouteAdvertiser
pubClient *eventbus.Client
updatePub *eventbus.Publisher[appctype.RouteUpdate]
storePub *eventbus.Publisher[appctype.RouteInfo]
// storeRoutesFunc will be called to persist routes if it is not nil. // hasStoredRoutes records whether the connector was initialized with
storeRoutesFunc func(*RouteInfo) error // persisted route information.
hasStoredRoutes bool
// mu guards the fields that follow // mu guards the fields that follow
mu sync.Mutex mu syncs.Mutex
// domains is a map of lower case domain names with no trailing dot, to an // domains is a map of lower case domain names with no trailing dot, to an
// ordered list of resolved IP addresses. // ordered list of resolved IP addresses.
@ -164,53 +158,83 @@ type AppConnector struct {
writeRateDay *rateLogger writeRateDay *rateLogger
} }
// Config carries the settings for an [AppConnector].
type Config struct {
// Logf is the logger to which debug logs from the connector will be sent.
// It must be non-nil.
Logf logger.Logf
// EventBus receives events when the collection of routes maintained by the
// connector is updated. It must be non-nil.
EventBus *eventbus.Bus
// RouteAdvertiser allows the connector to update the set of advertised routes.
RouteAdvertiser RouteAdvertiser
// RouteInfo, if non-nil, use used as the initial set of routes for the
// connector. If nil, the connector starts empty.
RouteInfo *appctype.RouteInfo
// HasStoredRoutes indicates that the connector should assume stored routes.
HasStoredRoutes bool
}
// NewAppConnector creates a new AppConnector. // NewAppConnector creates a new AppConnector.
func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, routeInfo *RouteInfo, storeRoutesFunc func(*RouteInfo) error) *AppConnector { func NewAppConnector(c Config) *AppConnector {
switch {
case c.Logf == nil:
panic("missing logger")
case c.EventBus == nil:
panic("missing event bus")
}
ec := c.EventBus.Client("appc.AppConnector")
ac := &AppConnector{ ac := &AppConnector{
logf: logger.WithPrefix(logf, "appc: "), logf: logger.WithPrefix(c.Logf, "appc: "),
routeAdvertiser: routeAdvertiser, eventBus: c.EventBus,
storeRoutesFunc: storeRoutesFunc, pubClient: ec,
updatePub: eventbus.Publish[appctype.RouteUpdate](ec),
storePub: eventbus.Publish[appctype.RouteInfo](ec),
routeAdvertiser: c.RouteAdvertiser,
hasStoredRoutes: c.HasStoredRoutes,
} }
if routeInfo != nil { if c.RouteInfo != nil {
ac.domains = routeInfo.Domains ac.domains = c.RouteInfo.Domains
ac.wildcards = routeInfo.Wildcards ac.wildcards = c.RouteInfo.Wildcards
ac.controlRoutes = routeInfo.Control ac.controlRoutes = c.RouteInfo.Control
} }
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, l int64) { ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, ln int64) {
ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, l) ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, ln)
metricStoreRoutes(c, l) metricStoreRoutes(c, ln)
}) })
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, l int64) { ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, ln int64) {
ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, l) ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, ln)
}) })
return ac return ac
} }
// ShouldStoreRoutes returns true if the appconnector was created with the controlknob on // ShouldStoreRoutes returns true if the appconnector was created with the controlknob on
// and is storing its discovered routes persistently. // and is storing its discovered routes persistently.
func (e *AppConnector) ShouldStoreRoutes() bool { func (e *AppConnector) ShouldStoreRoutes() bool { return e.hasStoredRoutes }
return e.storeRoutesFunc != nil
}
// storeRoutesLocked takes the current state of the AppConnector and persists it // storeRoutesLocked takes the current state of the AppConnector and persists it
func (e *AppConnector) storeRoutesLocked() error { func (e *AppConnector) storeRoutesLocked() {
if !e.ShouldStoreRoutes() { if e.storePub.ShouldPublish() {
return nil // log write rate and write size
} numRoutes := int64(len(e.controlRoutes))
for _, rs := range e.domains {
// log write rate and write size numRoutes += int64(len(rs))
numRoutes := int64(len(e.controlRoutes)) }
for _, rs := range e.domains { e.writeRateMinute.update(numRoutes)
numRoutes += int64(len(rs)) e.writeRateDay.update(numRoutes)
e.storePub.Publish(appctype.RouteInfo{
// Clone here, as the subscriber will handle these outside our lock.
Control: slices.Clone(e.controlRoutes),
Domains: maps.Clone(e.domains),
Wildcards: slices.Clone(e.wildcards),
})
} }
e.writeRateMinute.update(numRoutes)
e.writeRateDay.update(numRoutes)
return e.storeRoutesFunc(&RouteInfo{
Control: e.controlRoutes,
Domains: e.domains,
Wildcards: e.wildcards,
})
} }
// ClearRoutes removes all route state from the AppConnector. // ClearRoutes removes all route state from the AppConnector.
@ -220,7 +244,8 @@ func (e *AppConnector) ClearRoutes() error {
e.controlRoutes = nil e.controlRoutes = nil
e.domains = nil e.domains = nil
e.wildcards = nil e.wildcards = nil
return e.storeRoutesLocked() e.storeRoutesLocked()
return nil
} }
// UpdateDomainsAndRoutes starts an asynchronous update of the configuration // UpdateDomainsAndRoutes starts an asynchronous update of the configuration
@ -249,6 +274,18 @@ func (e *AppConnector) Wait(ctx context.Context) {
e.queue.Wait(ctx) e.queue.Wait(ctx)
} }
// Close closes the connector and cleans up resources associated with it.
// It is safe (and a noop) to call Close on nil.
func (e *AppConnector) Close() {
if e == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.queue.Shutdown() // TODO(creachadair): Should we wait for it too?
e.pubClient.Close()
}
func (e *AppConnector) updateDomains(domains []string) { func (e *AppConnector) updateDomains(domains []string) {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@ -280,20 +317,26 @@ func (e *AppConnector) updateDomains(domains []string) {
} }
} }
// Everything left in oldDomains is a domain we're no longer tracking // Everything left in oldDomains is a domain we're no longer tracking and we
// and if we are storing route info we can unadvertise the routes // can unadvertise the routes.
if e.ShouldStoreRoutes() { if e.hasStoredRoutes {
toRemove := []netip.Prefix{} toRemove := []netip.Prefix{}
for _, addrs := range oldDomains { for _, addrs := range oldDomains {
for _, a := range addrs { for _, a := range addrs {
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen())) toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
} }
} }
e.queue.Add(func() {
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil { if len(toRemove) != 0 {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err) if ra := e.routeAdvertiser; ra != nil {
e.queue.Add(func() {
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
})
} }
}) e.updatePub.Publish(appctype.RouteUpdate{Unadvertise: toRemove})
}
} }
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards) e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
@ -314,11 +357,10 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
var toRemove []netip.Prefix var toRemove []netip.Prefix
// If we're storing routes and know e.controlRoutes is a good // If we know e.controlRoutes is a good representation of what should be in
// representation of what should be in AdvertisedRoutes we can stop // AdvertisedRoutes we can stop advertising routes that used to be in
// advertising routes that used to be in e.controlRoutes but are not // e.controlRoutes but are not in routes.
// in routes. if e.hasStoredRoutes {
if e.ShouldStoreRoutes() {
toRemove = routesWithout(e.controlRoutes, routes) toRemove = routesWithout(e.controlRoutes, routes)
} }
@ -335,19 +377,23 @@ nextRoute:
} }
} }
e.queue.Add(func() { if e.routeAdvertiser != nil {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil { e.queue.Add(func() {
e.logf("failed to advertise routes: %v: %v", routes, err) if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
} e.logf("failed to advertise routes: %v: %v", routes, err)
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil { }
e.logf("failed to unadvertise routes: %v: %v", toRemove, err) if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
} e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
})
}
e.updatePub.Publish(appctype.RouteUpdate{
Advertise: routes,
Unadvertise: toRemove,
}) })
e.controlRoutes = routes e.controlRoutes = routes
if err := e.storeRoutesLocked(); err != nil { e.storeRoutesLocked()
e.logf("failed to store route info: %v", err)
}
} }
// Domains returns the currently configured domain list. // Domains returns the currently configured domain list.
@ -372,124 +418,6 @@ func (e *AppConnector) DomainRoutes() map[string][]netip.Addr {
return drCopy return drCopy
} }
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
// response is being returned over the PeerAPI. The response is parsed and
// matched against the configured domains, if matched the routeAdvertiser is
// advised to advertise the discovered route.
func (e *AppConnector) ObserveDNSResponse(res []byte) error {
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return err
}
if err := p.SkipAllQuestions(); err != nil {
return err
}
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
// a CNAME chain back to the original query for flattening. The keys are
// CNAME record targets, and the value is the name the record answers, so
// for www.example.com CNAME example.com, the map would contain
// ["example.com"] = "www.example.com".
var cnameChain map[string]string
// addressRecords is a list of address records found in the response.
var addressRecords map[string][]netip.Addr
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return err
}
if h.Class != dnsmessage.ClassINET {
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
switch h.Type {
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
default:
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
if len(domain) == 0 {
continue
}
if h.Type == dnsmessage.TypeCNAME {
res, err := p.CNAMEResource()
if err != nil {
return err
}
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
if len(cname) == 0 {
continue
}
mak.Set(&cnameChain, cname, domain)
continue
}
switch h.Type {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return err
}
addr := netip.AddrFrom4(r.A)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return err
}
addr := netip.AddrFrom16(r.AAAA)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
default:
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
}
e.mu.Lock()
defer e.mu.Unlock()
for domain, addrs := range addressRecords {
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
// domain and none of the CNAMEs in the chain are routed
if !isRouted {
continue
}
// advertise each address we have learned for the routed domain, that
// was not already known.
var toAdvertise []netip.Prefix
for _, addr := range addrs {
if !e.isAddrKnownLocked(domain, addr) {
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
}
}
if len(toAdvertise) > 0 {
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
e.scheduleAdvertisement(domain, toAdvertise...)
}
}
return nil
}
// starting from the given domain that resolved to an address, find it, or any // starting from the given domain that resolved to an address, find it, or any
// of the domains in the CNAME chain toward resolving it, that are routed // of the domains in the CNAME chain toward resolving it, that are routed
// domains, returning the routed domain name and a bool indicating whether a // domains, returning the routed domain name and a bool indicating whether a
@ -544,10 +472,13 @@ func (e *AppConnector) isAddrKnownLocked(domain string, addr netip.Addr) bool {
// associated with the given domain. // associated with the given domain.
func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Prefix) { func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Prefix) {
e.queue.Add(func() { e.queue.Add(func() {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil { if e.routeAdvertiser != nil {
e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err) if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
return e.logf("failed to advertise routes for %s: %v: %v", domain, routes, err)
return
}
} }
e.updatePub.Publish(appctype.RouteUpdate{Advertise: routes})
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@ -561,9 +492,7 @@ func (e *AppConnector) scheduleAdvertisement(domain string, routes ...netip.Pref
e.logf("[v2] advertised route for %v: %v", domain, addr) e.logf("[v2] advertised route for %v: %v", domain, addr)
} }
} }
if err := e.storeRoutesLocked(); err != nil { e.storeRoutesLocked()
e.logf("failed to store route info: %v", err)
}
}) })
} }
@ -581,8 +510,8 @@ func (e *AppConnector) addDomainAddrLocked(domain string, addr netip.Addr) {
slices.SortFunc(e.domains[domain], compareAddr) slices.SortFunc(e.domains[domain], compareAddr)
} }
func compareAddr(l, r netip.Addr) int { func compareAddr(a, b netip.Addr) int {
return l.Compare(r) return a.Compare(b)
} }
// routesWithout returns a without b where a and b // routesWithout returns a without b where a and b

@ -1,10 +1,11 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package appc package appc
import ( import (
"context" stdcmp "cmp"
"fmt"
"net/netip" "net/netip"
"reflect" "reflect"
"slices" "slices"
@ -12,28 +13,31 @@ import (
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/net/dns/dnsmessage" "golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc/appctest" "tailscale.com/appc/appctest"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/types/appctype"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus/eventbustest"
"tailscale.com/util/mak" "tailscale.com/util/mak"
"tailscale.com/util/must" "tailscale.com/util/must"
"tailscale.com/util/slicesx" "tailscale.com/util/slicesx"
) )
func fakeStoreRoutes(*RouteInfo) error { return nil }
func TestUpdateDomains(t *testing.T) { func TestUpdateDomains(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() a := NewAppConnector(Config{
var a *AppConnector Logf: t.Logf,
if shouldStore { EventBus: bus,
a = NewAppConnector(t.Logf, &appctest.RouteCollector{}, &RouteInfo{}, fakeStoreRoutes) HasStoredRoutes: shouldStore,
} else { })
a = NewAppConnector(t.Logf, &appctest.RouteCollector{}, nil, nil) t.Cleanup(a.Close)
}
a.UpdateDomains([]string{"example.com"})
a.UpdateDomains([]string{"example.com"})
a.Wait(ctx) a.Wait(ctx)
if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) { if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want) t.Errorf("got %v; want %v", got, want)
@ -58,15 +62,19 @@ func TestUpdateDomains(t *testing.T) {
} }
func TestUpdateRoutes(t *testing.T) { func TestUpdateRoutes(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
a.updateDomains([]string{"*.example.com"}) a.updateDomains([]string{"*.example.com"})
// This route should be collapsed into the range // This route should be collapsed into the range
@ -103,19 +111,37 @@ func TestUpdateRoutes(t *testing.T) {
if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) { if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) {
t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes()) t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes())
} }
if err := eventbustest.Expect(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.2.1/32")}),
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.1/32")}),
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{
Advertise: prefixes("192.0.0.1/32", "192.0.2.0/24"),
Unadvertise: prefixes("192.0.2.1/32"),
}),
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
} }
} }
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) { func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
ctx := context.Background() ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")}) mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")})
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}) rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")} routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
@ -125,23 +151,36 @@ func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) { if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes) t.Fatalf("got %v, want %v", rc.Routes(), routes)
} }
if err := eventbustest.ExpectExactly(w,
eqUpdate(appctype.RouteUpdate{
Advertise: prefixes("192.0.2.0/24"),
Unadvertise: prefixes("192.0.2.1/32"),
}),
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
} }
} }
func TestDomainRoutes(t *testing.T) { func TestDomainRoutes(t *testing.T) {
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
a.updateDomains([]string{"example.com"}) a.updateDomains([]string{"example.com"})
if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil { if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil {
t.Errorf("ObserveDNSResponse: %v", err) t.Errorf("ObserveDNSResponse: %v", err)
} }
a.Wait(context.Background()) a.Wait(t.Context())
want := map[string][]netip.Addr{ want := map[string][]netip.Addr{
"example.com": {netip.MustParseAddr("192.0.0.8")}, "example.com": {netip.MustParseAddr("192.0.0.8")},
@ -150,19 +189,29 @@ func TestDomainRoutes(t *testing.T) {
if got := a.DomainRoutes(); !reflect.DeepEqual(got, want) { if got := a.DomainRoutes(); !reflect.DeepEqual(got, want) {
t.Fatalf("DomainRoutes: got %v, want %v", got, want) t.Fatalf("DomainRoutes: got %v, want %v", got, want)
} }
if err := eventbustest.ExpectExactly(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}),
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
} }
} }
func TestObserveDNSResponse(t *testing.T) { func TestObserveDNSResponse(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
// a has no domains configured, so it should not advertise any routes // a has no domains configured, so it should not advertise any routes
if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil { if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil {
@ -239,19 +288,38 @@ func TestObserveDNSResponse(t *testing.T) {
if !slices.Contains(a.domains["example.com"], netip.MustParseAddr("192.0.2.1")) { if !slices.Contains(a.domains["example.com"], netip.MustParseAddr("192.0.2.1")) {
t.Errorf("missing %v from %v", "192.0.2.1", a.domains["exmaple.com"]) t.Errorf("missing %v from %v", "192.0.2.1", a.domains["exmaple.com"])
} }
if err := eventbustest.ExpectExactly(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}), // from initial DNS response, via example.com
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.9/32")}), // from CNAME response
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.10/32")}), // from CNAME response, mid-chain
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("2001:db8::1/128")}), // v6 DNS response
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.2.0/24")}), // additional prefix
eventbustest.Type[appctype.RouteInfo](),
// N.B. no update for 192.0.2.1 as it is already covered
); err != nil {
t.Error(err)
}
} }
} }
func TestWildcardDomains(t *testing.T) { func TestWildcardDomains(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
a.updateDomains([]string{"*.example.com"}) a.updateDomains([]string{"*.example.com"})
if err := a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8")); err != nil { if err := a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8")); err != nil {
@ -278,6 +346,13 @@ func TestWildcardDomains(t *testing.T) {
if len(a.wildcards) != 1 { if len(a.wildcards) != 1 {
t.Errorf("expected only one wildcard domain, got %v", a.wildcards) t.Errorf("expected only one wildcard domain, got %v", a.wildcards)
} }
if err := eventbustest.ExpectExactly(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}),
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
} }
} }
@ -393,8 +468,10 @@ func prefixes(in ...string) []netip.Prefix {
} }
func TestUpdateRouteRouteRemoval(t *testing.T) { func TestUpdateRouteRouteRemoval(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) { assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
@ -406,12 +483,14 @@ func TestUpdateRouteRouteRemoval(t *testing.T) {
} }
} }
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
// nothing has yet been advertised // nothing has yet been advertised
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{}) assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
@ -434,12 +513,21 @@ func TestUpdateRouteRouteRemoval(t *testing.T) {
wantRemovedRoutes = prefixes("1.2.3.2/32") wantRemovedRoutes = prefixes("1.2.3.2/32")
} }
assertRoutes("removal", wantRoutes, wantRemovedRoutes) assertRoutes("removal", wantRoutes, wantRemovedRoutes)
if err := eventbustest.Expect(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32", "1.2.3.2/32")}), // no duplicates here
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
} }
} }
func TestUpdateDomainRouteRemoval(t *testing.T) { func TestUpdateDomainRouteRemoval(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) { assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
@ -451,12 +539,14 @@ func TestUpdateDomainRouteRemoval(t *testing.T) {
} }
} }
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{}) assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
a.UpdateDomainsAndRoutes([]string{"a.example.com", "b.example.com"}, []netip.Prefix{}) a.UpdateDomainsAndRoutes([]string{"a.example.com", "b.example.com"}, []netip.Prefix{})
@ -489,12 +579,30 @@ func TestUpdateDomainRouteRemoval(t *testing.T) {
wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32") wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
} }
assertRoutes("removal", wantRoutes, wantRemovedRoutes) assertRoutes("removal", wantRoutes, wantRemovedRoutes)
wantEvents := []any{
// Each DNS record observed triggers an update.
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.2/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.3/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.4/32")}),
}
if shouldStore {
wantEvents = append(wantEvents, eqUpdate(appctype.RouteUpdate{
Unadvertise: prefixes("1.2.3.3/32", "1.2.3.4/32"),
}))
}
if err := eventbustest.Expect(w, wantEvents...); err != nil {
t.Error(err)
}
} }
} }
func TestUpdateWildcardRouteRemoval(t *testing.T) { func TestUpdateWildcardRouteRemoval(t *testing.T) {
ctx := t.Context()
bus := eventbustest.NewBus(t)
for _, shouldStore := range []bool{false, true} { for _, shouldStore := range []bool{false, true} {
ctx := context.Background() w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) { assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
@ -506,12 +614,14 @@ func TestUpdateWildcardRouteRemoval(t *testing.T) {
} }
} }
var a *AppConnector a := NewAppConnector(Config{
if shouldStore { Logf: t.Logf,
a = NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) EventBus: bus,
} else { RouteAdvertiser: rc,
a = NewAppConnector(t.Logf, rc, nil, nil) HasStoredRoutes: shouldStore,
} })
t.Cleanup(a.Close)
assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{}) assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
a.UpdateDomainsAndRoutes([]string{"a.example.com", "*.b.example.com"}, []netip.Prefix{}) a.UpdateDomainsAndRoutes([]string{"a.example.com", "*.b.example.com"}, []netip.Prefix{})
@ -544,6 +654,22 @@ func TestUpdateWildcardRouteRemoval(t *testing.T) {
wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32") wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
} }
assertRoutes("removal", wantRoutes, wantRemovedRoutes) assertRoutes("removal", wantRoutes, wantRemovedRoutes)
wantEvents := []any{
// Each DNS record observed triggers an update.
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.2/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.3/32")}),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.4/32")}),
}
if shouldStore {
wantEvents = append(wantEvents, eqUpdate(appctype.RouteUpdate{
Unadvertise: prefixes("1.2.3.3/32", "1.2.3.4/32"),
}))
}
if err := eventbustest.Expect(w, wantEvents...); err != nil {
t.Error(err)
}
} }
} }
@ -646,10 +772,22 @@ func TestMetricBucketsAreSorted(t *testing.T) {
// routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling // routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
// back into AppConnector via authReconfig. If everything is called // back into AppConnector via authReconfig. If everything is called
// synchronously, this results in a deadlock on AppConnector.mu. // synchronously, this results in a deadlock on AppConnector.mu.
//
// TODO(creachadair, 2025-09-18): Remove this along with the advertiser
// interface once the LocalBackend is switched to use the event bus and the
// tests have been updated not to need it.
func TestUpdateRoutesDeadlock(t *testing.T) { func TestUpdateRoutesDeadlock(t *testing.T) {
ctx := context.Background() ctx := t.Context()
bus := eventbustest.NewBus(t)
w := eventbustest.NewWatcher(t, bus)
rc := &appctest.RouteCollector{} rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes) a := NewAppConnector(Config{
Logf: t.Logf,
EventBus: bus,
RouteAdvertiser: rc,
HasStoredRoutes: true,
})
t.Cleanup(a.Close)
advertiseCalled := new(atomic.Bool) advertiseCalled := new(atomic.Bool)
unadvertiseCalled := new(atomic.Bool) unadvertiseCalled := new(atomic.Bool)
@ -693,4 +831,42 @@ func TestUpdateRoutesDeadlock(t *testing.T) {
if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) { if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
t.Fatalf("got %v, want %v", rc.Routes(), want) t.Fatalf("got %v, want %v", rc.Routes(), want)
} }
if err := eventbustest.ExpectExactly(w,
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("127.0.0.1/32", "127.0.0.2/32")}),
eventbustest.Type[appctype.RouteInfo](),
eqUpdate(appctype.RouteUpdate{Advertise: prefixes("127.0.0.1/32"), Unadvertise: prefixes("127.0.0.2/32")}),
eventbustest.Type[appctype.RouteInfo](),
); err != nil {
t.Error(err)
}
}
type textUpdate struct {
Advertise []string
Unadvertise []string
}
func routeUpdateToText(u appctype.RouteUpdate) textUpdate {
var out textUpdate
for _, p := range u.Advertise {
out.Advertise = append(out.Advertise, p.String())
}
for _, p := range u.Unadvertise {
out.Unadvertise = append(out.Unadvertise, p.String())
}
return out
}
// eqUpdate generates an eventbus test filter that matches a appctype.RouteUpdate
// message equal to want, or reports an error giving a human-readable diff.
func eqUpdate(want appctype.RouteUpdate) func(appctype.RouteUpdate) error {
return func(got appctype.RouteUpdate) error {
if diff := cmp.Diff(routeUpdateToText(got), routeUpdateToText(want),
cmpopts.SortSlices(stdcmp.Less[string]),
); diff != "" {
return fmt.Errorf("wrong update (-got, +want):\n%s", diff)
}
return nil
}
} }

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package appctest contains code to help test App Connectors. // Package appctest contains code to help test App Connectors.

@ -0,0 +1,173 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package appc
import (
"cmp"
"net/netip"
"slices"
"sync"
"tailscale.com/tailcfg"
"tailscale.com/types/appctype"
"tailscale.com/util/mak"
"tailscale.com/util/set"
)
// Conn25 holds the developing state for the as yet nascent next generation app connector.
// There is currently (2025-12-08) no actual app connecting functionality.
type Conn25 struct {
mu sync.Mutex
transitIPs map[tailcfg.NodeID]map[netip.Addr]netip.Addr
}
const dupeTransitIPMessage = "Duplicate transit address in ConnectorTransitIPRequest"
// HandleConnectorTransitIPRequest creates a ConnectorTransitIPResponse in response to a ConnectorTransitIPRequest.
// It updates the connectors mapping of TransitIP->DestinationIP per peer (tailcfg.NodeID).
// If a peer has stored this mapping in the connector Conn25 will route traffic to TransitIPs to DestinationIPs for that peer.
func (c *Conn25) HandleConnectorTransitIPRequest(nid tailcfg.NodeID, ctipr ConnectorTransitIPRequest) ConnectorTransitIPResponse {
resp := ConnectorTransitIPResponse{}
seen := map[netip.Addr]bool{}
for _, each := range ctipr.TransitIPs {
if seen[each.TransitIP] {
resp.TransitIPs = append(resp.TransitIPs, TransitIPResponse{
Code: OtherFailure,
Message: dupeTransitIPMessage,
})
continue
}
tipresp := c.handleTransitIPRequest(nid, each)
seen[each.TransitIP] = true
resp.TransitIPs = append(resp.TransitIPs, tipresp)
}
return resp
}
func (c *Conn25) handleTransitIPRequest(nid tailcfg.NodeID, tipr TransitIPRequest) TransitIPResponse {
c.mu.Lock()
defer c.mu.Unlock()
if c.transitIPs == nil {
c.transitIPs = make(map[tailcfg.NodeID]map[netip.Addr]netip.Addr)
}
peerMap, ok := c.transitIPs[nid]
if !ok {
peerMap = make(map[netip.Addr]netip.Addr)
c.transitIPs[nid] = peerMap
}
peerMap[tipr.TransitIP] = tipr.DestinationIP
return TransitIPResponse{}
}
func (c *Conn25) transitIPTarget(nid tailcfg.NodeID, tip netip.Addr) netip.Addr {
c.mu.Lock()
defer c.mu.Unlock()
return c.transitIPs[nid][tip]
}
// TransitIPRequest details a single TransitIP allocation request from a client to a
// connector.
type TransitIPRequest struct {
// TransitIP is the intermediate destination IP that will be received at this
// connector and will be replaced by DestinationIP when performing DNAT.
TransitIP netip.Addr `json:"transitIP,omitzero"`
// DestinationIP is the final destination IP that connections to the TransitIP
// should be mapped to when performing DNAT.
DestinationIP netip.Addr `json:"destinationIP,omitzero"`
}
// ConnectorTransitIPRequest is the request body for a PeerAPI request to
// /connector/transit-ip and can include zero or more TransitIP allocation requests.
type ConnectorTransitIPRequest struct {
// TransitIPs is the list of requested mappings.
TransitIPs []TransitIPRequest `json:"transitIPs,omitempty"`
}
// TransitIPResponseCode appears in TransitIPResponse and signifies success or failure status.
type TransitIPResponseCode int
const (
// OK indicates that the mapping was created as requested.
OK TransitIPResponseCode = 0
// OtherFailure indicates that the mapping failed for a reason that does not have
// another relevant [TransitIPResponsecode].
OtherFailure TransitIPResponseCode = 1
)
// TransitIPResponse is the response to a TransitIPRequest
type TransitIPResponse struct {
// Code is an error code indicating success or failure of the [TransitIPRequest].
Code TransitIPResponseCode `json:"code,omitzero"`
// Message is an error message explaining what happened, suitable for logging but
// not necessarily suitable for displaying in a UI to non-technical users. It
// should be empty when [Code] is [OK].
Message string `json:"message,omitzero"`
}
// ConnectorTransitIPResponse is the response to a ConnectorTransitIPRequest
type ConnectorTransitIPResponse struct {
// TransitIPs is the list of outcomes for each requested mapping. Elements
// correspond to the order of [ConnectorTransitIPRequest.TransitIPs].
TransitIPs []TransitIPResponse `json:"transitIPs,omitempty"`
}
const AppConnectorsExperimentalAttrName = "tailscale.com/app-connectors-experimental"
// PickSplitDNSPeers looks at the netmap peers capabilities and finds which peers
// want to be connectors for which domains.
func PickSplitDNSPeers(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView, peers map[tailcfg.NodeID]tailcfg.NodeView) map[string][]tailcfg.NodeView {
var m map[string][]tailcfg.NodeView
if !hasCap(AppConnectorsExperimentalAttrName) {
return m
}
apps, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.AppConnectorAttr](self.CapMap(), AppConnectorsExperimentalAttrName)
if err != nil {
return m
}
tagToDomain := make(map[string][]string)
for _, app := range apps {
for _, tag := range app.Connectors {
tagToDomain[tag] = append(tagToDomain[tag], app.Domains...)
}
}
// NodeIDs are Comparable, and we have a map of NodeID to NodeView anyway, so
// use a Set of NodeIDs to deduplicate, and populate into a []NodeView later.
var work map[string]set.Set[tailcfg.NodeID]
for _, peer := range peers {
if !peer.Valid() || !peer.Hostinfo().Valid() {
continue
}
if isConn, _ := peer.Hostinfo().AppConnector().Get(); !isConn {
continue
}
for _, t := range peer.Tags().All() {
domains := tagToDomain[t]
for _, domain := range domains {
if work[domain] == nil {
mak.Set(&work, domain, set.Set[tailcfg.NodeID]{})
}
work[domain].Add(peer.ID())
}
}
}
// Populate m. Make a []tailcfg.NodeView from []tailcfg.NodeID using the peers map.
// And sort it to our preference.
for domain, ids := range work {
nodes := make([]tailcfg.NodeView, 0, ids.Len())
for id := range ids {
nodes = append(nodes, peers[id])
}
// The ordering of the nodes in the map vals is semantic (dnsConfigForNetmap uses the first node it can
// get a peer api url for as its split dns target). We can think of it as a preference order, except that
// we don't (currently 2026-01-14) have any preference over which node is chosen.
slices.SortFunc(nodes, func(a, b tailcfg.NodeView) int {
return cmp.Compare(a.ID(), b.ID())
})
mak.Set(&m, domain, nodes)
}
return m
}

@ -0,0 +1,311 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package appc
import (
"encoding/json"
"net/netip"
"reflect"
"testing"
"tailscale.com/tailcfg"
"tailscale.com/types/appctype"
"tailscale.com/types/opt"
)
// TestHandleConnectorTransitIPRequestZeroLength tests that if sent a
// ConnectorTransitIPRequest with 0 TransitIPRequests, we respond with a
// ConnectorTransitIPResponse with 0 TransitIPResponses.
func TestHandleConnectorTransitIPRequestZeroLength(t *testing.T) {
c := &Conn25{}
req := ConnectorTransitIPRequest{}
nid := tailcfg.NodeID(1)
resp := c.HandleConnectorTransitIPRequest(nid, req)
if len(resp.TransitIPs) != 0 {
t.Fatalf("n TransitIPs in response: %d, want 0", len(resp.TransitIPs))
}
}
// TestHandleConnectorTransitIPRequestStoresAddr tests that if sent a
// request with a transit addr and a destination addr we store that mapping
// and can retrieve it. If sent another req with a different dst for that transit addr
// we store that instead.
func TestHandleConnectorTransitIPRequestStoresAddr(t *testing.T) {
c := &Conn25{}
nid := tailcfg.NodeID(1)
tip := netip.MustParseAddr("0.0.0.1")
dip := netip.MustParseAddr("1.2.3.4")
dip2 := netip.MustParseAddr("1.2.3.5")
mr := func(t, d netip.Addr) ConnectorTransitIPRequest {
return ConnectorTransitIPRequest{
TransitIPs: []TransitIPRequest{
{TransitIP: t, DestinationIP: d},
},
}
}
resp := c.HandleConnectorTransitIPRequest(nid, mr(tip, dip))
if len(resp.TransitIPs) != 1 {
t.Fatalf("n TransitIPs in response: %d, want 1", len(resp.TransitIPs))
}
got := resp.TransitIPs[0].Code
if got != TransitIPResponseCode(0) {
t.Fatalf("TransitIP Code: %d, want 0", got)
}
gotAddr := c.transitIPTarget(nid, tip)
if gotAddr != dip {
t.Fatalf("Connector stored destination for tip: %v, want %v", gotAddr, dip)
}
// mapping can be overwritten
resp2 := c.HandleConnectorTransitIPRequest(nid, mr(tip, dip2))
if len(resp2.TransitIPs) != 1 {
t.Fatalf("n TransitIPs in response: %d, want 1", len(resp2.TransitIPs))
}
got2 := resp.TransitIPs[0].Code
if got2 != TransitIPResponseCode(0) {
t.Fatalf("TransitIP Code: %d, want 0", got2)
}
gotAddr2 := c.transitIPTarget(nid, tip)
if gotAddr2 != dip2 {
t.Fatalf("Connector stored destination for tip: %v, want %v", gotAddr, dip2)
}
}
// TestHandleConnectorTransitIPRequestMultipleTIP tests that we can
// get a req with multiple mappings and we store them all. Including
// multiple transit addrs for the same destination.
func TestHandleConnectorTransitIPRequestMultipleTIP(t *testing.T) {
c := &Conn25{}
nid := tailcfg.NodeID(1)
tip := netip.MustParseAddr("0.0.0.1")
tip2 := netip.MustParseAddr("0.0.0.2")
tip3 := netip.MustParseAddr("0.0.0.3")
dip := netip.MustParseAddr("1.2.3.4")
dip2 := netip.MustParseAddr("1.2.3.5")
req := ConnectorTransitIPRequest{
TransitIPs: []TransitIPRequest{
{TransitIP: tip, DestinationIP: dip},
{TransitIP: tip2, DestinationIP: dip2},
// can store same dst addr for multiple transit addrs
{TransitIP: tip3, DestinationIP: dip},
},
}
resp := c.HandleConnectorTransitIPRequest(nid, req)
if len(resp.TransitIPs) != 3 {
t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs))
}
for i := 0; i < 3; i++ {
got := resp.TransitIPs[i].Code
if got != TransitIPResponseCode(0) {
t.Fatalf("i=%d TransitIP Code: %d, want 0", i, got)
}
}
gotAddr1 := c.transitIPTarget(nid, tip)
if gotAddr1 != dip {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip)
}
gotAddr2 := c.transitIPTarget(nid, tip2)
if gotAddr2 != dip2 {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip2)
}
gotAddr3 := c.transitIPTarget(nid, tip3)
if gotAddr3 != dip {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip3, gotAddr3, dip)
}
}
// TestHandleConnectorTransitIPRequestSameTIP tests that if we get
// a req that has more than one TransitIPRequest for the same transit addr
// only the first is stored, and the subsequent ones get an error code and
// message in the response.
func TestHandleConnectorTransitIPRequestSameTIP(t *testing.T) {
c := &Conn25{}
nid := tailcfg.NodeID(1)
tip := netip.MustParseAddr("0.0.0.1")
tip2 := netip.MustParseAddr("0.0.0.2")
dip := netip.MustParseAddr("1.2.3.4")
dip2 := netip.MustParseAddr("1.2.3.5")
dip3 := netip.MustParseAddr("1.2.3.6")
req := ConnectorTransitIPRequest{
TransitIPs: []TransitIPRequest{
{TransitIP: tip, DestinationIP: dip},
// cannot have dupe TransitIPs in one ConnectorTransitIPRequest
{TransitIP: tip, DestinationIP: dip2},
{TransitIP: tip2, DestinationIP: dip3},
},
}
resp := c.HandleConnectorTransitIPRequest(nid, req)
if len(resp.TransitIPs) != 3 {
t.Fatalf("n TransitIPs in response: %d, want 3", len(resp.TransitIPs))
}
got := resp.TransitIPs[0].Code
if got != TransitIPResponseCode(0) {
t.Fatalf("i=0 TransitIP Code: %d, want 0", got)
}
msg := resp.TransitIPs[0].Message
if msg != "" {
t.Fatalf("i=0 TransitIP Message: \"%s\", want \"%s\"", msg, "")
}
got1 := resp.TransitIPs[1].Code
if got1 != TransitIPResponseCode(1) {
t.Fatalf("i=1 TransitIP Code: %d, want 1", got1)
}
msg1 := resp.TransitIPs[1].Message
if msg1 != dupeTransitIPMessage {
t.Fatalf("i=1 TransitIP Message: \"%s\", want \"%s\"", msg1, dupeTransitIPMessage)
}
got2 := resp.TransitIPs[2].Code
if got2 != TransitIPResponseCode(0) {
t.Fatalf("i=2 TransitIP Code: %d, want 0", got2)
}
msg2 := resp.TransitIPs[2].Message
if msg2 != "" {
t.Fatalf("i=2 TransitIP Message: \"%s\", want \"%s\"", msg, "")
}
gotAddr1 := c.transitIPTarget(nid, tip)
if gotAddr1 != dip {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip, gotAddr1, dip)
}
gotAddr2 := c.transitIPTarget(nid, tip2)
if gotAddr2 != dip3 {
t.Fatalf("Connector stored destination for tip(%v): %v, want %v", tip2, gotAddr2, dip3)
}
}
// TestGetDstIPUnknownTIP tests that unknown transit addresses can be looked up without problem.
func TestTransitIPTargetUnknownTIP(t *testing.T) {
c := &Conn25{}
nid := tailcfg.NodeID(1)
tip := netip.MustParseAddr("0.0.0.1")
got := c.transitIPTarget(nid, tip)
want := netip.Addr{}
if got != want {
t.Fatalf("Unknown transit addr, want: %v, got %v", want, got)
}
}
func TestPickSplitDNSPeers(t *testing.T) {
getBytesForAttr := func(name string, domains []string, tags []string) []byte {
attr := appctype.AppConnectorAttr{
Name: name,
Domains: domains,
Connectors: tags,
}
bs, err := json.Marshal(attr)
if err != nil {
t.Fatalf("test setup: %v", err)
}
return bs
}
appOneBytes := getBytesForAttr("app1", []string{"example.com"}, []string{"tag:one"})
appTwoBytes := getBytesForAttr("app2", []string{"a.example.com"}, []string{"tag:two"})
appThreeBytes := getBytesForAttr("app3", []string{"woo.b.example.com", "hoo.b.example.com"}, []string{"tag:three1", "tag:three2"})
appFourBytes := getBytesForAttr("app4", []string{"woo.b.example.com", "c.example.com"}, []string{"tag:four1", "tag:four2"})
makeNodeView := func(id tailcfg.NodeID, name string, tags []string) tailcfg.NodeView {
return (&tailcfg.Node{
ID: id,
Name: name,
Tags: tags,
Hostinfo: (&tailcfg.Hostinfo{AppConnector: opt.NewBool(true)}).View(),
}).View()
}
nvp1 := makeNodeView(1, "p1", []string{"tag:one"})
nvp2 := makeNodeView(2, "p2", []string{"tag:four1", "tag:four2"})
nvp3 := makeNodeView(3, "p3", []string{"tag:two", "tag:three1"})
nvp4 := makeNodeView(4, "p4", []string{"tag:two", "tag:three2", "tag:four2"})
for _, tt := range []struct {
name string
want map[string][]tailcfg.NodeView
peers []tailcfg.NodeView
config []tailcfg.RawMessage
}{
{
name: "empty",
},
{
name: "bad-config", // bad config should return a nil map rather than error.
config: []tailcfg.RawMessage{tailcfg.RawMessage(`hey`)},
},
{
name: "no-peers",
config: []tailcfg.RawMessage{tailcfg.RawMessage(appOneBytes)},
},
{
name: "peers-that-are-not-connectors",
config: []tailcfg.RawMessage{tailcfg.RawMessage(appOneBytes)},
peers: []tailcfg.NodeView{
(&tailcfg.Node{
ID: 5,
Name: "p5",
Tags: []string{"tag:one"},
}).View(),
(&tailcfg.Node{
ID: 6,
Name: "p6",
Tags: []string{"tag:one"},
}).View(),
},
},
{
name: "peers-that-dont-match-tags",
config: []tailcfg.RawMessage{tailcfg.RawMessage(appOneBytes)},
peers: []tailcfg.NodeView{
makeNodeView(5, "p5", []string{"tag:seven"}),
makeNodeView(6, "p6", nil),
},
},
{
name: "matching-tagged-connector-peers",
config: []tailcfg.RawMessage{
tailcfg.RawMessage(appOneBytes),
tailcfg.RawMessage(appTwoBytes),
tailcfg.RawMessage(appThreeBytes),
tailcfg.RawMessage(appFourBytes),
},
peers: []tailcfg.NodeView{
nvp1,
nvp2,
nvp3,
nvp4,
makeNodeView(5, "p5", nil),
},
want: map[string][]tailcfg.NodeView{
// p5 has no matching tags and so doesn't appear
"example.com": {nvp1},
"a.example.com": {nvp3, nvp4},
"woo.b.example.com": {nvp2, nvp3, nvp4},
"hoo.b.example.com": {nvp3, nvp4},
"c.example.com": {nvp2, nvp4},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
selfNode := &tailcfg.Node{}
if tt.config != nil {
selfNode.CapMap = tailcfg.NodeCapMap{
tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config,
}
}
selfView := selfNode.View()
peers := map[tailcfg.NodeID]tailcfg.NodeView{}
for _, p := range tt.peers {
peers[p.ID()] = p
}
got := PickSplitDNSPeers(func(_ tailcfg.NodeCapability) bool {
return true
}, selfView, peers)
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("got %v, want %v", got, tt.want)
}
})
}
}

@ -0,0 +1,61 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package appc
import (
"errors"
"net/netip"
"go4.org/netipx"
)
// errPoolExhausted is returned when there are no more addresses to iterate over.
var errPoolExhausted = errors.New("ip pool exhausted")
// ippool allows for iteration over all the addresses within a netipx.IPSet.
// netipx.IPSet has a Ranges call that returns the "minimum and sorted set of IP ranges that covers [the set]".
// netipx.IPRange is "an inclusive range of IP addresses from the same address family.". So we can iterate over
// all the addresses in the set by keeping a track of the last address we returned, calling Next on the last address
// to get the new one, and if we run off the edge of the current range, starting on the next one.
type ippool struct {
// ranges defines the addresses in the pool
ranges []netipx.IPRange
// last is internal tracking of which the last address provided was.
last netip.Addr
// rangeIdx is internal tracking of which netipx.IPRange from the IPSet we are currently on.
rangeIdx int
}
func newIPPool(ipset *netipx.IPSet) *ippool {
if ipset == nil {
return &ippool{}
}
return &ippool{ranges: ipset.Ranges()}
}
// next returns the next address from the set, or errPoolExhausted if we have
// iterated over the whole set.
func (ipp *ippool) next() (netip.Addr, error) {
if ipp.rangeIdx >= len(ipp.ranges) {
// ipset is empty or we have iterated off the end
return netip.Addr{}, errPoolExhausted
}
if !ipp.last.IsValid() {
// not initialized yet
ipp.last = ipp.ranges[0].From()
return ipp.last, nil
}
currRange := ipp.ranges[ipp.rangeIdx]
if ipp.last == currRange.To() {
// then we need to move to the next range
ipp.rangeIdx++
if ipp.rangeIdx >= len(ipp.ranges) {
return netip.Addr{}, errPoolExhausted
}
ipp.last = ipp.ranges[ipp.rangeIdx].From()
return ipp.last, nil
}
ipp.last = ipp.last.Next()
return ipp.last, nil
}

@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package appc
import (
"errors"
"net/netip"
"testing"
"go4.org/netipx"
"tailscale.com/util/must"
)
func TestNext(t *testing.T) {
a := ippool{}
_, err := a.next()
if !errors.Is(err, errPoolExhausted) {
t.Fatalf("expected errPoolExhausted, got %v", err)
}
var isb netipx.IPSetBuilder
ipset := must.Get(isb.IPSet())
b := newIPPool(ipset)
_, err = b.next()
if !errors.Is(err, errPoolExhausted) {
t.Fatalf("expected errPoolExhausted, got %v", err)
}
isb.AddRange(netipx.IPRangeFrom(netip.MustParseAddr("192.168.0.0"), netip.MustParseAddr("192.168.0.2")))
isb.AddRange(netipx.IPRangeFrom(netip.MustParseAddr("200.0.0.0"), netip.MustParseAddr("200.0.0.0")))
isb.AddRange(netipx.IPRangeFrom(netip.MustParseAddr("201.0.0.0"), netip.MustParseAddr("201.0.0.1")))
ipset = must.Get(isb.IPSet())
c := newIPPool(ipset)
expected := []string{
"192.168.0.0",
"192.168.0.1",
"192.168.0.2",
"200.0.0.0",
"201.0.0.0",
"201.0.0.1",
}
for i, want := range expected {
addr, err := c.next()
if err != nil {
t.Fatal(err)
}
if addr != netip.MustParseAddr(want) {
t.Fatalf("next call %d want: %s, got: %v", i, want, addr)
}
}
_, err = c.next()
if !errors.Is(err, errPoolExhausted) {
t.Fatalf("expected errPoolExhausted, got %v", err)
}
_, err = c.next()
if !errors.Is(err, errPoolExhausted) {
t.Fatalf("expected errPoolExhausted, got %v", err)
}
}

@ -0,0 +1,132 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_appconnectors
package appc
import (
"net/netip"
"strings"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/util/mak"
)
// ObserveDNSResponse is a callback invoked by the DNS resolver when a DNS
// response is being returned over the PeerAPI. The response is parsed and
// matched against the configured domains, if matched the routeAdvertiser is
// advised to advertise the discovered route.
func (e *AppConnector) ObserveDNSResponse(res []byte) error {
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return err
}
if err := p.SkipAllQuestions(); err != nil {
return err
}
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
// a CNAME chain back to the original query for flattening. The keys are
// CNAME record targets, and the value is the name the record answers, so
// for www.example.com CNAME example.com, the map would contain
// ["example.com"] = "www.example.com".
var cnameChain map[string]string
// addressRecords is a list of address records found in the response.
var addressRecords map[string][]netip.Addr
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return err
}
if h.Class != dnsmessage.ClassINET {
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
switch h.Type {
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
default:
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
domain := strings.TrimSuffix(strings.ToLower(h.Name.String()), ".")
if len(domain) == 0 {
continue
}
if h.Type == dnsmessage.TypeCNAME {
res, err := p.CNAMEResource()
if err != nil {
return err
}
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
if len(cname) == 0 {
continue
}
mak.Set(&cnameChain, cname, domain)
continue
}
switch h.Type {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return err
}
addr := netip.AddrFrom4(r.A)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return err
}
addr := netip.AddrFrom16(r.AAAA)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
default:
if err := p.SkipAnswer(); err != nil {
return err
}
continue
}
}
e.mu.Lock()
defer e.mu.Unlock()
for domain, addrs := range addressRecords {
domain, isRouted := e.findRoutedDomainLocked(domain, cnameChain)
// domain and none of the CNAMEs in the chain are routed
if !isRouted {
continue
}
// advertise each address we have learned for the routed domain, that
// was not already known.
var toAdvertise []netip.Prefix
for _, addr := range addrs {
if !e.isAddrKnownLocked(domain, addr) {
toAdvertise = append(toAdvertise, netip.PrefixFrom(addr, addr.BitLen()))
}
}
if len(toAdvertise) > 0 {
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
e.scheduleAdvertisement(domain, toAdvertise...)
}
}
return nil
}

@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build ts_omit_appconnectors
package appc
func (e *AppConnector) ObserveDNSResponse(res []byte) error { return nil }

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build tailscale_go //go:build tailscale_go
@ -17,6 +17,9 @@ func init() {
panic("binary built with tailscale_go build tag but failed to read build info or find tailscale.toolchain.rev in build info") panic("binary built with tailscale_go build tag but failed to read build info or find tailscale.toolchain.rev in build info")
} }
want := strings.TrimSpace(GoToolchainRev) want := strings.TrimSpace(GoToolchainRev)
if os.Getenv("TS_GO_NEXT") == "1" {
want = strings.TrimSpace(GoToolchainNextRev)
}
if tsRev != want { if tsRev != want {
if os.Getenv("TS_PERMIT_TOOLCHAIN_MISMATCH") == "1" { if os.Getenv("TS_PERMIT_TOOLCHAIN_MISMATCH") == "1" {
fmt.Fprintf(os.Stderr, "tailscale.toolchain.rev = %q, want %q; but ignoring due to TS_PERMIT_TOOLCHAIN_MISMATCH=1\n", tsRev, want) fmt.Fprintf(os.Stderr, "tailscale.toolchain.rev = %q, want %q; but ignoring due to TS_PERMIT_TOOLCHAIN_MISMATCH=1\n", tsRev, want)

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package atomicfile contains code related to writing to filesystems // Package atomicfile contains code related to writing to filesystems

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build !windows //go:build !windows

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !windows //go:build !js && !windows
@ -31,11 +31,11 @@ func TestDoesNotOverwriteIrregularFiles(t *testing.T) {
// The least troublesome thing to make that is not a file is a unix socket. // The least troublesome thing to make that is not a file is a unix socket.
// Making a null device sadly requires root. // Making a null device sadly requires root.
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"}) ln, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer l.Close() defer ln.Close()
err = WriteFile(path, []byte("hello"), 0644) err = WriteFile(path, []byte("hello"), 0644)
if err == nil { if err == nil {

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package atomicfile package atomicfile

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package atomicfile package atomicfile

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package atomicfile package atomicfile

@ -44,7 +44,7 @@ var (
) )
func replaceFileW(replaced *uint16, replacement *uint16, backup *uint16, flags uint32, exclude unsafe.Pointer, reserved unsafe.Pointer) (err error) { func replaceFileW(replaced *uint16, replacement *uint16, backup *uint16, flags uint32, exclude unsafe.Pointer, reserved unsafe.Pointer) (err error) {
r1, _, e1 := syscall.Syscall6(procReplaceFileW.Addr(), 6, uintptr(unsafe.Pointer(replaced)), uintptr(unsafe.Pointer(replacement)), uintptr(unsafe.Pointer(backup)), uintptr(flags), uintptr(exclude), uintptr(reserved)) r1, _, e1 := syscall.SyscallN(procReplaceFileW.Addr(), uintptr(unsafe.Pointer(replaced)), uintptr(unsafe.Pointer(replacement)), uintptr(unsafe.Pointer(backup)), uintptr(flags), uintptr(exclude), uintptr(reserved))
if int32(r1) == 0 { if int32(r1) == 0 {
err = errnoErr(e1) err = errnoErr(e1)
} }

@ -18,7 +18,7 @@ fi
eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion` eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
if [ "$1" = "shellvars" ]; then if [ "$#" -ge 1 ] && [ "$1" = "shellvars" ]; then
cat <<EOF cat <<EOF
VERSION_MINOR="$VERSION_MINOR" VERSION_MINOR="$VERSION_MINOR"
VERSION_SHORT="$VERSION_SHORT" VERSION_SHORT="$VERSION_SHORT"
@ -41,7 +41,15 @@ while [ "$#" -gt 1 ]; do
fi fi
shift shift
ldflags="$ldflags -w -s" ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture,ts_omit_relayserver,ts_omit_taildrop,ts_omit_tpm" tags="${tags:+$tags,},$(GOOS= GOARCH= $go run ./cmd/featuretags --min --add=osrouter)"
;;
--min)
# --min is like --extra-small but even smaller, removing all features,
# even if it results in a useless binary (e.g. removing both netstack +
# osrouter). It exists for benchmarking purposes only.
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,},$(GOOS= GOARCH= $go run ./cmd/featuretags --min)"
;; ;;
--box) --box)
if [ ! -z "${TAGS:-}" ]; then if [ ! -z "${TAGS:-}" ]; then
@ -57,4 +65,4 @@ while [ "$#" -gt 1 ]; do
esac esac
done done
exec $go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@" exec $go build ${tags:+-tags=$tags} -trimpath -ldflags "$ldflags" "$@"

@ -26,7 +26,7 @@ eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client" DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}" DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.19" DEFAULT_BASE="tailscale/alpine-base:3.22"
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked # Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default # Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
# annotations defined here will be overriden by release scripts that call this script. # annotations defined here will be overriden by release scripts that call this script.
@ -38,6 +38,7 @@ TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}" TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}" BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms PLATFORM="${PLATFORM:-}" # default to all platforms
FILES="${FILES:-}" # default to no extra files
# OCI annotations that will be added to the image. # OCI annotations that will be added to the image.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md # https://github.com/opencontainers/image-spec/blob/main/annotations.md
ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}" ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}"
@ -62,6 +63,7 @@ case "$TARGET" in
--push="${PUSH}" \ --push="${PUSH}" \
--target="${PLATFORM}" \ --target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \ --annotations="${ANNOTATIONS}" \
--files="${FILES}" \
/usr/local/bin/containerboot /usr/local/bin/containerboot
;; ;;
k8s-operator) k8s-operator)
@ -80,6 +82,7 @@ case "$TARGET" in
--push="${PUSH}" \ --push="${PUSH}" \
--target="${PLATFORM}" \ --target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \ --annotations="${ANNOTATIONS}" \
--files="${FILES}" \
/usr/local/bin/operator /usr/local/bin/operator
;; ;;
k8s-nameserver) k8s-nameserver)
@ -98,6 +101,7 @@ case "$TARGET" in
--push="${PUSH}" \ --push="${PUSH}" \
--target="${PLATFORM}" \ --target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \ --annotations="${ANNOTATIONS}" \
--files="${FILES}" \
/usr/local/bin/k8s-nameserver /usr/local/bin/k8s-nameserver
;; ;;
tsidp) tsidp)
@ -116,6 +120,7 @@ case "$TARGET" in
--push="${PUSH}" \ --push="${PUSH}" \
--target="${PLATFORM}" \ --target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \ --annotations="${ANNOTATIONS}" \
--files="${FILES}" \
/usr/local/bin/tsidp /usr/local/bin/tsidp
;; ;;
k8s-proxy) k8s-proxy)
@ -134,6 +139,7 @@ case "$TARGET" in
--push="${PUSH}" \ --push="${PUSH}" \
--target="${PLATFORM}" \ --target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \ --annotations="${ANNOTATIONS}" \
--files="${FILES}" \
/usr/local/bin/k8s-proxy /usr/local/bin/k8s-proxy
;; ;;
*) *)

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package chirp implements a client to communicate with the BIRD Internet // Package chirp implements a client to communicate with the BIRD Internet

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package chirp package chirp
@ -24,7 +24,7 @@ type fakeBIRD struct {
func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD { func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD {
sock := filepath.Join(t.TempDir(), "sock") sock := filepath.Join(t.TempDir(), "sock")
l, err := net.Listen("unix", sock) ln, err := net.Listen("unix", sock)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -33,7 +33,7 @@ func newFakeBIRD(t *testing.T, protocols ...string) *fakeBIRD {
pe[p] = false pe[p] = false
} }
return &fakeBIRD{ return &fakeBIRD{
Listener: l, Listener: ln,
protocolsEnabled: pe, protocolsEnabled: pe,
sock: sock, sock: sock,
} }
@ -123,12 +123,12 @@ type hangingListener struct {
func newHangingListener(t *testing.T) *hangingListener { func newHangingListener(t *testing.T) *hangingListener {
sock := filepath.Join(t.TempDir(), "sock") sock := filepath.Join(t.TempDir(), "sock")
l, err := net.Listen("unix", sock) ln, err := net.Listen("unix", sock)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return &hangingListener{ return &hangingListener{
Listener: l, Listener: ln,
t: t, t: t,
done: make(chan struct{}), done: make(chan struct{}),
sock: sock, sock: sock,

@ -0,0 +1,151 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !ts_omit_acme
package local
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/url"
"strings"
"time"
"go4.org/mem"
)
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use [Client.CertPair].
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return defaultClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return lc.CertPairWithValidity(ctx, domain, 0)
}
// CertPairWithValidity returns a cert and private key for the provided DNS
// domain.
//
// It returns a cached certificate from disk if it's still valid.
// When minValidity is non-zero, the returned certificate will be valid for at
// least the given duration, if permitted by the CA. If the certificate is
// valid, but for less than minValidity, it will be synchronously renewed.
//
// API maturity: this is considered a stable API.
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// [tls.Config.GetCertificate].
//
// Deprecated: use [Client.GetCertificate].
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// [tls.Config.GetCertificate].
//
// API maturity: this is considered a stable API.
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := lc.ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := lc.CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use [Client.ExpandSNIName].
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}

@ -0,0 +1,84 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debugportmapper
package local
import (
"cmp"
"context"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
"strconv"
"time"
"tailscale.com/client/tailscale/apitype"
)
// DebugPortmapOpts contains options for the [Client.DebugPortmap] command.
type DebugPortmapOpts struct {
// Duration is how long the mapping should be created for. It defaults
// to 5 seconds if not set.
Duration time.Duration
// Type is the kind of portmap to debug. The empty string instructs the
// portmap client to perform all known types. Other valid options are
// "pmp", "pcp", and "upnp".
Type string
// GatewayAddr specifies the gateway address used during portmapping.
// If set, SelfAddr must also be set. If unset, it will be
// autodetected.
GatewayAddr netip.Addr
// SelfAddr specifies the gateway address used during portmapping. If
// set, GatewayAddr must also be set. If unset, it will be
// autodetected.
SelfAddr netip.Addr
// LogHTTP instructs the debug-portmap endpoint to print all HTTP
// requests and responses made to the logs.
LogHTTP bool
}
// DebugPortmap invokes the debug-portmap endpoint, and returns an
// io.ReadCloser that can be used to read the logs that are printed during this
// process.
//
// opts can be nil; if so, default values will be used.
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
vals := make(url.Values)
if opts == nil {
opts = &DebugPortmapOpts{}
}
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
vals.Set("type", opts.Type)
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
if opts.GatewayAddr.IsValid() != opts.SelfAddr.IsValid() {
return nil, fmt.Errorf("both GatewayAddr and SelfAddr must be provided if one is")
} else if opts.GatewayAddr.IsValid() {
vals.Set("gateway_and_self", fmt.Sprintf("%s/%s", opts.GatewayAddr, opts.SelfAddr))
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
if err != nil {
return nil, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, nil
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package local contains a Go client for the Tailscale LocalAPI. // Package local contains a Go client for the Tailscale LocalAPI.
@ -9,7 +9,6 @@ import (
"bytes" "bytes"
"cmp" "cmp"
"context" "context"
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
@ -28,22 +27,24 @@ import (
"sync" "sync"
"time" "time"
"go4.org/mem"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/drive" "tailscale.com/drive"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/net/udprelay/status"
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/syncs"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka" "tailscale.com/types/appctype"
"tailscale.com/types/dnstype" "tailscale.com/types/dnstype"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/tkatype" "tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus" "tailscale.com/util/eventbus"
"tailscale.com/util/syspolicy/setting"
) )
// defaultClient is the default Client when using the legacy // defaultClient is the default Client when using the legacy
@ -382,18 +383,17 @@ func (lc *Client) UserMetrics(ctx context.Context) ([]byte, error) {
// //
// IncrementCounter does not support gauge metrics or negative delta values. // IncrementCounter does not support gauge metrics or negative delta values.
func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) error { func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) error {
type metricUpdate struct { if !buildfeatures.HasClientMetrics {
Name string `json:"name"` return nil
Type string `json:"type"`
Value int `json:"value"` // amount to increment by
} }
if delta < 0 { if delta < 0 {
return errors.New("negative delta not allowed") return errors.New("negative delta not allowed")
} }
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]metricUpdate{{ _, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name: name, Name: name,
Type: "counter", Type: "counter",
Value: delta, Value: delta,
Op: "add",
}})) }}))
return err return err
} }
@ -402,15 +402,23 @@ func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int)
// metric by the given delta. If the metric has yet to exist, a new gauge // metric by the given delta. If the metric has yet to exist, a new gauge
// metric is created and initialized to delta. The delta value can be negative. // metric is created and initialized to delta. The delta value can be negative.
func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error { func (lc *Client) IncrementGauge(ctx context.Context, name string, delta int) error {
type metricUpdate struct { _, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name string `json:"name"`
Type string `json:"type"`
Value int `json:"value"` // amount to increment by
}
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]metricUpdate{{
Name: name, Name: name,
Type: "gauge", Type: "gauge",
Value: delta, Value: delta,
Op: "add",
}}))
return err
}
// SetGauge sets the value of a Tailscale daemon's gauge metric to the given value.
// If the metric has yet to exist, a new gauge metric is created and initialized to value.
func (lc *Client) SetGauge(ctx context.Context, name string, value int) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/upload-client-metrics", 200, jsonBody([]clientmetric.MetricUpdate{{
Name: name,
Type: "gauge",
Value: value,
Op: "set",
}})) }}))
return err return err
} }
@ -594,68 +602,17 @@ func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, erro
return x, nil return x, nil
} }
// DebugPortmapOpts contains options for the [Client.DebugPortmap] command. // QueryOptionalFeatures queries the optional features supported by the Tailscale daemon.
type DebugPortmapOpts struct { func (lc *Client) QueryOptionalFeatures(ctx context.Context) (*apitype.OptionalFeatures, error) {
// Duration is how long the mapping should be created for. It defaults body, err := lc.send(ctx, "POST", "/localapi/v0/debug-optional-features", 200, nil)
// to 5 seconds if not set.
Duration time.Duration
// Type is the kind of portmap to debug. The empty string instructs the
// portmap client to perform all known types. Other valid options are
// "pmp", "pcp", and "upnp".
Type string
// GatewayAddr specifies the gateway address used during portmapping.
// If set, SelfAddr must also be set. If unset, it will be
// autodetected.
GatewayAddr netip.Addr
// SelfAddr specifies the gateway address used during portmapping. If
// set, GatewayAddr must also be set. If unset, it will be
// autodetected.
SelfAddr netip.Addr
// LogHTTP instructs the debug-portmap endpoint to print all HTTP
// requests and responses made to the logs.
LogHTTP bool
}
// DebugPortmap invokes the debug-portmap endpoint, and returns an
// io.ReadCloser that can be used to read the logs that are printed during this
// process.
//
// opts can be nil; if so, default values will be used.
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
vals := make(url.Values)
if opts == nil {
opts = &DebugPortmapOpts{}
}
vals.Set("duration", cmp.Or(opts.Duration, 5*time.Second).String())
vals.Set("type", opts.Type)
vals.Set("log_http", strconv.FormatBool(opts.LogHTTP))
if opts.GatewayAddr.IsValid() != opts.SelfAddr.IsValid() {
return nil, fmt.Errorf("both GatewayAddr and SelfAddr must be provided if one is")
} else if opts.GatewayAddr.IsValid() {
vals.Set("gateway_and_self", fmt.Sprintf("%s/%s", opts.GatewayAddr, opts.SelfAddr))
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("error %w: %s", err, body)
} }
res, err := lc.doLocalRequestNiceError(req) var x apitype.OptionalFeatures
if err != nil { if err := json.Unmarshal(body, &x); err != nil {
return nil, err return nil, err
} }
if res.StatusCode != 200 { return &x, nil
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, nil
} }
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development. // SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
@ -675,6 +632,9 @@ func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) er
// the provided duration. If the duration is in the past, the debug logging // the provided duration. If the duration is in the past, the debug logging
// is disabled. // is disabled.
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error { func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
if !buildfeatures.HasDebug {
return feature.ErrUnavailable
}
body, err := lc.send(ctx, "POST", body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d", fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil) url.QueryEscape(component), int64(d.Seconds())), 200, nil)
@ -814,6 +774,9 @@ func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, siz
// machine is properly configured to forward IP packets as a subnet router // machine is properly configured to forward IP packets as a subnet router
// or exit node. // or exit node.
func (lc *Client) CheckIPForwarding(ctx context.Context) error { func (lc *Client) CheckIPForwarding(ctx context.Context) error {
if !buildfeatures.HasAdvertiseRoutes {
return nil
}
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding") body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil { if err != nil {
return err return err
@ -926,36 +889,12 @@ func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Pref
return decodeJSON[*ipn.Prefs](body) return decodeJSON[*ipn.Prefs](body)
} }
// GetEffectivePolicy returns the effective policy for the specified scope.
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// ReloadEffectivePolicy reloads the effective policy for the specified scope
// by reading and merging policy settings from all applicable policy sources.
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// GetDNSOSConfig returns the system DNS configuration for the current device. // GetDNSOSConfig returns the system DNS configuration for the current device.
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used. // That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) { func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
if !buildfeatures.HasDNS {
return nil, feature.ErrUnavailable
}
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig") body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
if err != nil { if err != nil {
return nil, err return nil, err
@ -971,6 +910,9 @@ func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, err
// It returns the raw DNS response bytes and the resolvers that were used to answer the query // It returns the raw DNS response bytes and the resolvers that were used to answer the query
// (often just one, but can be more if we raced multiple resolvers). // (often just one, but can be more if we raced multiple resolvers).
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) { func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
if !buildfeatures.HasDNS {
return nil, nil, feature.ErrUnavailable
}
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType)) body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -1001,28 +943,6 @@ func (lc *Client) Logout(ctx context.Context) error {
return err return err
} }
// SetDNS adds a DNS TXT record for the given domain name, containing
// the provided TXT value. The intended use case is answering
// LetsEncrypt/ACME dns-01 challenges.
//
// The control plane will only permit SetDNS requests with very
// specific names and values. The name should be
// "_acme-challenge." + your node's MagicDNS name. It's expected that
// clients cache the certs from LetsEncrypt (or whichever CA is
// providing them) and only request new ones as needed; the control plane
// rate limits SetDNS requests.
//
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
_, err := lc.send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil)
return err
}
// DialTCP connects to the host's port via Tailscale. // DialTCP connects to the host's port via Tailscale.
// //
// The host may be a base DNS name (resolved from the netmap inside // The host may be a base DNS name (resolved from the netmap inside
@ -1103,117 +1023,6 @@ func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error)
return &derpMap, nil return &derpMap, nil
} }
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use [Client.CertPair].
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return defaultClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
//
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return lc.CertPairWithValidity(ctx, domain, 0)
}
// CertPairWithValidity returns a cert and private key for the provided DNS
// domain.
//
// It returns a cached certificate from disk if it's still valid.
// When minValidity is non-zero, the returned certificate will be valid for at
// least the given duration, if permitted by the CA. If the certificate is
// valid, but for less than minValidity, it will be synchronously renewed.
//
// API maturity: this is considered a stable API.
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
if err != nil {
return nil, nil, err
}
// with ?type=pair, the response PEM is first the one private
// key PEM block, then the cert PEM blocks.
i := mem.Index(mem.B(res), mem.S("--\n--"))
if i == -1 {
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
}
i += len("--\n")
keyPEM, certPEM = res[:i], res[i:]
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
return nil, nil, fmt.Errorf("unexpected output: key in cert")
}
return certPEM, keyPEM, nil
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// [tls.Config.GetCertificate].
//
// Deprecated: use [Client.GetCertificate].
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
//
// It returns a cached certificate from disk if it's still valid.
//
// It's the right signature to use as the value of
// [tls.Config.GetCertificate].
//
// API maturity: this is considered a stable API.
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
name := hi.ServerName
if !strings.Contains(name, ".") {
if v, ok := lc.ExpandSNIName(ctx, name); ok {
name = v
}
}
certPEM, keyPEM, err := lc.CertPair(ctx, name)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &cert, nil
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use [Client.ExpandSNIName].
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", false
}
for _, d := range st.CertDomains {
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
return d, true
}
}
return "", false
}
// PingOpts contains options for the ping request. // PingOpts contains options for the ping request.
// //
// The zero value is valid, which means to use defaults. // The zero value is valid, which means to use defaults.
@ -1247,197 +1056,6 @@ func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.Ping
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{}) return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
} }
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockInit initializes the tailnet key authority.
//
// TODO(tom): Plumb through disablement secrets.
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type initRequest struct {
Keys []tka.Key
DisablementValues [][]byte
SupportDisablement []byte
}
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
// enable unattended bringup in the locked tailnet.
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
}
var b bytes.Buffer
type wrapRequest struct {
TSKey string
TKAKey string // key.NLPrivate.MarshalText
}
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
return "", err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
if err != nil {
return "", fmt.Errorf("error: %w", err)
}
return string(body), nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[[]tkatype.MarshaledSignature](body)
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
v.Set("limit", fmt.Sprint(maxEntries))
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
}
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
// This endpoint expects an empty JSON stanza as the payload.
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
}
return decodeJSON[*tka.DeeplinkValidationResult](body)
}
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
}{removeKeys, forkFrom.String()}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
r := bytes.NewReader(aum.Serialize())
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
if err != nil {
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
r := bytes.NewReader(aum.Serialize())
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
if err != nil {
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return nil
}
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
}
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
return nil
}
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be // DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over // run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
// to another replica whilst there is still some grace period for the existing connections to terminate. // to another replica whilst there is still some grace period for the existing connections to terminate.
@ -1449,40 +1067,6 @@ func (lc *Client) DisconnectControl(ctx context.Context) error {
return nil return nil
} }
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
sc, err := getServeConfigFromJSON(body)
if err != nil {
return nil, err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
sc.ETag = h.Get("Etag")
return sc, nil
}
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
if err := json.Unmarshal(body, &sc); err != nil {
return nil, err
}
return sc, nil
}
// tailscaledConnectHint gives a little thing about why tailscaled (or // tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections. // platform equivalent) is not answering localapi connections.
// //
@ -1638,6 +1222,16 @@ func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
return err return err
} }
// DebugPeerRelaySessions returns debug information about the current peer
// relay sessions running through this node.
func (lc *Client) DebugPeerRelaySessions(ctx context.Context) (*status.ServerStatus, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/debug-peer-relay-sessions", 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[*status.ServerStatus](body)
}
// StreamDebugCapture streams a pcap-formatted packet capture. // StreamDebugCapture streams a pcap-formatted packet capture.
// //
// The provided context does not determine the lifetime of the // The provided context does not determine the lifetime of the
@ -1775,7 +1369,7 @@ type IPNBusWatcher struct {
httpRes *http.Response httpRes *http.Response
dec *json.Decoder dec *json.Decoder
mu sync.Mutex mu syncs.Mutex
closed bool closed bool
} }
@ -1811,3 +1405,34 @@ func (lc *Client) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggesti
} }
return decodeJSON[apitype.ExitNodeSuggestionResponse](body) return decodeJSON[apitype.ExitNodeSuggestionResponse](body)
} }
// CheckSOMarkInUse reports whether the socket mark option is in use. This will only
// be true if tailscale is running on Linux and tailscaled uses SO_MARK.
func (lc *Client) CheckSOMarkInUse(ctx context.Context) (bool, error) {
body, err := lc.get200(ctx, "/localapi/v0/check-so-mark-in-use")
if err != nil {
return false, err
}
var res struct {
UseSOMark bool `json:"useSoMark"`
}
if err := json.Unmarshal(body, &res); err != nil {
return false, fmt.Errorf("invalid JSON from check-so-mark-in-use: %w", err)
}
return res.UseSOMark, nil
}
// ShutdownTailscaled requests a graceful shutdown of tailscaled.
func (lc *Client) ShutdownTailscaled(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/shutdown", 200, nil)
return err
}
func (lc *Client) GetAppConnectorRouteInfo(ctx context.Context) (appctype.RouteInfo, error) {
body, err := lc.get200(ctx, "/localapi/v0/appc-route-info")
if err != nil {
return appctype.RouteInfo{}, err
}
return decodeJSON[appctype.RouteInfo](body)
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -0,0 +1,55 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_serve
package local
import (
"context"
"encoding/json"
"fmt"
"net/http"
"tailscale.com/ipn"
)
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
}
sc, err := getServeConfigFromJSON(body)
if err != nil {
return nil, err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
sc.ETag = h.Get("Etag")
return sc, nil
}
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
if err := json.Unmarshal(body, &sc); err != nil {
return nil, err
}
return sc, nil
}
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
}
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
if err != nil {
return fmt.Errorf("sending serve config: %w", err)
}
return nil
}

@ -0,0 +1,40 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_syspolicy
package local
import (
"context"
"net/http"
"tailscale.com/util/syspolicy/setting"
)
// GetEffectivePolicy returns the effective policy for the specified scope.
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// ReloadEffectivePolicy reloads the effective policy for the specified scope
// by reading and merging policy settings from all applicable policy sources.
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}

@ -0,0 +1,204 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
package local
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockInit initializes the tailnet key authority.
//
// TODO(tom): Plumb through disablement secrets.
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type initRequest struct {
Keys []tka.Key
DisablementValues [][]byte
SupportDisablement []byte
}
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
// enable unattended bringup in the locked tailnet.
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
}
var b bytes.Buffer
type wrapRequest struct {
TSKey string
TKAKey string // key.NLPrivate.MarshalText
}
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
return "", err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
if err != nil {
return "", fmt.Errorf("error: %w", err)
}
return string(body), nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
RemoveKeys []tka.Key
}
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
if err != nil {
return nil, fmt.Errorf("error: %w", err)
}
return decodeJSON[[]tkatype.MarshaledSignature](body)
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
v.Set("limit", fmt.Sprint(maxEntries))
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[[]ipnstate.NetworkLockUpdate](body)
}
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
// This endpoint expects an empty JSON stanza as the payload.
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
}
return decodeJSON[*tka.DeeplinkValidationResult](body)
}
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
}{removeKeys, forkFrom.String()}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
r := bytes.NewReader(aum.Serialize())
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
if err != nil {
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
r := bytes.NewReader(aum.Serialize())
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
if err != nil {
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build cgo || !darwin //go:build cgo || !darwin

@ -0,0 +1,76 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build cgo || !darwin
// Package systray provides a minimal Tailscale systray application.
package systray
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
//go:embed tailscale-systray.service
var embedSystemd string
func InstallStartupScript(initSystem string) error {
switch initSystem {
case "systemd":
return installSystemd()
default:
return fmt.Errorf("unsupported init system '%s'", initSystem)
}
}
func installSystemd() error {
// Find the path to tailscale, just in case it's not where the example file
// has it placed, and replace that before writing the file.
tailscaleBin, err := exec.LookPath("tailscale")
if err != nil {
return fmt.Errorf("failed to find tailscale binary %w", err)
}
var output bytes.Buffer
scanner := bufio.NewScanner(strings.NewReader(embedSystemd))
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "ExecStart=") {
line = fmt.Sprintf("ExecStart=%s systray", tailscaleBin)
}
output.WriteString(line + "\n")
}
configDir, err := os.UserConfigDir()
if err != nil {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("unable to locate user home: %w", err)
}
configDir = filepath.Join(homeDir, ".config")
}
systemdDir := filepath.Join(configDir, "systemd", "user")
if err := os.MkdirAll(systemdDir, 0o755); err != nil {
return fmt.Errorf("failed creating systemd uuser dir: %w", err)
}
serviceFile := filepath.Join(systemdDir, "tailscale-systray.service")
if err := os.WriteFile(serviceFile, output.Bytes(), 0o755); err != nil {
return fmt.Errorf("failed writing systemd user service: %w", err)
}
fmt.Printf("Successfully installed systemd service to: %s\n", serviceFile)
fmt.Println("To enable and start the service, run:")
fmt.Println(" systemctl --user daemon-reload")
fmt.Println(" systemctl --user enable --now tailscale-systray")
return nil
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build cgo || !darwin //go:build cgo || !darwin
@ -48,7 +48,12 @@ var (
) )
// Run starts the systray menu and blocks until the menu exits. // Run starts the systray menu and blocks until the menu exits.
func (menu *Menu) Run() { // If client is nil, a default local.Client is used.
func (menu *Menu) Run(client *local.Client) {
if client == nil {
client = &local.Client{}
}
menu.lc = client
menu.updateState() menu.updateState()
// exit cleanly on SIGINT and SIGTERM // exit cleanly on SIGINT and SIGTERM
@ -61,8 +66,8 @@ func (menu *Menu) Run() {
case <-menu.bgCtx.Done(): case <-menu.bgCtx.Done():
} }
}() }()
go menu.lc.IncrementGauge(menu.bgCtx, "systray_running", 1) go menu.lc.SetGauge(menu.bgCtx, "systray_running", 1)
defer menu.lc.IncrementGauge(menu.bgCtx, "systray_running", -1) defer menu.lc.SetGauge(menu.bgCtx, "systray_running", 0)
systray.Run(menu.onReady, menu.onExit) systray.Run(menu.onReady, menu.onExit)
} }
@ -71,7 +76,7 @@ func (menu *Menu) Run() {
type Menu struct { type Menu struct {
mu sync.Mutex // protects the entire Menu mu sync.Mutex // protects the entire Menu
lc local.Client lc *local.Client
status *ipnstate.Status status *ipnstate.Status
curProfile ipn.LoginProfile curProfile ipn.LoginProfile
allProfiles []ipn.LoginProfile allProfiles []ipn.LoginProfile
@ -153,8 +158,31 @@ func init() {
// onReady is called by the systray package when the menu is ready to be built. // onReady is called by the systray package when the menu is ready to be built.
func (menu *Menu) onReady() { func (menu *Menu) onReady() {
log.Printf("starting") log.Printf("starting")
if os.Getuid() == 0 || os.Getuid() != os.Geteuid() || os.Getenv("SUDO_USER") != "" || os.Getenv("DOAS_USER") != "" {
fmt.Fprintln(os.Stderr, `
It appears that you might be running the systray with sudo/doas.
This can lead to issues with D-Bus, and should be avoided.
The systray application should be run with the same user as your desktop session.
This usually means that you should run the application like:
tailscale systray
See https://tailscale.com/kb/1597/linux-systray for more information.`)
}
setAppIcon(disconnected) setAppIcon(disconnected)
menu.rebuild() menu.rebuild()
menu.mu.Lock()
if menu.readonly {
fmt.Fprintln(os.Stderr, `
No permission to manage Tailscale. Set operator by running:
sudo tailscale set --operator=$USER
See https://tailscale.com/s/cli-operator for more information.`)
}
menu.mu.Unlock()
} }
// updateState updates the Menu state from the Tailscale local client. // updateState updates the Menu state from the Tailscale local client.
@ -290,11 +318,14 @@ func (menu *Menu) rebuild() {
menu.rebuildExitNodeMenu(ctx) menu.rebuildExitNodeMenu(ctx)
} }
if menu.status != nil { menu.more = systray.AddMenuItem("More settings", "")
menu.more = systray.AddMenuItem("More settings", "") if menu.status != nil && menu.status.BackendState == "Running" {
// web client is only available if backend is running
onClick(ctx, menu.more, func(_ context.Context) { onClick(ctx, menu.more, func(_ context.Context) {
webbrowser.Open("http://100.100.100.100/") webbrowser.Open("http://100.100.100.100/")
}) })
} else {
menu.more.Disable()
} }
// TODO(#15528): this menu item shouldn't be necessary at all, // TODO(#15528): this menu item shouldn't be necessary at all,
@ -320,9 +351,9 @@ func profileTitle(profile ipn.LoginProfile) string {
if profile.NetworkProfile.DomainName != "" { if profile.NetworkProfile.DomainName != "" {
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
// windows and mac don't support multi-line menu // windows and mac don't support multi-line menu
title += " (" + profile.NetworkProfile.DomainName + ")" title += " (" + profile.NetworkProfile.DisplayNameOrDefault() + ")"
} else { } else {
title += "\n" + profile.NetworkProfile.DomainName title += "\n" + profile.NetworkProfile.DisplayNameOrDefault()
} }
} }
return title return title
@ -341,6 +372,7 @@ func setRemoteIcon(menu *systray.MenuItem, urlStr string) {
} }
cacheMu.Lock() cacheMu.Lock()
defer cacheMu.Unlock()
b, ok := httpCache[urlStr] b, ok := httpCache[urlStr]
if !ok { if !ok {
resp, err := http.Get(urlStr) resp, err := http.Get(urlStr)
@ -364,7 +396,6 @@ func setRemoteIcon(menu *systray.MenuItem, urlStr string) {
resp.Body.Close() resp.Body.Close()
} }
} }
cacheMu.Unlock()
if len(b) > 0 { if len(b) > 0 {
menu.SetIcon(b) menu.SetIcon(b)
@ -481,7 +512,7 @@ func (menu *Menu) watchIPNBus() {
} }
func (menu *Menu) watchIPNBusInner() error { func (menu *Menu) watchIPNBusInner() error {
watcher, err := menu.lc.WatchIPNBus(menu.bgCtx, ipn.NotifyNoPrivateKeys) watcher, err := menu.lc.WatchIPNBus(menu.bgCtx, 0)
if err != nil { if err != nil {
return fmt.Errorf("watching ipn bus: %w", err) return fmt.Errorf("watching ipn bus: %w", err)
} }
@ -521,9 +552,9 @@ func (menu *Menu) copyTailscaleIP(device *ipnstate.PeerStatus) {
err := clipboard.WriteAll(ip) err := clipboard.WriteAll(ip)
if err != nil { if err != nil {
log.Printf("clipboard error: %v", err) log.Printf("clipboard error: %v", err)
} else {
menu.sendNotification(fmt.Sprintf("Copied Address for %v", name), ip)
} }
menu.sendNotification(fmt.Sprintf("Copied Address for %v", name), ip)
} }
// sendNotification sends a desktop notification with the given title and content. // sendNotification sends a desktop notification with the given title and content.

@ -0,0 +1,10 @@
[Unit]
Description=Tailscale System Tray
After=graphical.target
[Service]
Type=simple
ExecStart=/usr/bin/tailscale systray
[Install]
WantedBy=default.target

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// Package apitype contains types for the Tailscale LocalAPI and control plane API. // Package apitype contains types for the Tailscale LocalAPI and control plane API.
@ -94,3 +94,13 @@ type DNSQueryResponse struct {
// Resolvers is the list of resolvers that the forwarder deemed able to resolve the query. // Resolvers is the list of resolvers that the forwarder deemed able to resolve the query.
Resolvers []*dnstype.Resolver Resolvers []*dnstype.Resolver
} }
// OptionalFeatures describes which optional features are enabled in the build.
type OptionalFeatures struct {
// Features is the map of optional feature names to whether they are
// enabled.
//
// Disabled features may be absent from the map. (That is, false values
// are not guaranteed to be present.)
Features map[string]bool
}

@ -1,19 +1,52 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package apitype package apitype
// DNSConfig is the DNS configuration for a tailnet
// used in /tailnet/{tailnet}/dns/config.
type DNSConfig struct { type DNSConfig struct {
Resolvers []DNSResolver `json:"resolvers"` // Resolvers are the global DNS resolvers to use
FallbackResolvers []DNSResolver `json:"fallbackResolvers"` // overriding the local OS configuration.
Routes map[string][]DNSResolver `json:"routes"` Resolvers []DNSResolver `json:"resolvers"`
Domains []string `json:"domains"`
Nameservers []string `json:"nameservers"` // FallbackResolvers are used as global resolvers when
Proxied bool `json:"proxied"` // the client is unable to determine the OS's preferred DNS servers.
TempCorpIssue13969 string `json:"TempCorpIssue13969,omitempty"` FallbackResolvers []DNSResolver `json:"fallbackResolvers"`
// Routes map DNS name suffixes to a set of DNS resolvers,
// used for Split DNS and other advanced routing overlays.
Routes map[string][]DNSResolver `json:"routes"`
// Domains are the search domains to use.
Domains []string `json:"domains"`
// Proxied means MagicDNS is enabled.
Proxied bool `json:"proxied"`
// TempCorpIssue13969 is from an internal hack day prototype,
// See tailscale/corp#13969.
TempCorpIssue13969 string `json:"TempCorpIssue13969,omitempty"`
// Nameservers are the IP addresses of global nameservers to use.
// This is a deprecated format but may still be found in tailnets
// that were configured a long time ago. When making updates,
// set Resolvers and leave Nameservers empty.
Nameservers []string `json:"nameservers"`
} }
// DNSResolver is a DNS resolver in a DNS configuration.
type DNSResolver struct { type DNSResolver struct {
Addr string `json:"addr"` // Addr is the address of the DNS resolver.
// It is usually an IP address or a DoH URL.
// See dnstype.Resolver.Addr for full details.
Addr string `json:"addr"`
// BootstrapResolution is an optional suggested resolution for
// the DoT/DoH resolver.
BootstrapResolution []string `json:"bootstrapResolution,omitempty"` BootstrapResolution []string `json:"bootstrapResolution,omitempty"`
// UseWithExitNode signals this resolver should be used
// even when a tailscale exit node is configured on a device.
UseWithExitNode bool `json:"useWithExitNode,omitempty"`
} }

@ -0,0 +1,34 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !ts_omit_acme
package tailscale
import (
"context"
"crypto/tls"
"tailscale.com/client/local"
)
// GetCertificate is an alias for [tailscale.com/client/local.GetCertificate].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.GetCertificate].
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return local.GetCertificate(hi)
}
// CertPair is an alias for [tailscale.com/client/local.CertPair].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.CertPair].
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return local.CertPair(ctx, domain)
}
// ExpandSNIName is an alias for [tailscale.com/client/local.ExpandSNIName].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.ExpandSNIName].
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return local.ExpandSNIName(ctx, name)
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// The servetls program shows how to run an HTTPS server // The servetls program shows how to run an HTTPS server

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package tailscale package tailscale

@ -1,11 +1,10 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package tailscale package tailscale
import ( import (
"context" "context"
"crypto/tls"
"tailscale.com/client/local" "tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
@ -32,23 +31,11 @@ type IPNBusWatcher = local.IPNBusWatcher
// Deprecated: import [tailscale.com/client/local] instead. // Deprecated: import [tailscale.com/client/local] instead.
type BugReportOpts = local.BugReportOpts type BugReportOpts = local.BugReportOpts
// DebugPortmapOpts is an alias for [tailscale.com/client/local.DebugPortmapOpts].
//
// Deprecated: import [tailscale.com/client/local] instead.
type DebugPortmapOpts = local.DebugPortmapOpts
// PingOpts is an alias for [tailscale.com/client/local.PingOpts]. // PingOpts is an alias for [tailscale.com/client/local.PingOpts].
// //
// Deprecated: import [tailscale.com/client/local] instead. // Deprecated: import [tailscale.com/client/local] instead.
type PingOpts = local.PingOpts type PingOpts = local.PingOpts
// GetCertificate is an alias for [tailscale.com/client/local.GetCertificate].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.GetCertificate].
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return local.GetCertificate(hi)
}
// SetVersionMismatchHandler is an alias for [tailscale.com/client/local.SetVersionMismatchHandler]. // SetVersionMismatchHandler is an alias for [tailscale.com/client/local.SetVersionMismatchHandler].
// //
// Deprecated: import [tailscale.com/client/local] instead. // Deprecated: import [tailscale.com/client/local] instead.
@ -90,17 +77,3 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) { func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return local.StatusWithoutPeers(ctx) return local.StatusWithoutPeers(ctx)
} }
// CertPair is an alias for [tailscale.com/client/local.CertPair].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.CertPair].
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return local.CertPair(ctx, domain)
}
// ExpandSNIName is an alias for [tailscale.com/client/local.ExpandSNIName].
//
// Deprecated: import [tailscale.com/client/local] instead and use [local.Client.ExpandSNIName].
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return local.ExpandSNIName(ctx, name)
}

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build !go1.23 //go:build !go1.23

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19 //go:build go1.19

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package tailscale package tailscale

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package web package web

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package web package web
@ -192,7 +192,7 @@ func (s *Server) controlSupportsCheckMode(ctx context.Context) bool {
if err != nil { if err != nil {
return true return true
} }
controlURL, err := url.Parse(prefs.ControlURLOrDefault()) controlURL, err := url.Parse(prefs.ControlURLOrDefault(s.polc))
if err != nil { if err != nil {
return true return true
} }

@ -34,10 +34,10 @@
"prettier-plugin-organize-imports": "^3.2.2", "prettier-plugin-organize-imports": "^3.2.2",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.7", "vite": "^5.4.21",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^3.5.0", "vite-tsconfig-paths": "^3.5.0",
"vitest": "^1.3.1" "vitest": "^1.6.1"
}, },
"resolutions": { "resolutions": {
"@typescript-eslint/eslint-plugin": "^6.2.1", "@typescript-eslint/eslint-plugin": "^6.2.1",

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// qnap.go contains handlers and logic, such as authentication, // qnap.go contains handlers and logic, such as authentication,

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import { useCallback } from "react" import { useCallback } from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import * as Primitive from "@radix-ui/react-popover" import * as Primitive from "@radix-ui/react-popover"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames" import cx from "classnames"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import React from "react" import React from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"

@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
import { useMemo } from "react" import { useMemo } from "react"
@ -66,7 +66,7 @@ export default function useExitNodes(node: NodeData, filter?: string) {
// match from a list of exit node `options` to `nodes`. // match from a list of exit node `options` to `nodes`.
const addBestMatchNode = ( const addBestMatchNode = (
options: ExitNode[], options: ExitNode[],
name: (l: ExitNodeLocation) => string name: (loc: ExitNodeLocation) => string
) => { ) => {
const bestNode = highestPriorityNode(options) const bestNode = highestPriorityNode(options)
if (!bestNode || !bestNode.Location) { if (!bestNode || !bestNode.Location) {
@ -86,7 +86,7 @@ export default function useExitNodes(node: NodeData, filter?: string) {
locationNodesMap.forEach( locationNodesMap.forEach(
// add one node per country // add one node per country
(countryNodes) => (countryNodes) =>
addBestMatchNode(flattenMap(countryNodes), (l) => l.Country) addBestMatchNode(flattenMap(countryNodes), (loc) => loc.Country)
) )
} else { } else {
// Otherwise, show the best match on a city-level, // Otherwise, show the best match on a city-level,
@ -97,12 +97,12 @@ export default function useExitNodes(node: NodeData, filter?: string) {
countryNodes.forEach( countryNodes.forEach(
// add one node per city // add one node per city
(cityNodes) => (cityNodes) =>
addBestMatchNode(cityNodes, (l) => `${l.Country}: ${l.City}`) addBestMatchNode(cityNodes, (loc) => `${loc.Country}: ${loc.City}`)
) )
// add the "Country: Best Match" node // add the "Country: Best Match" node
addBestMatchNode( addBestMatchNode(
flattenMap(countryNodes), flattenMap(countryNodes),
(l) => `${l.Country}: Best Match` (loc) => `${loc.Country}: Best Match`
) )
}) })
} }

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save