Compare commits

...

241 Commits

Author SHA1 Message Date
Andrew Dunham
089d15d8c2 net/dns/resolver: wrap errors with more context
To aid in debugging why we're seeing DNS resolution errors.

Updates #TODO

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9b3f438d5d675f757e9043dbbdc413fd722fb81a
2024-04-17 10:35:49 -04:00
Andrew Dunham
91692495d8 net/dns/resolver: use SystemDial in DoH forwarder
This ensures that we close the underlying connection(s) when a major
link change happens. If we don't do this, on mobile platforms switching
between WiFi and cellular can result in leftover connections in the
http.Client's connection pool which are bound to the "wrong" interface.

Updates #10821
Updates tailscale/corp#19124

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ibd51ce2efcaf4bd68e14f6fdeded61d4e99f9a01
2024-04-17 10:35:40 -04:00
Andrew Dunham
226486eb9a net/interfaces: handle removed interfaces in State.Equal
This wasn't previously handling the case where an interface in s2 was
removed and not present in s1, and would cause the Equal method to
incorrectly return that the states were equal.

Updates tailscale/corp#19124

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I3af22bc631015d1ddd0a1d01bfdf312161b9532d
2024-04-17 10:34:40 -04:00
Paul Scott
454a03a766 cmd/tailscale/cli: prepend "tailscale" to usage errors
Updates #11626

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Paul Scott
d07ede461a cmd/tailscale/cli: fix "subcommand required" errors when typod
Fixes #11672

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Paul Scott
3ff3445e9d cmd/tailscale/cli: improve ShortHelp/ShortUsage unit test, fix new errors
Updates #11364

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Paul Scott
eb34b8a173 cmd/tailscale/cli: remove explicit usageFunc - its default
Updates #cleanup

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Paul Scott
a50e4e604e cmd/tailscale/cli: remove duplicate "tailscale " in drive subcmd usage
Updates #cleanup

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Paul Scott
62d4be873d cmd/tailscale/cli: fix drive --help usage identation
Updates #cleanup

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-17 09:25:34 +01:00
Brad Fitzpatrick
7c1d6e35a5 all: use Go 1.22 range-over-int
Updates #11058

Change-Id: I35e7ef9b90e83cac04ca93fd964ad00ed5b48430
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-16 15:32:38 -07:00
Brad Fitzpatrick
068db1f972 net/interfaces: delete unused unexported function
It should've been deleted in 11ece02f52.

Updates #9040

Change-Id: If8a136bdb6c82804af658c9d2b0a8c63ce02d509
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-16 15:19:33 -07:00
Jonathan Nobels
7e2b4268d6 ipn/{localapi, ipnlocal}: forget the prior exit node when localAPI is used to zero the ExitNodeID (#11681)
Updates tailscale/corp#18724

When localAPI clients directly set ExitNodeID to "", the expected behaviour is that the prior exit node also gets zero'd - effectively setting the UI state back to 'no exit node was ever selected'

The IntenalExitNodePrior has been changed to be a non-opaque type, as it is read by the UI to render the users last selected exit node, and must be concrete. Future-us can either break this, or deprecate it and replace it with something more interesting.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-04-16 14:53:56 -04:00
Brad Fitzpatrick
0fba9e7570 cmd/tailscale/cli: prevent concurrent Start calls in 'up'
Seems to deflake tstest/integration tests. I can't reproduce it
anymore on one of my VMs that was consistently flaking after a dozen
runs before. Now I can run hundreds of times.

Updates #11649
Fixes #7036

Change-Id: I2f7d4ae97500d507bdd78af9e92cd1242e8e44b8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-16 10:03:53 -07:00
Irbe Krumina
26f9bbc02b cmd/k8s-operator,k8s-operator: document tailscale.com Custom Resource Definitions better. (#11665)
Updates tailscale/tailscale#10880

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-16 17:52:10 +01:00
Adrian Dewhurst
ca5cb41b43 tailcfg: document use of CapMap for peers
Updates tailscale/corp#17516
Updates #11508

Change-Id: Iad2dafb38ffb9948bc2f3dfaf9c268f7d772cf56
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-04-16 11:18:29 -04:00
Brad Fitzpatrick
3c1e2bba5b ipn/ipnlocal: remove outdated iOS hacky workaround in Start
We haven't needed this hack for quite some time Andrea says.

Updates #11649

Change-Id: Ie854b7edd0a01e92495669daa466c7c0d57e7438
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15 22:32:30 -07:00
Brad Fitzpatrick
dd6c76ea24 ipn: remove unused Options.LegacyMigrationPrefs
I'm on a mission to simplify LocalBackend.Start and its locking
and deflake some tests.

I noticed this hasn't been used since March 2023 when it was removed
from the Windows client in corp 66be796d33c.

So, delete.

Updates #11649

Change-Id: I40f2cb75fb3f43baf23558007655f65a8ec5e1b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15 22:13:53 -07:00
Brad Fitzpatrick
7ec0dc3834 ipn/ipnlocal: make StartLoginInteractive take (yet unused) context
In prep for future fix to undermentioned issue.

Updates tailscale/tailscale#7036

Change-Id: Ide114db917dcba43719482ffded6a9a54630d99e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15 15:23:48 -07:00
Claire Wang
9171b217ba cmd/tailscale, ipn/ipnlocal: add suggest exit node CLI option (#11407)
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-04-15 18:14:20 -04:00
Charlotte Brandhorst-Satzkorn
449f46c207 wgengine/magicsock: rebind/restun if a syscall.EPERM error is returned (#11711)
We have seen in macOS client logs that the "operation not permitted", a
syscall.EPERM error, is being returned when traffic is attempted to be
sent. This may be caused by security software on the client.

This change will perform a rebind and restun if we receive a
syscall.EPERM error on clients running darwin. Rebinds will only be
called if we haven't performed one specifically for an EPERM error in
the past 5 seconds.

Updates #11710

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-04-15 13:57:55 -07:00
Will Norris
14c8b674ea Revert "licenses: add gliderlabs/ssh license"
The gliderlabs/ssh license is actually already included in the standard
package listing.  I'm not sure why I thought it wasn't.

Updates tailscale/corp#5780

This reverts commit 11dca08e93.

Signed-off-by: Will Norris <will@tailscale.com>
2024-04-15 11:21:13 -07:00
Brad Fitzpatrick
952e06aa46 wgengine/router: don't attempt route cleanup on Synology
Trying to run iptables/nftables on Synology pauses for minutes with
lots of errors and ultimately does nothing as it's not used and we
lack permissions.

This fixes a regression from db760d0bac (#11601) that landed
between Synology testing on unstable 1.63.110 and 1.64.0 being cut.

Fixes #11737

Change-Id: Iaf9563363b8e45319a9b6fe94c8d5ffaecc9ccef
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15 09:49:25 -07:00
Irbe Krumina
38fb23f120 cmd/k8s-operator,k8s-operator: allow users to configure proxy env vars via ProxyClass (#11743)
Adds new ProxyClass.spec.statefulSet.pod.{tailscaleContainer,tailscaleInitContainer}.Env field
that allow users to provide key, value pairs that will be set as env vars for the respective containers.
Allow overriding all containerboot env vars,
but warn that this is not supported and might break (in docs + a warning when validating ProxyClass).

Updates tailscale/tailscale#10709

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-15 17:24:59 +01:00
Brad Fitzpatrick
9258bcc360 Makefile: fix default SYNO_ARCH in Makefile
It was broken with the move to dist in 32e0ba5e68 which doesn't accept
amd64 anymore.

Updates #cleanup

Change-Id: Iaaaba2d73c6a09a226934fe8e5c18b16731ee7a6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-15 08:59:48 -07:00
Brad Fitzpatrick
b9aa7421d6 ipn/ipnlocal: remove some dead code (legacyBackend methods) from LocalBackend
Nothing used it.

Updates #11649

Change-Id: Ic1c331d947974cd7d4738ff3aafe9c498853689e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-14 21:02:56 -07:00
Brad Fitzpatrick
a6739c49df paths: set default state path on AIX
Updates #11361

Change-Id: I196727a540be6b7c75303f9958490b1d76189fd6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 21:31:52 -07:00
Brad Fitzpatrick
271cfdb3d3 util/syspolicy: clean up doc grammar and consistency
Updates #cleanup

Change-Id: I912574cbd5ef4d8b7417b8b2a9b9a2ccfef88840
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 18:40:05 -07:00
Brad Fitzpatrick
bad3159b62 ipn/ipnlocal: delete useless SetControlClientGetterForTesting use
Updates #11649

Change-Id: I56c069b9c97bd3e30ff87ec6655ec57e1698427c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 18:06:06 -07:00
Brad Fitzpatrick
8186cd0349 ipn/ipnlocal: delete redundant TestStatusWithoutPeers
We have tstest/integration nowadays.

And this test was one of the lone holdouts using the to-be-nuked
SetControlClientGetterForTesting.

Updates #11649

Change-Id: Icf8a6a2e9b8ae1ac534754afa898c00dc0b7623b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 16:35:02 -07:00
Brad Fitzpatrick
68043a17c2 ipn/ipnlocal: centralize assignments to cc + ccAuto in new method
cc vs ccAuto is a mess. It needs to go. But this is a baby step towards
getting there.

Updates #11649

Change-Id: I34f33934844e580bd823a7d8f2b945cf26c87b3b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 16:35:02 -07:00
Brad Fitzpatrick
970b1e21d0 ipn/ipnlocal: inline assertClientLocked into its now sole caller
Updates #11649

Change-Id: I8e2a5e59125a0cad5c0a8c9ed8930585f1735d03
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 16:35:02 -07:00
Brad Fitzpatrick
170c618483 ipn/ipnlocal: remove dead code now that Android uses LocalAPI instead
The new Android app and its libtailscale don't use this anymore;
it uses LocalAPI like other clients now.

Updates #11649

Change-Id: Ic9f42b41e0e0280b82294329093dc6c275f41d50
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 15:57:50 -07:00
Flakes Updater
65f215115f go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-04-13 11:12:06 -07:00
Brad Fitzpatrick
a1abd12f35 cmd/tailscaled, net/tstun: build for aix/ppc64
At least in userspace-networking mode.

Fixes #11361

Change-Id: I78d33f0f7e05fe9e9ee95b97c99b593f8fe498f2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-13 11:03:22 -07:00
kari-ts
1cd51f95c7 ipnlocal: enable allow LAN for android (#11709)
Updates tailscale/corp#18984
Updates tailscale/corp#18202
2024-04-12 17:01:32 -07:00
Claire Wang
976d3c7b5f tailcfg: add exit destination for network flow logs node attribute (#11698)
Updates tailscale/corp#18625

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-04-12 16:31:27 -04:00
Joe Tsai
7a77a2edf1 logtail: optimize JSON processing (#11671)
Changes made:

* Avoid "encoding/json" for JSON processing, and instead use
"github.com/go-json-experiment/json/jsontext".
Use jsontext.Value.IsValid for validation, which is much faster.
Use jsontext.AppendQuote instead of our own JSON escaping.

* In drainPending, use a different maxLen depending on lowMem.
In lowMem mode, it is better to perform multiple uploads
than it is to construct a large body that OOMs the process.

* In drainPending, if an error is encountered draining,
construct an error message in the logtail JSON format
rather than something that is invalid JSON.

* In appendTextOrJSONLocked, use jsontext.Decoder to check
whether the input is a valid JSON object. This is faster than
the previous approach of unmarshaling into map[string]any and
then re-marshaling that data structure.
This is especially beneficial for network flow logging,
which produces relatively large JSON objects.

* In appendTextOrJSONLocked, enforce maxSize on the input.
If too large, then we may end up in a situation where the logs
can never be uploaded because it exceeds the maximum body size
that the Tailscale logs service accepts.

* Use "tailscale.com/util/truncate" to properly truncate a string
on valid UTF-8 boundaries.

* In general, remove unnecessary spaces in JSON output.

Performance:

    name       old time/op    new time/op    delta
    WriteText     776ns ± 2%     596ns ± 1%   -23.24%  (p=0.000 n=10+10)
    WriteJSON     110µs ± 0%       9µs ± 0%   -91.77%  (p=0.000 n=8+8)

    name       old alloc/op   new alloc/op   delta
    WriteText      448B ± 0%        0B       -100.00%  (p=0.000 n=10+10)
    WriteJSON    37.9kB ± 0%     0.0kB ± 0%   -99.87%  (p=0.000 n=10+10)

    name       old allocs/op  new allocs/op  delta
    WriteText      1.00 ± 0%      0.00       -100.00%  (p=0.000 n=10+10)
    WriteJSON     1.08k ± 0%     0.00k ± 0%   -99.91%  (p=0.000 n=10+10)

For text payloads, this is 1.30x faster.
For JSON payloads, this is 12.2x faster.

Updates #cleanup
Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-12 12:05:36 -07:00
Aaron Klotz
4d5d669cd5 net/dns: unconditionally write NRPT rules to local settings
We were being too aggressive when deciding whether to write our NRPT rules
to the local registry key or the group policy registry key.

After once again reviewing the document which calls itself a spec
(see issue), it is clear that the presence of the DnsPolicyConfig subkey
is the important part, not the presence of values set in the DNSClient
subkey. Furthermore, a footnote indicates that the presence of
DnsPolicyConfig in the GPO key will always override its counterpart in
the local key. The implication of this is important: we may unconditionally
write our NRPT rules to the local key. We copy our rules to the policy
key only when it contains NRPT rules belonging to somebody other than us.

Fixes https://github.com/tailscale/corp/issues/19071

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-04-12 11:56:26 -06:00
License Updater
9d021579e7 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-04-12 10:47:24 -07:00
Will Norris
11dca08e93 licenses: add gliderlabs/ssh license
This package is included in the tempfork directory, rather than as a go
module dependency, so is not included in the normal package list.

Updates tailscale/corp#5780

Signed-off-by: Will Norris <will@tailscale.com>
2024-04-11 16:22:23 -07:00
Jenny Zhang
2207643312 VERSION.txt: this is v1.65.0
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2024-04-11 14:20:42 -04:00
Jenny Zhang
09524b58f3 VERSION.txt: this is v1.64.0
Signed-off-by: Jenny Zhang <jz@tailscale.com>
2024-04-11 14:00:11 -04:00
James Tucker
a2eb1c22b0 wgengine/magicsock: allow disco communication without known endpoints
Just because we don't have known endpoints for a peer does not mean that
the peer should become unreachable. If we know the peers key, it should
be able to call us, then we can talk back via whatever path it called us
on. First step - don't drop the packet in this context.

Updates tailscale/corp#19106

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-11 09:29:49 -07:00
Patrick O'Doherty
7f4cda23ac scripts/installer.sh: add rpm GPG key import (#11686)
Extend the `zypper` install to import importing the GPG key used to sign
the repository packages.

Updates #11635

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-04-10 16:58:35 -07:00
James Tucker
8fa3026614 tsweb: switch to fastuuid for request ID generation
Request ID generation appears prominently in some services cumulative
allocation rate, and while this does not eradicate this issue (the API
still makes UUID objects), it does improve the overhead of this API and
reduce the amount of garbage that it produces.

Updates tailscale/corp#18266
Updates tailscale/corp#19054

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 14:05:20 -07:00
James Tucker
d0f3fa7d7e util/fastuuid: add a more efficient uuid generator
This still generates github.com/google/uuid UUID objects, but does so
using a ChaCha8 CSPRNG from the stdlib rand/v2 package. The public API
is backed by a sync.Pool to provide good performance in highly
concurrent operation.

Under high load the read API produces a lot of extra garbage and
overhead by way of temporaries and syscalls. This implementation reduces
both to minimal levels, and avoids any long held global lock by
utilizing sync.Pool.

Updates tailscale/corp#18266
Updates tailscale/corp#19054

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 14:05:20 -07:00
James Tucker
db760d0bac cmd/tailscaled: move cleanup to an implicit action during startup
This removes a potentially increased boot delay for certain boot
topologies where they block on ExecStartPre that may have socket
activation dependencies on other system services (such as
systemd-resolved and NetworkManager).

Also rename cleanup to clean up in affected/immediately nearby places
per code review commentary.

Fixes #11599

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-09 12:44:08 -07:00
Nick Khyl
8d83adde07 util/winutil/winenv: add package for current Windows environment details
Package winenv provides information about the current Windows environment.
This includes details such as whether the device is a server or workstation,
and if it is AD domain-joined, MDM-registered, or neither.

Updates tailscale/corp#18342

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-09 13:25:37 -05:00
Paul Scott
da4e92bf01 cmd/tailscale/cli: prefix all --help usages with "tailscale ...", some tidying
Also capitalises the start of all ShortHelp, allows subcommands to be hidden
with a "HIDDEN: " prefix in their ShortHelp, and adds a TS_DUMP_HELP envknob
to look at all --help messages together.

Fixes #11664

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-04-09 12:52:34 +01:00
Percy Wegmann
9da135dd64 cmd/tailscale/cli: moved share.go to drive.go
Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-08 20:11:20 -05:00
Percy Wegmann
1e0ebc6c6d cmd/tailscale/cli: rename share command to drive
Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-08 20:11:20 -05:00
Joe Tsai
b4ba492701 logtail: require Buffer.Write to not retain the provided slice (#11617)
Buffer.Write has the exact same signature of io.Writer.Write.
The latter requires that implementations to never retain
the provided input buffer, which is an expectation that most
users will have when they see a Write signature.

The current behavior of Buffer.Write where it does retain
the input buffer is a risky precedent to set.
Switch the behavior to match io.Writer.Write.

There are only two implementations of Buffer in existence:
* logtail.memBuffer
* filch.Filch

The former can be fixed by cloning the input to Write.
This will cause an extra allocation in every Write,
but we can fix that will pooling on the caller side
in a follow-up PR.

The latter only passes the input to os.File.Write,
which does respect the io.Writer.Write requirements.

Updates #cleanup
Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-08 15:01:07 -07:00
Irbe Krumina
231e44e742 Revert "cmd/{k8s-nameserver,k8s-operator},k8s-operator: add a kube nameserver, make operator deploy it (#11017)" (#11669)
Temporarily reverting this PR to avoid releasing
half finished featue.

This reverts commit 9e2f58f846.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-08 21:31:52 +01:00
Andrea Gottardo
0001237253 docs/policy: update ADMX and ADML files with new Windows 1.62 syspolicies
Updates ENG-2776

Updates the .admx and .adml files to include the new ManagedByOrganizationName, ManagedByCaption and ManagedByURL system policies, added in Tailscale v1.62 for Windows.

Co-authored-by: Andrea Gottardo <andrea@gottardo.me>
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-04-08 15:21:27 -05:00
Brad Fitzpatrick
b27238b654 derp/derphttp: don't block in LocalAddr method
The derphttp.Client mutex is held during connects (for up to 10
seconds) so this LocalAddr method (blocking on said mutex) could also
block for up to 10 seconds, causing a pileup upstream in
magicsock/wgengine and ultimately a watchdog timeout resulting in a
crash.

Updates #11519

Change-Id: Idd1d94ee00966be1b901f6899d8b9492f18add0f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 10:57:05 -07:00
Brad Fitzpatrick
e6983baa73 cmd/tailscale/cli: fix macOS crash reading envknob in init (#11667)
And add a test.

Regression from a5e1f7d703

Fixes tailscale/corp#19036

Change-Id: If90984049af0a4820c96e1f77ddf2fce8cb3043f

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 10:22:31 -07:00
Chloé Vulquin
0f3a292ebd cli/configure: respect $KUBECONFIG (#11604)
cmd/tailscale/cli: respect $KUBECONFIG

* `$KUBECONFIG` is a `$PATH`-like: it defines a *list*.
`tailscale config kubeconfig` works like the rest of the
ecosystem so that if $KUBECONFIG is set it will write to the first existant file in the list, if none exist then
the final entry in the list.
* if `$KUBECONFIG` is an empty string, the old logic takes over.

Notes:

* The logic for file detection is inlined based on what `kind` does.
Technically it's a race condition, since the file could be removed/added
in between the processing steps, but the fallout shouldn't be too bad.
https://github.com/kubernetes-sigs/kind/blob/v0.23.0-alpha/pkg/cluster/internal/kubeconfig/internal/kubeconfig/paths.go

* The sandboxed (App Store) variant relies on a specific temporary
entitlement to access the ~/.kube/config file.
The entitlement is only granted to specific files, and so is not
applicable to paths supplied by the user at runtime.
While there may be other ways to achieve this access to arbitrary
kubeconfig files, it's out of scope for now.

Updates #11645

Signed-off-by: Chloé Vulquin <code@toast.bunkerlabs.net>
2024-04-08 16:49:43 +01:00
Brad Fitzpatrick
c71e8db058 cmd/tailscale/cli: stop spamming os.Stdout/os.Stderr in tests
After:

    bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
    bradfitz@book1pro tailscale.com % ./cli.test
    bradfitz@book1pro tailscale.com %

Before:

    bradfitz@book1pro tailscale.com % ./tool/go test -c ./cmd/tailscale/cli
    bradfitz@book1pro tailscale.com % ./cli.test

    Warning: funnel=on for foo.test.ts.net:443, but no serve config
             run: `tailscale serve --help` to see how to configure handlers

    Warning: funnel=on for foo.test.ts.net:443, but no serve config
             run: `tailscale serve --help` to see how to configure handlers
    USAGE
      funnel <serve-port> {on|off}
      funnel status [--json]

    Funnel allows you to publish a 'tailscale serve'
    server publicly, open to the entire internet.

    Turning off Funnel only turns off serving to the internet.
    It does not affect serving to your tailnet.

    SUBCOMMANDS
      status  show current serve/funnel status
    error: path must be absolute

    error: invalid TCP source "localhost:5432": missing port in address

    error: invalid TCP source "tcp://somehost:5432"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:5432error: invalid TCP source "tcp://somehost:0"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:0error: invalid TCP source "tcp://somehost:65536"
    must be one of: localhost or 127.0.0.1

    tcp://somehost:65536error: path must be absolute

    error: cannot serve web; already serving TCP

    You don't have permission to enable this feature.

This also moves the color handling up to a generic spot so it's
not just one subcommand doing it itself. See
https://github.com/tailscale/tailscale/issues/11626#issuecomment-2041795129

Fixes #11643
Updates #11626

Change-Id: I3a49e659dcbce491f4a2cb784be20bab53f72303
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-08 06:46:45 -07:00
Anton Tolchanov
5336362e64 prober: export probe class and metrics from bandwidth prober
- Wrap each prober function into a probe class that allows associating
  metric labels and custom metrics with a given probe;
- Make sure all existing probe classes set a `class` metric label;
- Move bandwidth probe size from being a metric label to a separate
  gauge metric; this will make it possible to use it to calculate
  average used bandwidth using a PromQL query;
- Also export transfer time for the bandwidth prober (more accurate than
  the total probe time, since it excludes connection establishment
  time).

Updates tailscale/corp#17912

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-04-08 12:02:58 +01:00
Anton Tolchanov
21671ca374 prober: remove unused notification code
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-04-08 12:02:58 +01:00
Brad Fitzpatrick
b0fbd85592 net/tsdial: partially fix "tailscale nc" (UserDial) on macOS
At least in the case of dialing a Tailscale IP.

Updates #4529

Change-Id: I9fd667d088a14aec4a56e23aabc2b1ffddafa3fe
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-07 16:04:32 -07:00
Brad Fitzpatrick
a5e1f7d703 ipn/{ipnlocal,localapi}: add API to toggle use of exit node
This is primarily for GUIs, so they don't need to remember the most
recently used exit node themselves.

This adds some CLI commands, but they're disabled and behind the WIP
envknob, as we need to consider naming (on/off is ambiguous with
running an exit node, etc) as well as automatic exit node selection in
the future. For now the CLI commands are effectively developer debug
things to test the LocalAPI.

Updates tailscale/corp#18724

Change-Id: I9a32b00e3ffbf5b29bfdcad996a4296b5e37be7e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-07 16:01:00 -07:00
Maisem Ali
3f4c5daa15 wgengine/netstack: remove SubnetRouterWrapper
It was used when we only supported subnet routers on linux
and would nil out the SubnetRoutes slice as no other router
worked with it, but now we support subnet routers on ~all platforms.

The field it was setting to nil is now only used for network logging
and nowhere else, so keep the field but drop the SubnetRouterWrapper
as it's not useful.

Updates #cleanup

Change-Id: Id03f9b6ec33e47ad643e7b66e07911945f25db79
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-04-07 15:44:41 -07:00
alexelisenko
fe22032fb3 net/dns/{publicdns,resolver}: add start of Control D support
Updates #7946

[@bradfitz fixed up version of #8417]

Change-Id: I1dbf6fa8d525b25c0d7ad5c559a7f937c3cd142a
Signed-off-by: alexelisenko <39712468+alexelisenko@users.noreply.github.com>
Signed-off-by: Alex Paguis <alex@windscribe.com>
2024-04-07 11:55:37 -07:00
Brad Fitzpatrick
aa084a29c6 ipn/ipnlocal: name the unlockOnce type, plumb more, add Unlock method
This names the func() that Once-unlocked LocalBackend.mu. It does so
both for docs and because it can then have a method: Unlock, for the
few points that need to explicitly unlock early (the cause of all this
mess). This makes those ugly points easy to find, and also can then
make them stricter, panicking if the mutex is already unlocked. So a
normal call to the func just once-releases the mutex, returning false
if it's already done, but the Unlock method is the strict one.

Then this uses it more, so most the b.mu.Unlock calls remaining are
simple cases and usually defers.

Updates #11649

Change-Id: Ia070db66c54a55e59d2f76fdc26316abf0dd4627
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-06 21:49:23 -07:00
Brad Fitzpatrick
5e7c0b025c ipn/ipnlocal: add some "lockedOnEntry" helpers + guardrails, fix bug
A number of methods in LocalBackend (with suffixed "LockedOnEntry")
require b.mu be held but unlock it on the way out. That's asymmetric
and atypical and error prone.

This adds a helper method to LocalBackend that locks the mutex and
returns a sync.OnceFunc that unlocks the mutex. Then we pass around
that unlocker func down the chain to make it explicit (and somewhat
type check the passing of ownership) but also let the caller defer
unlock it, in the case of errors/panics that happen before the callee
gets around to calling the unlock.

This revealed a latent bug in LocalBackend.DeleteProfile which double
unlocked the mutex.

Updates #11649

Change-Id: I002f77567973bd77b8906bfa4ec9a2049b89836a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-06 20:43:54 -07:00
Flakes Updater
efb710d0e5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-04-06 15:12:24 -07:00
Brad Fitzpatrick
38377c37b5 ipn/localapi: sort localapi handler map keys
Updates #cleanup

Change-Id: I750ed8d033954f1f8786fb35dd16895bb1c5af8e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-05 20:44:11 -07:00
Maisem Ali
21b32b467e tsweb: handle panics in retHandler
We would have incomplete stats and missing logs in cases
of panics.

Updates tailscale/corp#18687

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-04-05 18:47:21 -07:00
Brad Fitzpatrick
ac2522092d cmd/tailscale/cli: make exit-node list not random
The output was changing randomly per run, due to range over a map.

Then some misc style tweaks I noticed while debugging.

Fixes #11629

Change-Id: I67aef0e68566994e5744d4828002f6eb70810ee1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-05 18:19:50 -07:00
James Tucker
6e334e64a1 net/netcheck,wgengine/magicsock: align DERP frame receive time heuristics
The netcheck package and the magicksock package coordinate via the
health package, but both sides have time based heuristics through
indirect dependencies. These were misaligned, so the implemented
heuristic aimed at reducing DERP moves while there is active traffic
were non-operational about 3/5ths of the time.

It is problematic to setup a good test for this integration presently,
so instead I added comment breadcrumbs along with the initial fix.

Updates #8603

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-05 13:04:42 -07:00
Irbe Krumina
1fbaf26106 util/linuxfw: fix chain comparison (#11639)
Don't compare pointer fields by pointer value, but by the actual value

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-05 19:43:58 +01:00
Charlotte Brandhorst-Satzkorn
8c75da27fc drive: move normalizeShareName into pkg drive and make func public (#11638)
This change makes the normalizeShareName function public, so it can be
used for validation in control.

Updates tailscale/corp#16827

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-04-05 11:43:13 -07:00
Will Morrison
306bacc669 cmd/tailscale/cli: Add CLI command to update certs on Synology devices.
Fixes #4674

Signed-off-by: Will Morrison <william.barr.morrison@gmail.com>
2024-04-05 07:08:46 -07:00
Brad Fitzpatrick
9699bb0a20 metrics: fix outdated docs on MultiLabelMap
And make NewMultiLabelMap panic earlier (at construction time)
if the comparable struct type T violates the documented rules,
rather than panicking at Add time.

Updates #cleanup

Change-Id: Ib1a03babdd501b8d699c4f18b1097a56c916c6d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-04 20:53:47 -07:00
Joonas Kuorilehto
fe0cfec4ad wgengine/router: enable ip forwarding on gokrazy
Only on Gokrazy, set sysctls to enable IP forwarding so subnet routing
and advertised exit node works.

Fixes #11405

Signed-off-by: Joonas Kuorilehto <joneskoo@derbian.fi>
2024-04-04 20:48:55 -07:00
Joe Tsai
4bbac72868 util/truncate: support []byte as well (#11614)
There are no mutations to the input,
so we can support both ~string and ~[]byte just fine.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-04 14:38:16 -07:00
Charlotte Brandhorst-Satzkorn
98cf71cd73 tailscale: switch tailfs to drive syntax for api and logs (#11625)
This change switches the api to /drive, rather than the previous /tailfs
as well as updates the log lines to reflect the new value. It also
cleans up some existing tailfs references.

Updates tailscale/corp#16827

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-04-04 13:07:58 -07:00
Percy Wegmann
853e3e29a0 wgengine/router: provide explicit hook to signal Android when VPN needs to be reconfigured
This allows clients to avoid establishing their VPN multiple times when
both routes and DNS are changing in rapid succession.

Updates tailscale/corp#18928

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-04-04 12:56:49 -05:00
Joe Tsai
1a38d2a3b4 util/zstdframe: support specifying a MaxWindowSize (#11595)
Specifying a smaller window size during compression
provides a knob to tweak the tradeoff between memory usage
and the compression ratio.

Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-04 10:46:20 -07:00
Andrew Dunham
7d7d159824 prober: support creating multiple probes in ForEachAddr
So that we can e.g. check TLS on multiple ports for a given IP.

Updates tailscale/corp#16367

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I81d840a4c88138de1cbb2032b917741c009470e6
2024-04-04 13:04:16 -04:00
Andrew Dunham
ac574d875c prober: add helper function to check all IPs for a DNS hostname
This allows us to check all IP addresses (and address families) for a
given DNS hostname while dynamically discovering new IPs and removing
old ones as they're no longer valid.

Also add a testable example that demonstrates how to use it.

Alternative to #11610
Updates tailscale/corp#16367

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6d6f39bafc30e6dfcf6708185d09faee2a374599
2024-04-04 11:11:33 -04:00
Brad Fitzpatrick
8d7894c68e clientupdate, net/dns: fix some "tailsacle" typos
Updates #cleanup

Change-Id: I982175e74b0c8c5b3e01a573e5785e6596b7ac39
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 21:08:25 -07:00
Brad Fitzpatrick
92d3f64e95 go.toolchain.rev: bump to Go 1.22.2
Update tailscale/corp#18893

Change-Id: I4c04f5153ad43429d7f510c9ac2194c3b2fbc6c1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 11:11:07 -07:00
Charlotte Brandhorst-Satzkorn
93618a3518 tailscale: update tailfs functions and vars to use drive naming (#11597)
This change updates all tailfs functions and the majority of the tailfs
variables to use the new drive naming.

Updates tailscale/corp#16827

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-04-03 10:09:58 -07:00
Brad Fitzpatrick
2409661a0d control/controlclient: delete old naclbox code, require ts2021 Noise
Updates #11585
Updates tailscale/corp#18882

Change-Id: I90e2e4a211c58d429e2b128604614dde18986442
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 09:17:27 -07:00
Brad Fitzpatrick
b9611461e5 ipn/ipnlocal: q-encode (RFC 2047) Tailscale serve header values
Updates #11603

RELNOTE=Tailscale serve headers are now RFC 2047 Q-encoded

Change-Id: I1314b65ecf5d39a5a601676346ec2c334fdef042
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-03 09:08:29 -07:00
Claire Wang
262fa8a01e ipn/ipnlocal: populate peers' capabilities (#11365)
Populates capabilties field of peers in ipn status.
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-04-03 10:55:28 -04:00
James Tucker
9eaa56df93 tsweb: update doc on BucketedStatsOptions.Finish to match behavior
I originally came to update this to match the documented behavior, but
the code is deliberately avoiding this behavior currently, making it
hard to decide how to update this. For now just align the documentation
to the behavior.

Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-02 17:22:59 -07:00
Charlotte Brandhorst-Satzkorn
14683371ee tailscale: update tailfs file and package names (#11590)
This change updates the tailfs file and package names to their new
naming convention.

Updates #tailscale/corp#16827

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-04-02 13:32:30 -07:00
Brad Fitzpatrick
1c259100b0 cmd/{derper,derpprobe}: add --version flag
Fixes #11582

Change-Id: If99fc1ab6b89d624fbb07bd104dd882d2c7b50b4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-02 12:48:07 -07:00
Patrick O'Doherty
1535d0feca safeweb: move http.Serve for HTTP redirects into lib (#11592)
Refactor the interaction between caller/library when establishing the
HTTP to HTTPS redirects by moving the call to http.Serve into safeweb.
This makes linting for other uses of http.Serve easier without having to
account for false positives created by the old interface.

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

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-04-02 12:04:24 -07:00
James Tucker
f384742375 net/packet: allow more ICMP errors
We now allow some more ICMP errors to flow, specifically:

- ICMP parameter problem in both IPv4 and IPv6 (corrupt headers)
- ICMP Packet Too Big (for IPv6 PMTU)

Updates #311
Updates #8102
Updates #11002

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-02 11:31:49 -07:00
Irbe Krumina
92ca770b8d util/linuxfw: fix MSS clamping in nftables mode (#11588)
MSS clamping for nftables was mostly not ran due to to an earlier rule in the FORWARD chain issuing accept verdict.
This commit places the clamping rule into a chain of its own to ensure that it gets ran.

Updates tailscale/tailscale#11002

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-04-02 19:31:33 +01:00
Kyle Carberry
27038ee3c2 hostinfo: cache device model to speed up init
This was causing a relatively consistent ~10ms of delay on Linux.

Signed-off-by: Kyle Carberry <kyle@carberry.com>
2024-04-02 09:09:43 -07:00
Brad Fitzpatrick
ec87e219ae logtail: delete unused code from old way to configure zstd
Updates #cleanup

Change-Id: I666ecf08ea67e461adf2a3f4daa9d1753b2dc1e4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-04-01 20:43:06 -07:00
Joe Tsai
e2586bc674 logtail: always zstd compress with FastestCompression and LowMemory (#11583)
This is based on empirical testing using actual logs data.

FastestCompression only incurs a marginal <1% compression ratio hit
for a 2.25x reduction in memory use for small payloads
(which are common if log uploads happen at a decently high frequency).
The memory savings for large payloads is much lower
(less than 1.1x reduction).

LowMemory only incurs a marginal <5% hit on performance
for a 1.6-2.0x reduction in memory use for small or large payloads.

The memory gains for both settings justifies the loss of benefits,
which are arguably minimal.

tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-04-01 18:12:09 -07:00
James Tucker
7558a1d594 ipn/ipnlocal: disable sockstats on (unstable) mobile by default
We're tracking down a new instance of memory usage, and excessive memory usage
from sockstats is definitely not going to help with debugging, so disable it by
default on mobile.

Updates tailscale/corp#18514

Signed-off-by: James Tucker <james@tailscale.com>
2024-04-01 14:44:20 -07:00
Asutorufa
e20ce7bf0c net/dns: close ctx when close dns directManager
Signed-off-by: Asutorufa <16442314+Asutorufa@users.noreply.github.com>
2024-03-29 20:47:03 -07:00
Will Norris
1d2af801fa .github/workflows: remove go-licenses action
This is now handled by an action running in corp.

Updates tailscale/corp#18803

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-29 19:38:13 -07:00
License Updater
e80b99cdd1 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-29 16:32:25 -07:00
Andrew Lytvynov
5aa4cfad06 safeweb: detect mux handler conflicts (#11562)
When both muxes match, and one of them is a wildcard "/" pattern (which
is common in browser muxes), choose the more specific pattern.
If both are non-wildcard matches, there is a pattern overlap, so return
an error.

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

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-29 16:07:09 -06:00
Brad Fitzpatrick
e7599c1f7e logtail: prevent js/wasm clients from picking TLS client cert
Corp details:
https://github.com/tailscale/corp/issues/18177#issuecomment-2026598715
https://github.com/tailscale/corp/pull/18775#issuecomment-2027505036

Updates tailscale/corp#18177

Change-Id: I7c03a4884540b8519e0996088d085af77991f477
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-29 13:18:33 -07:00
Irbe Krumina
5fb721d4ad util/linuxfw,wgengine/router: skip IPv6 firewall configuration in partial iptables mode (#11546)
We have hosts that support IPv6, but not IPv6 firewall configuration
in iptables mode.
We also have hosts that have some support for IPv6 firewall
configuration in iptables mode, but do not have iptables filter table.
We should:
- configure ip rules for all hosts that support IPv6
- only configure firewall rules in iptables mode if the host
has iptables filter table.

Updates tailscale/tailscale#11540

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-29 05:23:03 +00:00
Patrick O'Doherty
af61179c2f safeweb: add opt-in inline style CSP toggle (#11551)
Allow the use of inline styles with safeweb via an opt-in configuration
item. This will append `style-src "self" "unsafe-inline"` to the default
CSP. The `style-src` directive will be used in lieu of the fallback
`default-src "self"` directive.

Updates tailscale/corp#8027

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-28 13:15:01 -07:00
Brad Fitzpatrick
b0941b79d6 tsweb: make BucketedStats not track 400s, 404s, etc
Updates tailscale/corp#18687

Change-Id: I142ccb1301ec4201c70350799ff03222bce96668
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-28 08:56:33 -07:00
Brad Fitzpatrick
354cac74a9 tsweb/varz: add charset=utf-8 to varz handler
Some of our labels contain UTF-8 and get mojibaked in the browser
right now.

Updates tailscale/corp#18687

Change-Id: I6069cffd6cc8813df415f06bb308bc2fc3ab65c4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-27 19:56:22 -07:00
James Tucker
9401b09028 control/controlclient: move client watchdog to cover initial request
The initial control client request can get stuck in the event that a
connection is established but then lost part way through, without any
ICMP or RST. Ensure that the control client will be restarted by timing
out that initial request as well.

Fixes #11542

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-27 16:02:52 -07:00
Irbe Krumina
9b5176c4d9 cmd/k8s-operator: fix failing tests (#11541)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-27 20:56:07 +00:00
Irbe Krumina
9e2f58f846 cmd/{k8s-nameserver,k8s-operator},k8s-operator: add a kube nameserver, make operator deploy it (#11017)
* cmd/k8s-nameserver,k8s-operator: add a nameserver that can resolve ts.net DNS names in cluster.

Adds a simple nameserver that can respond to A record queries for ts.net DNS names.
It can respond to queries from in-memory records, populated from a ConfigMap
mounted at /config. It dynamically updates its records as the ConfigMap
contents changes.
It will respond with NXDOMAIN to queries for any other record types
(AAAA to be implemented in the future).
It can respond to queries over UDP or TCP. It runs a miekg/dns
DNS server with a single registered handler for ts.net domain names.
Queries for other domain names will be refused.

The intended use of this is:
1) to allow non-tailnet cluster workloads to talk to HTTPS tailnet
services exposed via Tailscale operator egress over HTTPS
2) to allow non-tailnet cluster workloads to talk to workloads in
the same cluster that have been exposed to tailnet over their
MagicDNS names but on their cluster IPs.

Updates tailscale/tailscale#10499

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/k8s-operator/deploy/crds,k8s-operator: add DNSConfig CustomResource Definition

DNSConfig CRD can be used to configure
the operator to deploy kube nameserver (./cmd/k8s-nameserver) to cluster.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/k8s-operator,k8s-operator: optionally reconcile nameserver resources

Adds a new reconciler that reconciles DNSConfig resources.
If a DNSConfig is deployed to cluster,
the reconciler creates kube nameserver resources.
This reconciler is only responsible for creating
nameserver resources and not for populating nameserver's records.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

* cmd/{k8s-operator,k8s-nameserver}: generate DNSConfig CRD for charts, append to static manifests

Signed-off-by: Irbe Krumina <irbe@tailscale.com>

---------

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-27 20:18:17 +00:00
Patrick O'Doherty
b60c4664c7 safeweb: return http.Handler from safeweb.RedirectHTTP (#11538)
Updates #cleanup

Change the return type of the safeweb.RedirectHTTP method to a handler
that can be passed directly to http.Serve without any http.HandlerFunc
wrapping necessary.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-27 11:44:17 -07:00
Brad Fitzpatrick
3e6306a782 derp/derphttp: make CONNECT Host match request-target's authority-form
This CONNECT client doesn't match what Go's net/http.Transport does
(making the two values match).  This makes it match.

This is all pretty unspecified but most clients & doc examples show
these matching. And some proxy implementations (such as Zscaler) care.

Updates tailscale/corp#18716

Change-Id: I135c5facbbcec9276faa772facbde1bb0feb2d26
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-27 11:36:28 -07:00
Patrick O'Doherty
8f27520633 safeweb: init (#11467)
Updates https://github.com/tailscale/corp/issues/8027

Safeweb is a wrapper around http.Server & tsnet that encodes some
application security defaults.

Safeweb asks developers to split their HTTP routes into two
http.ServeMuxs for serving browser and API-facing endpoints
repsectively. It then wraps these HTTP routes with the
context-appropriate security controls.

safeweb.Server#Serve will serve the HTTP muxes over the provided
listener. Caller are responsible for creating and tearing down their
application's listeners. Applications being served over HTTPS that wish
to implement HTTP redirects can use the Server#HTTPRedirect handler to
do so.

Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
2024-03-27 10:10:59 -07:00
Andrea Gottardo
008676f76e cmd/serve: update warning for sandboxed macOS builds (#11530) 2024-03-27 09:03:52 -07:00
Percy Wegmann
66e4d843c1 ipn/localapi: add support for multipart POST to file-put
This allows sending multiple files via Taildrop in one request.
Progress is tracked via ipn.Notify.

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-27 08:53:52 -05:00
Percy Wegmann
bed818a978 ipn/localapi: add support for multipart POST to file-put
This allows sending multiple files via Taildrop in one request.
Progress is tracked via ipn.Notify.

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-27 08:53:52 -05:00
Maisem Ali
0d8cd1645a go.mod: bump github.com/gaissmai/bart
To pick up https://github.com/gaissmai/bart/pull/17.

Updates #deps

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2024-03-26 16:06:52 -07:00
Percy Wegmann
eb42a16da9 ipn/ipnlocal: report Taildrive access message on failed responses
For example, if we get a 404 when downloading a file, we'll report access.

Also, to reduce verbosty of logs, this elides 0 length files.

Updates tailscale/corp#17818

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-26 16:37:08 -05:00
Andrew Lytvynov
5d41259a63 cmd/tailscale/cli: remove Beta tag from tailscale update (#11529)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-26 15:28:34 -06:00
Charlotte Brandhorst-Satzkorn
acb611f034 ipn/localipn: introduce logs for tailfs (#11496)
This change introduces some basic logging into the access and share
pathways for tailfs.

Updates tailscale/corp#17818

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-03-26 13:14:43 -07:00
Irbe Krumina
4cbef20569 cmd/k8s-operator: redact auth key from debug logs (#11523)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-26 16:20:32 +00:00
Brad Fitzpatrick
55baf9474f metrics, tsweb/varz: add multi-label map metrics
Updates tailscale/corp#18640

Change-Id: Ia9ae25956038e9d3266ea165537ac6f02485b74c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-26 09:01:04 -07:00
Flakes Updater
90a4d6ce69 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-03-26 08:37:52 -07:00
Brad Fitzpatrick
6d90966c1f logtail: move a scratch buffer to Logger
Rather than pass around a scratch buffer, put it on the Logger.

This is a baby step towards removing the background uploading
goroutine and starting it as needed.

Updates tailscale/corp#18514 (insofar as it led me to look at this code)

Change-Id: I6fd94581c28bde40fdb9fca788eb9590bcedae1b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-25 17:33:42 -07:00
Irbe Krumina
06e22a96b1 .github/workflows: fix path filter for 'Kubernetes manifests' test job (#11520)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-25 19:44:30 +00:00
Chris Milson-Tokunaga
b6dfd7443a Change type of installCRDs (#11478)
Including the double quotes (`"`) around the value made it appear like the helm chart should expect a string value for `installCRDs`.

Signed-off-by: Chris Milson-Tokunaga <chris.w.milson@gmail.com>
2024-03-25 19:11:55 +00:00
Percy Wegmann
8b8b315258 net/tstun: use gaissmai/bart instead of tempfork/device
This implementation uses less memory than tempfork/device,
which helps avoid OOM conditions in the iOS VPN extension when
switching to a Tailnet with ExitNode routing enabled.

Updates tailscale/corp#18514

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-25 12:31:14 -05:00
Andrew Lytvynov
1e7050e73a go.mod: bump github.com/docker/docker (#11515)
There's a vulnerability https://pkg.go.dev/vuln/GO-2024-2659 that
govulncheck flags, even though it's only reachable from tests and
cmd/sync-containers and cannot be exploited there.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-25 10:45:35 -06:00
Brad Fitzpatrick
a36cfb4d3d tailcfg, ipn/ipnlocal, wgengine/magicsock: add only-tcp-443 node attr
Updates tailscale/corp#17879

Change-Id: I0dc305d147b76c409cf729b599a94fa723aef0e0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-25 08:48:25 -07:00
Brad Fitzpatrick
7b34154df2 all: deprecate Node.Capabilities (more), remove PeerChange.Capabilities [capver 89]
First we had Capabilities []string. Then
https://tailscale.com/blog/acl-grants (#4217) brought CapMap, a
superset of Capabilities. Except we never really finished the
transition inside the codebase to go all-in on CapMap. This does so.

Notably, this coverts Capabilities on the wire early to CapMap
internally so the code can only deal in CapMap, even against an old
control server.

In the process, this removes PeerChange.Capabilities support, which no
known control plane sent anyway. They can and should use
PeerChange.CapMap instead.

Updates #11508
Updates #4217

Change-Id: I872074e226b873f9a578d9603897b831d50b25d9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-24 21:08:46 -07:00
Brad Fitzpatrick
4992aca6ec tsweb/varz: flesh out munging of expvar keys into valid Prometheus metrics
From a problem we hit with how badger registers expvars; it broke
trunkd's exported metrics.

Updates tailscale/corp#1297

Change-Id: I42e1552e25f734c6f521b6e993d57a82849464b2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-24 20:06:06 -07:00
Brad Fitzpatrick
b104688e04 ipn/ipnlocal, types/netmap: replace hasCapability with set lookup on NetworkMap
When node attributes were super rare, the O(n) slice scans looking for
node attributes was more acceptable. But now more code and more users
are using increasingly more node attributes. Time to make it a map.

Noticed while working on tailscale/corp#17879

Updates #cleanup

Change-Id: Ic17c80341f418421002fbceb47490729048756d2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-22 15:30:46 -07:00
Percy Wegmann
8c88853db6 ipn/ipnlocal: add c2n /debug/pprof/allocs endpoint
This behaves the same as typical debug/pprof/allocs.

Updates tailscale/corp#18514

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-22 17:29:59 -05:00
Brad Fitzpatrick
f45594d2c9 control/controlclient: free memory on iOS before full netmap work
Updates tailscale/corp#18514

Change-Id: I8d0330334b030ed8692b25549a0ee887ac6d7188
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-22 09:02:19 -07:00
James Tucker
e0f97738ee localapi: reduce garbage production in bus watcher
Updates #optimization

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-21 17:50:20 -07:00
James Tucker
3f7313dbdb util/linuxfw,wgengine/router: enable IPv6 configuration when netfilter is disabled
Updates #11434

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-21 16:10:47 -07:00
Brad Fitzpatrick
8444937c89 control/controlclient: fix panic regression from earlier load balancer hint header
In the recent 20e9f3369 we made HealthChangeRequest machine requests
include a NodeKey, as it was the oddball machine request that didn't
include one. Unfortunately, that code was sometimes being called (at
least in some of our integration tests) without a node key due to its
registration with health.RegisterWatcher(direct.ReportHealthChange).

Fortunately tests in corp caught this before we cut a release. It's
possible this only affects this particular integration test's
environment, but still worth fixing.

Updates tailscale/corp#1297

Change-Id: I84046779955105763dc1be5121c69fec3c138672
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 12:54:58 -07:00
Joe Tsai
85febda86d all: use zstdframe where sensible (#11491)
Use the zstdframe package where sensible instead of plumbing
around our own zstd.Encoder just for stateless operations.

This causes logtail to have a dependency on zstd,
but that's arguably okay since zstd support is implicit
to the protocol between a client and the logging service.
Also, virtually every caller to logger.NewLogger was
manually setting up a zstd.Encoder anyways,
meaning that zstd was functionally always a dependency.

Updates #cleanup
Updates tailscale/corp#18514

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-03-21 12:20:38 -07:00
Joe Tsai
d4bfe34ba7 util/zstdframe: add package for stateless zstd compression (#11481)
The Go zstd package is not friendly for stateless zstd compression.
Passing around multiple zstd.Encoder just for stateless compression
is a waste of memory since the memory is never freed and seldom
used if no compression operations are happening.

For performance, we pool the relevant Encoder/Decoder
with the specific options set.

Functionally, this package is a wrapper over the Go zstd package
with a more ergonomic API for stateless operations.

This package can be used to cleanup various pre-existing zstd.Encoder
pools or one-off handlers spread throughout our codebases.

Performance:

	BenchmarkEncode/Best               1690        610926 ns/op      25.78 MB/s           1 B/op          0 allocs/op
	    zstd_test.go:137: memory: 50.336 MiB
	    zstd_test.go:138: ratio:  3.269x
	BenchmarkEncode/Better            10000        100939 ns/op     156.04 MB/s           0 B/op          0 allocs/op
	    zstd_test.go:137: memory: 20.399 MiB
	    zstd_test.go:138: ratio:  3.131x
	BenchmarkEncode/Default            15775         74976 ns/op     210.08 MB/s         105 B/op          0 allocs/op
	    zstd_test.go:137: memory: 1.586 MiB
	    zstd_test.go:138: ratio:  3.064x
	BenchmarkEncode/Fastest            23222         53977 ns/op     291.81 MB/s          26 B/op          0 allocs/op
	    zstd_test.go:137: memory: 599.458 KiB
	    zstd_test.go:138: ratio:  2.898x
	BenchmarkEncode/FastestLowMemory                   23361         50789 ns/op     310.13 MB/s          15 B/op          0 allocs/op
	    zstd_test.go:137: memory: 334.458 KiB
	    zstd_test.go:138: ratio:  2.898x
	BenchmarkEncode/FastestNoChecksum                  23086         50253 ns/op     313.44 MB/s          26 B/op          0 allocs/op
	    zstd_test.go:137: memory: 599.458 KiB
	    zstd_test.go:138: ratio:  2.900x

	BenchmarkDecode/Checksum                           70794         17082 ns/op     300.96 MB/s           4 B/op          0 allocs/op
	    zstd_test.go:163: memory: 316.438 KiB
	BenchmarkDecode/NoChecksum                         74935         15990 ns/op     321.51 MB/s           4 B/op          0 allocs/op
	    zstd_test.go:163: memory: 316.438 KiB
	BenchmarkDecode/LowMemory                          71043         16739 ns/op     307.13 MB/s           0 B/op          0 allocs/op
	    zstd_test.go:163: memory: 79.347 KiB

We can see that the options are taking effect where compression ratio improves
with higher levels and compression speed diminishes.
We can also see that LowMemory takes effect where the pooled coder object
references less memory than other cases.
We can see that the pooling is taking effect as there are 0 amortized allocations.

Additional performance:

	BenchmarkEncodeParallel/zstd-24                     1857        619264 ns/op        1796 B/op         49 allocs/op
	BenchmarkEncodeParallel/zstdframe-24                1954        532023 ns/op        4293 B/op         49 allocs/op
	BenchmarkDecodeParallel/zstd-24                     5288        197281 ns/op        2516 B/op         49 allocs/op
	BenchmarkDecodeParallel/zstdframe-24                6441        196254 ns/op        2513 B/op         49 allocs/op

In concurrent usage, handling the pooling in this package
has a marginal benefit over the zstd package,
which relies on a Go channel as the pooling mechanism.
In particular, coders can be freed by the GC when not in use.
Coders can be shared throughout the program if they use this package
instead of multiple independent pools doing the same thing.
The allocations are unrelated to pooling as they're caused by the spawning of goroutines.

Updates #cleanup
Updates tailscale/corp#18514
Updates tailscale/corp#17653
Updates tailscale/corp#18005

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-03-21 11:39:20 -07:00
Brad Fitzpatrick
6a860cfb35 ipn/ipnlocal: add c2n pprof option to force a GC
Like net/http/pprof has.

Updates tailscale/corp#18514

Change-Id: I264adb6dcf5732d19707783b29b7273b4ca69cf4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 10:24:06 -07:00
Brad Fitzpatrick
5d1c72f76b wgengine/magicsock: don't use endpoint debug ringbuffer on mobile.
Save some memory.

Updates tailscale/corp#18514

Change-Id: Ibcaf3c6d8e5cc275c81f04141d0f176e2249509b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-21 06:58:55 -07:00
Andrew Dunham
512fc0b502 util/reload: add new package to handle periodic value loading
This can be used to reload a value periodically, whether from disk or
another source, while handling jitter and graceful shutdown.

Updates tailscale/corp#1297

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Iee2b4385c9abae59805f642a7308837877cb5b3f
2024-03-20 18:23:54 -04:00
Adrian Dewhurst
2f7e7be2ea control/controlclient: do not alias peer CapMap
Updates #cleanup

Change-Id: I10fd5e04310cdd7894a3caa3045b86eb0a06b6a0
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-03-20 15:42:44 -04:00
Percy Wegmann
067ed0bf6f ipnlocal: ensure TailFS share notifications are non-nil
This allows the UI to distinguish between 'no shares' versus
'not being notified about shares'.

Updates ENG-2843

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-20 13:54:33 -05:00
Brad Fitzpatrick
20e9f3369d control/controlclient: send load balancing hint HTTP request header
Updates tailscale/corp#1297

Change-Id: I0b102081e81dfc1261f4b05521ab248a2e4a1298
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-20 09:39:41 -07:00
Percy Wegmann
15c58cb77c tailfs: include whitespace in test share and filenames
Since TailFS allows spaces in folder and file names, test with spaces.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-20 09:41:18 -05:00
James Tucker
e37eded256 tool/gocross: add android autoflags (#11465)
Updates tailscale/corp#18202

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-19 16:08:20 -07:00
Claire Wang
221de01745 control/controlclient: fix sending peer capmap changes (#11457)
Instead of just checking if a peer capmap is nil, compare the previous
state peer capmap with the new peer capmap.
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-19 18:56:06 -04:00
Andrew Dunham
6da1dc84de wgengine: fix logger data race in tests
Observed in:
    https://github.com/tailscale/tailscale/actions/runs/8350904950/job/22858266932?pr=11463

Updates #11226

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9b57db4b34b6ad91d240cd9fa7e344fc0376d52d
2024-03-19 18:51:25 -04:00
Andrew Dunham
e382e4cee6 syncs: add Swap method
To mimic sync.Map.Swap, sync/atomic.Value.Swap, etc.

Updates tailscale/corp#1297

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If7627da1bce8b552873b21d7e5ebb98904e9a650
2024-03-19 18:39:44 -04:00
Andrea Gottardo
6288c9b41e version/prop: remove IsMacAppSandboxEnabled (#11461)
Fixes tailscale/corp#18441

For a few days, IsMacAppStore() has been returning `false` on App Store builds (IPN-macOS target in Xcode).

I regressed this in #11369 by introducing logic to detect the sandbox by checking for the APP_SANDBOX_CONTAINER_ID environment variable. I thought that was a more robust approach instead of checking the name of the executable. However, it appears that on recent macOS versions this environment variable is no longer getting set, so we should go back to the previous logic that checks for the executable path, or HOME containing references to macsys.

This PR also adds additional checks to the logic by also checking XPC_SERVICE_NAME in addition to HOME where possible. That environment variable is set inside the network extension, either macos or macsys and is good to look at if for any reason HOME is not set.
2024-03-19 14:50:34 -07:00
Mario Minardi
68d9e49a5b api.md: add missing backtick to GET searchpaths doc (#11459)
Add missing backtick to GET searchpaths api documentation.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-19 11:31:03 -06:00
Will Norris
349799a1ba api.md: format API docs with prettier
Mostly inconsequential minor fixes for consistency.  A couple of changes
to actual JSON examples, but all still very readable, so I think it's
fine.

Updates #cleanup

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-19 09:23:50 -07:00
Irbe Krumina
b0c3e6f6c5 cmd/k8s-operator,ipn/conf.go: fix --accept-routes for proxies (#11453)
Fix a bug where all proxies got configured with --accept-routes set to true.
The bug was introduced in https://github.com/tailscale/tailscale/pull/11238.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-19 14:54:17 +00:00
James Tucker
7fe4cbbaf3 types/views: optimize slices contains under some conditions (#11449)
In control there are conditions where the leaf functions are not being
optimized away (i.e. At is not inlined), resulting in undesirable time
spent copying during SliceContains. This optimization is likely
irrelevant to simpler code or smaller structures.

Updates #optimization

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-18 16:19:16 -07:00
Mario Minardi
d2ccfa4edd cmd/tailscale,ipn/ipnlocal: enable web client over quad 100 by default (#11419)
Enable the web client over 100.100.100.100 by default. Accepting traffic
from [tailnet IP]:5252 still requires setting the `webclient` user pref.

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

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-18 15:47:21 -06:00
Will Norris
4d747c1833 api.md: document device expiration endpoint
This was originally built for testing node expiration flows, but is also
useful for customers to force device re-auth without actually deleting
the device from the tailnet.

Updates tailscale/corp#18408

Signed-off-by: Will Norris <will@tailscale.com>
2024-03-18 12:49:55 -07:00
Mario Minardi
e0886ad167 ipn/ipnlocal, tailcfg: add disable-web-client node attribute (#11418)
Add a disable-web-client node attribute and add handling for disabling
the web client when this node attribute is set.

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

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-18 10:32:33 -06:00
Marwan Sulaiman
da7c3d1753 envknob: ensure f is not nil before using it
This PR fixes a panic that I saw in the mac app where
parsing the env file fails but we don't get to see the
error due to the panic of using f.Name()

Fixes #11425

Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
2024-03-15 12:46:41 -04:00
Andrea Gottardo
08ebac9acb version,cli,safesocket: detect non-sandboxed macOS GUI (#11369)
Updates ENG-2848

We can safely disable the App Sandbox for our macsys GUI, allowing us to use `tailscale ssh` and do a few other things that we've wanted to do for a while. This PR:

- allows Tailscale SSH to be used from the macsys GUI binary when called from a CLI
- tweaks the detection of client variants in prop.go, with new functions `IsMacSys()`, `IsMacSysApp()` and `IsMacAppSandboxEnabled()`

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-03-14 14:28:06 -07:00
Irbe Krumina
ea55f96310 cmd/tailscale/cli: fix configuring partially empty kubeconfig (#11417)
When a user deletes the last cluster/user/context from their
kubeconfig via 'kubectl delete-[cluster|user|context] command,
kubectx sets the relevant field in kubeconfig to 'null'.
This was breaking our conversion logic that was assuming that the field
is either non-existant or is an array.

Updates tailscale/corp#18320

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-14 20:26:20 +00:00
Anton Tolchanov
cf8948da5f net/routetable: increase route limit used by the test
I was running all tests while preparing a recent stable release, and
this was failing because my computer is connected to a fairly large
tailnet.

```
--- FAIL: TestGetRouteTable (0.01s)
    routetable_linux_test.go:32: expected at least one default route;
    ...
```

```
$ ip route show table 52  | wc -l
1051
```

Updates #cleanup

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-14 16:10:40 +00:00
Andrew Lytvynov
decd9893e4 ipn/ipnlocal: validate domain of PopBrowserURL on default control URL (#11394)
If the client uses the default Tailscale control URL, validate that all
PopBrowserURLs are under tailscale.com or *.tailscale.com. This reduces
the risk of a compromised control plane opening phishing pages for
example.

The client trusts control for many other things, but this is one easy
way to reduce that trust a bit.

Fixes #11393

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-13 18:31:07 -06:00
Andrew Lytvynov
48eef9e6eb clientupdate: do not allow msiexec to reboot the OS (#11409)
According to
https://learn.microsoft.com/en-us/windows/win32/msi/standard-installer-command-line-options#promptrestart,
`/promptrestart` is ignored with `/quiet` is set, so msiexec.exe can
sometimes silently trigger a reboot. The best we can do to reduce
unexpected disruption is to just prevent restarts, until the user
chooses to do it. Restarts aren't normally needed for Tailscale updates,
but there seem to be some situations where it's triggered.

Updates #18254

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-03-13 16:55:24 -06:00
Anton Tolchanov
da3cf12194 VERSION.txt: this is v1.63.0
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 14:51:52 +00:00
Anton Tolchanov
f12d2557f9 prober: add a DERP bandwidth probe
Updates tailscale/corp#17912

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Anton Tolchanov
5018683d58 prober: remove unused derp prober latency measurements
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Anton Tolchanov
205a10b51a prober: export probe counters and cumulative latency
Updates #cleanup

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-03-13 13:36:45 +00:00
Andrew Dunham
7429e8912a wgengine/netstack: fix bug with duplicate SYN packets in client limit
This fixes a bug that was introduced in #11258 where the handling of the
per-client limit didn't properly account for the fact that the gVisor
TCP forwarder will return 'true' to indicate that it's handled a
duplicate SYN packet, but not launch the handler goroutine.

In such a case, we neither decremented our per-client limit in the
wrapper function, nor did we do so in the handler function, leading to
our per-client limit table slowly filling up without bound.

Fix this by doing the same duplicate-tracking logic that the TCP
forwarder does so we can detect such cases and appropriately decrement
our in-flight counter.

Updates tailscale/corp#12184

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib6011a71d382a10d68c0802593f34b8153d06892
2024-03-11 08:05:00 -04:00
Brad Fitzpatrick
ad33e47270 ipn/{ipnlocal,localapi}: add debug verb to force spam IPN bus NetMap
To force the problem in its worst case scenario before fixing it.

Updates tailscale/corp#17859

Change-Id: I2c8b8e5f15c7801e1ab093feeafac52ec175a763
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-09 17:47:15 -08:00
Flakes Updater
04fceae898 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-03-09 11:51:43 -08:00
James Tucker
055117ad45 util/linuxfw: fix support for containers without IPv6 iptables filters (#11381)
There are container environments such as GitHub codespaces that have
partial IPv6 support - routing support is enabled at the kernel level,
but lacking IPv6 filter support in the iptables module.

In the specific example of the codespaces environment, this also has
pre-existing legacy iptables rules in the IPv4 tables, as such the
nascent firewall mode detection will always pick iptables.

We would previously fault trying to install rules to the filter table,
this catches that condition earlier, and disables IPv6 support under
these conditions.

Updates #5621
Updates #11344
Updates #11354

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:46:21 -08:00
James Tucker
43fba6e04d util/linuxfw: correct logical error in NAT table check (#11380)
Updates #11344
Updates #11354

Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:35:13 -08:00
panchajanya
50a570a83f Code Improvements (#11311)
build_docker, update-flake: cleanup and apply shellcheck fixes

Was editing this file to match my needs while shellcheck warnings
bugged me out.
REV isn't getting used anywhere. Better remove it.

Updates #cleanup

Signed-off-by: Panchajanya1999 <kernel@panchajanya.dev>
Signed-off-by: James Tucker <james@tailscale.com>
2024-03-08 15:24:36 -08:00
Percy Wegmann
e496451928 ipn,cmd/tailscale,client/tailscale: add support for renaming TailFS shares
- Updates API to support renaming TailFS shares.
- Adds a CLI rename subcommand for renaming a share.
- Renames the CLI subcommand 'add' to 'set' to make it clear that
  this is an add or update.
- Adds a unit test for TailFS in ipnlocal

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-08 14:48:26 -06:00
Percy Wegmann
6c160e6321 ipn,tailfs: tie TailFS share configuration to user profile
Previously, the configuration of which folders to share persisted across
profile changes. Now, it is tied to the user's profile.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-08 14:48:26 -06:00
Percy Wegmann
16ae0f65c0 cmd/viewer: import views when generating byteSliceField
Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-08 14:48:26 -06:00
Andrew Dunham
f072d017bd wgengine/magicsock: don't change DERP home when not connected to control
This pretty much always results in an outage because peers won't
discover our new home region and thus won't be able to establish
connectivity.

Updates tailscale/corp#18095

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ic0d09133f198b528dd40c6383b16d7663d9d37a7
2024-03-08 14:15:13 -05:00
Sonia Appasamy
54e52532eb version/mkversion: enforce synology versions within int32 range
Synology requires version numbers are within int32 range. This
change updates the version logic to keep things closer within the
range, and errors on building when the range is exceeded.

Updates #cleanup

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-03-08 12:47:59 -05:00
Claire Wang
74e33b9c50 tailcfg: bump CapabilityVersion (#11368)
bump version for adding NodeAttrSuggestExitNode
remove extra s from NodeAttrSuggestExitNode
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-07 14:17:40 -05:00
Mario Minardi
c662bd9fe7 client/web: dedupe packages in yarn.lock (#11327)
Run yarn-deduplicate on yarn.lock to dedupe packages. This is being done
to reduce the number of redundant packages fetched by yarn when existing
versions in the lockfile satisfy the version dependency we need.

See https://github.com/scinos/yarn-deduplicate for details on the tool
used to perform this deduplication.

Updates #cleanup

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-03-07 09:29:20 -07:00
Andrew Dunham
34176432d6 cmd/derper, types/logger: move log filter to shared package
So we can use it in trunkd to quiet down the logs there.

Updates #5563

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie3177dc33f5ad103db832aab5a3e0e4f128f973f
2024-03-07 11:05:03 -05:00
Irbe Krumina
3047b6274c docs/k8s: don't run subnet router in userspace mode (#11363)
There should not be a need to do that unless we run on host network

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-07 13:56:11 +00:00
Andrew Dunham
9884d06b80 net/interfaces: fix test hang on Darwin
This test could hang because the subprocess was blocked on writing to
the stdout pipe if we find the address we're looking for early in the
output.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I68d82c22a5d782098187ae6d8577e43063b72573
2024-03-06 22:37:40 -05:00
Andrew Dunham
62cf83eb92 go.mod: bump gvisor
The `stack.PacketBufferPtr` type no longer exists; replace it with
`*stack.PacketBuffer` instead.

Updates #8043

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ib56ceff09166a042aa3d9b80f50b2aa2d34b3683
2024-03-06 20:22:20 -05:00
Andrew Dunham
8f27d519bb tsweb: add String method to tsweb.RequestID
In case we want to change the format to something opaque later.

Updates tailscale/corp#2549

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie2eac8b885b694be607e9d5101d24b650026d89c
2024-03-06 19:48:04 -05:00
Irbe Krumina
90c4067010 util/linuxfw: add container-friendly IPv6 NAT check (#11353)
Remove IPv6 NAT check when routing is being set up
using nftables.
This is unnecessary as support for nftables was
added after support for IPv6.
https://tldp.org/HOWTO/Linux+IPv6-HOWTO/ch18s04.html
https://wiki.nftables.org/wiki-nftables/index.php/Building_and_installing_nftables_from_sources

Additionally, run an extra check for IPv6 NAT support
when the routing is set up with iptables.
This is because the earlier checks rely on
being able to use modprobe and on /proc/net/ip6_tables_names
being populated on start - these conditions are usually not
true in container environments.

Updates tailscale/tailscale#11344

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-03-06 21:53:51 +00:00
Percy Wegmann
fd942b5384 ipn/ipnlocal: reduce allocations in TailFS share notifications
This eliminates unnecessary map.Clone() calls and also eliminates
repetitive notifications about the same set of shares.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-06 14:21:53 -06:00
Percy Wegmann
6f66f5a75a ipn: add comment about thread-safety to StateStore
Updates #cleanup

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-06 12:42:18 -06:00
Andrea Gottardo
0cb86468ca ipn/localapi: add set-gui-visible endpoint
Updates tailscale/corp#17859

Provides a local API endpoint to be called from the GUI to inform the backend when the client menu is opened or closed.

cc @bradfitz

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2024-03-06 10:39:52 -08:00
Percy Wegmann
00373f07ac ipn/ipnlocal: exclude mullvad exit nodes from TailFS peers list
This is a temporary solution to at least omit Mullvad exit nodes
from the list of TailFS peers. Once we can identify peers that are
actually sharing via TailFS, we can remove this, but for alpha it'll
be sufficient to just omit Mullvad.

Updates tailscale/corp#17766

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-06 12:27:32 -06:00
Sonia Appasamy
c58c59ee54 {ipn,cmd/tailscale/cli}: move ServeConfig mutation logic to ipn/serve
Moving logic that manipulates a ServeConfig into recievers on the
ServeConfig in the ipn package. This is setup work to allow the
web client and cli to both utilize these shared functions to edit
the serve config.

Any logic specific to flag parsing or validation is left untouched
in the cli command. The web client will similarly manage its
validation of user's requested changes. If validation logic becomes
similar-enough, we can make a serve util for shared functionality,
which likely does not make sense in ipn.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-03-06 13:26:03 -05:00
Kristoffer Dalby
65255b060b client/tailscale: add postures to UserRuleMatch
Updates tailscale/corp#17770

Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-03-06 15:36:17 +01:00
License Updater
d59878e457 licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:55:26 -08:00
License Updater
797d75c50a licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:53:52 -08:00
License Updater
6a4e5329c3 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-03-05 17:53:05 -08:00
Andrew Dunham
4338db28f7 wgengine/magicsock: prefer link-local addresses to private ones
Since link-local addresses are definitionally more likely to be a direct
(lower-latency, more reliable) connection than a non-link-local private
address, give those a bit of a boost when selecting endpoints.

Updates #8097

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I93fdeb07de55ba39ba5fcee0834b579ca05c2a4e
2024-03-05 20:32:45 -05:00
Sonia Appasamy
65c3c690cf {ipn/serve,cmd/tailscale/cli}: move some shared funcs to ipn
In preparation for changes to allow configuration of serve/funnel
from the web client, this commit moves some functionality that will
be shared between the CLI and web client to the ipn package's
serve.go file, where some other util funcs are already defined.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-03-05 14:30:38 -05:00
Brad Fitzpatrick
8780e33500 go.toolchain.rev: bump Go toolchain to 1.22.1
Updates tailscale/corp#18000

Change-Id: I45de95e974ea55b0dac2218b3c82d124c4793390
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-03-05 10:51:13 -08:00
Paul Scott
2fa20e3787 util/cmpver: add Less/LessEq helper funcs
Updates tailscale/corp#17199

Signed-off-by: Paul Scott <paul@tailscale.com>
2024-03-05 16:57:04 +00:00
Claire Wang
d610f8eec0 tailcfg: add suggest exit node related node attribute (#11329)
Updates tailscale/corp#17516

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-03-05 10:54:41 -05:00
Chris Palmer
13853e7f29 tsweb: add more test cases for TestCleanRedirectURL (#11331)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-03-04 17:13:36 -08:00
Irbe Krumina
dff6f3377f docs/k8s: update docs (#11307)
Update docs for static Tailscale deployments on kube
to always use firewall mode autodection when in non-userspace.
Also add a note about running multiple replicas and a few suggestions how folks could do that.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <1687799+knyar@users.noreply.github.com>
2024-03-04 14:59:51 +00:00
Percy Wegmann
232a2d627c tailfs: only impersonate unprivileged user if able to sudo -u as that user
When serving TailFS shares, tailscaled executes another tailscaled to act as a
file server. It attempts to execute this child process as an unprivileged user
using sudo -u. This is important to avoid accessing files as root, which would
result in potential privilege escalation.

Previously, tailscaled assumed that it was running as someone who can sudo -u,
and would fail if it was unable to sudo -u.

With this commit, if tailscaled is unable to sudo -u as the requested user, and
tailscaled is not running as root, then tailscaled executes the the file server
process under the same identity that ran tailscaled, since this is already an
unprivileged identity.

In the unlikely event that tailscaled is running as root but is unable to
sudo -u, it will refuse to run the child file server process in order to avoid
privilege escalation.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-03-01 17:42:19 -06:00
Flakes Updater
00554ad277 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-02-29 19:53:19 -08:00
Andrew Lytvynov
23fbf0003f clientupdate: handle multiple versions in "apk info tailscale" output (#11310)
The package info output can list multiple package versions, and not in
descending order. Find the newest version in the output, instead of the
first one.

Fixes #11309

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-02-29 11:54:46 -07:00
Irbe Krumina
097c5ed927 util/linuxfw: insert rather than append nftables DNAT rule (#11303)
Ensure that the latest DNATNonTailscaleTraffic rule
gets inserted on top of any pre-existing rules.

Updates tailscale/tailscale#11281

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-29 16:53:43 +00:00
Percy Wegmann
e324a5660f ipn: include full tailfs shares in ipn notifications
This allows the Mac application to regain access to restricted
folders after restarts.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-29 10:16:44 -06:00
Percy Wegmann
80f1cb6227 tailfs: support storing bookmark data on shares
This allows the sandboxed Mac application to store security-
scoped URL bookmarks in order to maintain access to restricted
folders across restarts.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-29 10:16:44 -06:00
Brad Fitzpatrick
f18f591bc6 wgengine: plumb the PeerByKey from wgengine to magicsock
This was just added in 69f4b459 which doesn't yet use it. This still
doesn't yet use it. It just pushes it down deeper into magicsock where
it'll used later.

Updates #7617

Change-Id: If2f8fd380af150ffc763489e1ff4f8ca2899fac6
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 19:36:34 -08:00
Andrew Lytvynov
c7474431f1 tsweb: allow empty redirect URL in CleanRedirectURL (#11295)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-02-28 15:57:42 -08:00
Brad Fitzpatrick
b68a09cb34 ipn/ipnlocal: make active IPN sessions keyed by sessionID
We used a HandleSet before when we didn't have a unique handle. But a
sessionID is a unique handle, so use that instead. Then that replaces
the other map we had.

And now we'll have a way to look up an IPN session by sessionID for
later.

Updates tailscale/corp#17859

Change-Id: I5f647f367563ec8783c643e49f93817b341d9064
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 15:16:10 -08:00
Percy Wegmann
2d5d6f5403 ipn,wgengine: only intercept TailFS traffic on quad 100
This fixes a regression introduced with 993acf4 and released in
v1.60.0.

The regression caused us to intercept all userspace traffic to port
8080 which prevented users from exposing their own services to their
tailnet at port 8080.

Now, we only intercept traffic to port 8080 if it's bound for
100.100.100.100 or fd7a:115c:a1e0::53.

Fixes #11283

Signed-off-by: Percy Wegmann <percy@tailscale.com>
(cherry picked from commit 17cd0626f3)
2024-02-28 17:09:14 -06:00
Ross Zurowski
e83e2e881b client/web: fix Vite CJS deprecation warning (#11288)
Starting in Vite 5, Vite now issues a deprecation warning when using
a CJS-based Vite config file. This commit fixes it by adding the
`"type": "module"` to our package.json to opt our files into ESM module
behaviours.

Fixes #cleanup

Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2024-02-28 16:28:22 -05:00
Brad Fitzpatrick
69f4b4595a wgengine{,/wgint}: add wgint.Peer wrapper type, add to wgengine.Engine
This adds a method to wgengine.Engine and plumbed down into magicsock
to add a way to get a type-safe Tailscale-safe wrapper around a
wireguard-go device.Peer that only exposes methods that are safe for
Tailscale to use internally.

It also removes HandshakeAttempts from PeerStatusLite that was just
added as it wasn't needed yet and is now accessible ala cart as needed
from the Peer type accessor.

None of this is used yet.

Updates #7617

Change-Id: I07be0c4e6679883e6eeddf8dbed7394c9e79c5f4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 09:50:18 -08:00
James Tucker
7e17aeb36b .github/workflows: fix regular breakage of go toolchains
This server recently had a common ansible applied, which added a
periodic /tmp cleaner, as is needed on other CI machines to deal with
test tempfile leakage. The setting of $HOME to /tmp means that the go
toolchain in there was regularly getting pruned by the tmp cleaner, but
often incompletely, because it was also in use.

Move HOME to a runner owned directory.

Updates #11248

Signed-off-by: James Tucker <james@tailscale.com>
2024-02-28 08:17:35 -08:00
Brad Fitzpatrick
b4ff9a578f wgengine: rename local variable from 'found' to conventional 'ok'
Updates #cleanup

Change-Id: I799dc86ea9e4a3a949592abdd8e74282e7e5d086
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 07:33:57 -08:00
Brad Fitzpatrick
a8a525282c wgengine: use slices.Clone in two places
Updates #cleanup

Change-Id: I1cb30efb6d09180e82b807d6146f37897ef99307
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 07:33:57 -08:00
Brad Fitzpatrick
74b8985e19 ipn/ipnstate, wgengine: make PeerStatusLite.LastHandshake zero Time means none
... rather than 1970. Code was using IsZero against the 1970 team
(which isn't a zero value), but fortunately not anywhere that seems to
have mattered.

Updates #cleanup

Change-Id: I708a3f2a9398aaaedc9503678b4a8a311e0e019e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-28 07:33:57 -08:00
Andrew Dunham
3dd8ae2f26 net/tstun: fix spelling of "WireGuard"
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ida7e30f4689bc18f5f7502f53a0adb5ac3c7981a
2024-02-28 00:00:18 -05:00
Andrew Dunham
a20e46a80f util/cache: fix missing interface methods (#11275)
Updates #cleanup


Change-Id: Ib3a33a7609530ef8c9f3f58fc607a61e8655c4b5

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2024-02-27 23:03:49 -05:00
Andrew Dunham
23e9447871 tsweb: expose function to generate request IDs
For use in corp.

Updates tailscale/corp#2549

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71debae1ce9ae48cf69cc44c2ab5c443fc3b2005
2024-02-27 18:57:53 -05:00
Mario Minardi
7912d76da0 client/web: update to typescript 5.3.3 (#11267)
Update typescript to 5.3.3. This is a major bump from the previous
version of 4.8.3. This also requires adding newer versions of
@typescript-eslint/eslint-plugin and @typescript-eslint/parser to our
resolutions as eslint-config-react-app pulls in versions that otherwise
do not support typescript 5.x.

eslint-config-react-app has not been updated in 2 years and is seemingly
abandoned, so we may wish to fork it or move to a different eslint config
in the future.

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

Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-02-27 14:17:30 -07:00
Andrew Dunham
c5abbcd4b4 wgengine/netstack: add a per-client limit for in-flight TCP forwards
This is a fun one. Right now, when a client is connecting through a
subnet router, here's roughly what happens:

1. The client initiates a connection to an IP address behind a subnet
   router, and sends a TCP SYN
2. The subnet router gets the SYN packet from netstack, and after
   running through acceptTCP, starts DialContext-ing the destination IP,
   without accepting the connection¹
3. The client retransmits the SYN packet a few times while the dial is
   in progress, until either...
4. The subnet router successfully establishes a connection to the
   destination IP and sends the SYN-ACK back to the client, or...
5. The subnet router times out and sends a RST to the client.
6. If the connection was successful, the client ACKs the SYN-ACK it
   received, and traffic starts flowing

As a result, the notification code in forwardTCP never notices when a
new connection attempt is aborted, and it will wait until either the
connection is established, or until the OS-level connection timeout is
reached and it aborts.

To mitigate this, add a per-client limit on how many in-flight TCP
forwarding connections can be in-progress; after this, clients will see
a similar behaviour to the global limit, where new connection attempts
are aborted instead of waiting. This prevents a single misbehaving
client from blocking all other clients of a subnet router by ensuring
that it doesn't starve the global limiter.

Also, bump the global limit again to a higher value.

¹ We can't accept the connection before establishing a connection to the
remote server since otherwise we'd be opening the connection and then
immediately closing it, which breaks a bunch of stuff; see #5503 for
more details.

Updates tailscale/corp#12184

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I76e7008ddd497303d75d473f534e32309c8a5144
2024-02-27 15:25:40 -05:00
Claire Wang
352c1ac96c tailcfg: add latitude, longitude for node location (#11162)
Updates tailscale/corp#17590

Signed-off-by: Claire Wang <claire@tailscale.com>
2024-02-27 15:02:06 -05:00
Irbe Krumina
95dcc1745b cmd/k8s-operator: reconcile tailscale Ingresses when their backend Services change. (#11255)
This is so that if a backend Service gets created after the Ingress, it gets picked up by the operator.

Updates tailscale/tailscale#11251

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <1687799+knyar@users.noreply.github.com>
2024-02-27 15:19:53 +00:00
Irbe Krumina
303125d96d cmd/k8s-operator: configure all proxies with declarative config (#11238)
Containerboot container created for operator's ingress and egress proxies
are now always configured by passing a configfile to tailscaled
(tailscaled --config <configfile-path>.
It does not run 'tailscale set' or 'tailscale up'.
Upgrading existing setups to this version as well as
downgrading existing setups at this version works.

Updates tailscale/tailscale#10869

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-27 15:14:09 +00:00
Irbe Krumina
45d27fafd6 cmd/k8s-operator,k8s-operator,go.{mod,sum},tstest/tools: add Tailscale Kubernetes operator API docs (#11246)
Add logic to autogenerate CRD docs.
.github/workflows/kubemanifests.yaml CI workflow will fail if the doc is out of date with regard to the current CRDs.
Docs can be refreshed by running make kube-generate-all.

Updates tailscale/tailscale#11023

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-02-27 14:51:53 +00:00
Percy Wegmann
05acf76392 tailfs: fix race condition in tailfs_test
Ues a noop authenticator to avoid potential races in gowebdav's
built-in authenticator.

Fixes #11259

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-27 08:31:04 -06:00
Keli
086ef19439 scripts/installer.sh: auto-start tailscale on Alpine (#11214)
On Alpine, we add the tailscale service but fail to call start.
This means that tailscale does not start up until the user reboots the machine.

Fixes #11161

Signed-off-by: Keli Velazquez <keli@tailscale.com>
2024-02-27 09:17:12 -05:00
Brad Fitzpatrick
1cf85822d0 ipn/ipnstate, wgengine/wgint: add handshake attempts accessors
Not yet used. This is being made available so magicsock/wgengine can
use it to ignore certain sends (UDP + DERP) later on at least mobile,
letting wireguard-go think it's doing its full attempt schedule, but
we can cut it short conditionally based on what we know from the
control plane.

Updates #7617

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ia367cf6bd87b2aeedd3c6f4989528acdb6773ca7
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
eb28818403 wgengine: make pendOpen time later, after dup check
Otherwise on OS retransmits, we'd make redundant timers in Go's timer
heap that upon firing just do nothing (well, grab a mutex and check a
map and see that there's nothing to do).

Updates #cleanup

Change-Id: Id30b8b2d629cf9c7f8133a3f7eca5dc79e81facb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
219efebad4 wgengine: reduce critical section
No need to hold wgLock while using the device to LookupPeer;
that has its own mutex already.

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: Ib56049fcc7163cf5a2c2e7e12916f07b4f9d67cb
2024-02-26 19:09:12 -08:00
Brad Fitzpatrick
9a8c2f47f2 types/key: remove copy returning array by value
It's unnecessary. Returning an array value is already a copy.

Updates #cleanup

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Change-Id: If7f350b61003ea08f16a531b7b4e8ae483617939
2024-02-26 19:09:12 -08:00
Anton Tolchanov
8cc5c51888 health: warn about reverse path filtering and exit nodes
When reverse path filtering is in strict mode on Linux, using an exit
node blocks all network connectivity. This change adds a warning about
this to `tailscale status` and the logs.

Example in `tailscale status`:

```
- not connected to home DERP region 22
- The following issues on your machine will likely make usage of exit nodes impossible: [interface "eth0" has strict reverse-path filtering enabled], please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310
```

Example in the logs:
```
2024/02/21 21:17:07 health("overall"): error: multiple errors:
	not in map poll
	The following issues on your machine will likely make usage of exit nodes impossible: [interface "eth0" has strict reverse-path filtering enabled], please set rp_filter=2 instead of rp_filter=1; see https://github.com/tailscale/tailscale/issues/3310
```

Updates #3310

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-02-27 00:43:01 +00:00
Nick Khyl
7ef1fb113d cmd/tailscaled, ipn/ipnlocal, wgengine: shutdown tailscaled if wgdevice is closed
Tailscaled becomes inoperative if the Tailscale Tunnel wintun adapter is abruptly removed.
wireguard-go closes the device in case of a read error, but tailscaled keeps running.
This adds detection of a closed WireGuard device, triggering a graceful shutdown of tailscaled.
It is then restarted by the tailscaled watchdog service process.

Fixes #11222

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-02-26 14:45:35 -06:00
Nick Khyl
b42b9817b0 net/dns: do not wait for the interface registry key to appear if the windowsManager is being closed
The WinTun adapter may have been removed by the time we're closing
the dns.windowsManager, and its associated interface registry key might
also have been deleted. We shouldn't use winutil.OpenKeyWait and wait
for the interface key to appear when performing a cleanup as a part of
the windowsManager shutdown.

Updates #11222

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2024-02-26 14:45:35 -06:00
OSS Updater
82c569a83a go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-02-26 13:26:47 -05:00
Sonia Appasamy
95f26565db client/web: use grants on web UI frontend
Starts using peer capabilities to restrict the management client
on a per-view basis. This change also includes a bulky cleanup
of the login-toggle.tsx file, which was getting pretty unwieldy
in its previous form.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-26 12:59:37 -05:00
Sonia Appasamy
9aa704a05d client/web: restrict serveAPI endpoints to peer capabilities
This change adds a new apiHandler struct for use from serveAPI
to aid with restricting endpoints to specific peer capabilities.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-02-26 12:59:37 -05:00
Anton Tolchanov
cd9cf93de6 wgengine/netstack: expose TCP forwarder drops via clientmetrics
- add a clientmetric with a counter of TCP forwarder drops due to the
  max attempts;
- fix varz metric types, as they are all counters.

Updates #8210

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-02-26 17:32:34 +00:00
430 changed files with 16289 additions and 6573 deletions

View File

@@ -1,64 +0,0 @@
name: go-licenses
on:
# run action when a change lands in the main branch which updates go.mod or
# our license template file. Also allow manual triggering.
push:
branches:
- main
paths:
- go.mod
- .github/licenses.tmpl
- .github/workflows/go-licenses.yml
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
update-licenses:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
- name: Install go-licenses
run: |
go install github.com/google/go-licenses@v1.2.2-0.20220825154955-5eedde1c6584
- name: Run go-licenses
env:
# include all build tags to include platform-specific dependencies
GOFLAGS: "-tags=android,cgo,darwin,freebsd,ios,js,linux,openbsd,wasm,windows"
run: |
[ -d licenses ] || mkdir licenses
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
- name: Get access token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
installation_id: ${{ secrets.LICENSING_APP_INSTALLATION_ID }}
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply+license-updater@tailscale.com>
committer: License Updater <noreply+license-updater@tailscale.com>
branch: licenses/cli
commit-message: "licenses: update tailscale{,d} licenses"
title: "licenses: update tailscale{,d} licenses"
body: Triggered by ${{ github.repository }}@${{ github.sha }}
signoff: true
delete-branch: true
team-reviewers: opensource-license-reviewers

View File

@@ -2,8 +2,8 @@ name: "Kubernetes manifests"
on:
pull_request:
paths:
- './cmd/k8s-operator/**'
- './k8s-operator/**'
- 'cmd/k8s-operator/**'
- 'k8s-operator/**'
- '.github/workflows/kubemanifests.yaml'
# Cancel workflow run if there is a newer push to the same PR for which it is

View File

@@ -206,7 +206,7 @@ jobs:
- name: Run VM tests
run: ./tool/go test ./tstest/integration/vms -v -no-s3 -run-vm-tests -run=TestRunUbuntu2004
env:
HOME: "/tmp"
HOME: "/var/lib/ghrunner/home"
TMPDIR: "/tmp"
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
@@ -254,9 +254,6 @@ jobs:
goarch: amd64
- goos: openbsd
goarch: amd64
# Plan9 (disabled until 3p dependencies are fixed)
# - goos: plan9
# goarch: amd64
runs-on: ubuntu-22.04
steps:
@@ -305,6 +302,47 @@ jobs:
GOOS: ios
GOARCH: arm64
crossmin: # cross-compile for platforms where we only check cmd/tailscale{,d}
strategy:
fail-fast: false # don't abort the entire matrix if one element fails
matrix:
include:
# Plan9
- goos: plan9
goarch: amd64
# AIX
- goos: aix
goarch: ppc64
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v4
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# 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: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-
- name: build core
run: ./tool/go build ./cmd/tailscale ./cmd/tailscaled
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "0"
android:
# similar to cross above, but android fails to build a few pieces of the
# repo. We should fix those pieces, they're small, but as a stepping stone,

View File

@@ -1,5 +1,5 @@
IMAGE_REPO ?= tailscale/tailscale
SYNO_ARCH ?= "amd64"
SYNO_ARCH ?= "x86_64"
SYNO_DSM ?= "7"
TAGS ?= "latest"

View File

@@ -1 +1 @@
1.61.0
1.65.0

424
api.md

File diff suppressed because it is too large Load Diff

View File

@@ -20,9 +20,9 @@
set -eu
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
export PATH=$PWD/tool:$PATH
export PATH="$PWD"/tool:"$PATH"
eval $(./build_dist.sh shellvars)
eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
@@ -74,4 +74,4 @@ case "$TARGET" in
echo "unknown target: $TARGET"
exit 1
;;
esac
esac

View File

@@ -270,6 +270,14 @@ type UserRuleMatch struct {
Users []string `json:"users"`
Ports []string `json:"ports"`
LineNumber int `json:"lineNumber"`
// Postures is a list of posture policies that are
// associated with this match. The rules can be looked
// up in the ACLPreviewResponse parent struct.
// The source of the list is from srcPosture on
// an ACL or Grant rule:
// https://tailscale.com/kb/1288/device-posture#posture-conditions
Postures []string `json:"postures"`
}
// ACLPreviewResponse is the response type of previewACLPostRequest
@@ -277,6 +285,12 @@ type ACLPreviewResponse struct {
Matches []UserRuleMatch `json:"matches"` // ACL rules that match the specified user or ipport.
Type string `json:"type"` // The request type: currently only "user" or "ipport".
PreviewFor string `json:"previewFor"` // A specific user or ipport.
// Postures is a map of postures and associated rules that apply
// to this preview.
// For more details about the posture mapping, see:
// https://tailscale.com/kb/1288/device-posture#postures
Postures map[string][]string `json:"postures,omitempty"`
}
// ACLPreview is the response type of PreviewACLForUser, PreviewACLForIPPort, PreviewACLHuJSONForUser, and PreviewACLHuJSONForIPPort
@@ -284,6 +298,12 @@ type ACLPreview struct {
Matches []UserRuleMatch `json:"matches"`
User string `json:"user,omitempty"` // Filled if response of PreviewACLForUser or PreviewACLHuJSONForUser
IPPort string `json:"ipport,omitempty"` // Filled if response of PreviewACLForIPPort or PreviewACLHuJSONForIPPort
// Postures is a map of postures and associated rules that apply
// to this preview.
// For more details about the posture mapping, see:
// https://tailscale.com/kb/1288/device-posture#postures
Postures map[string][]string `json:"postures,omitempty"`
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
@@ -341,8 +361,9 @@ func (c *Client) PreviewACLForUser(ctx context.Context, acl ACL, user string) (r
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
Matches: b.Matches,
User: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -369,8 +390,9 @@ func (c *Client) PreviewACLForIPPort(ctx context.Context, acl ACL, ipport netip.
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
Matches: b.Matches,
IPPort: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -394,8 +416,9 @@ func (c *Client) PreviewACLHuJSONForUser(ctx context.Context, acl ACLHuJSON, use
}
return &ACLPreview{
Matches: b.Matches,
User: b.PreviewFor,
Matches: b.Matches,
User: b.PreviewFor,
Postures: b.Postures,
}, nil
}
@@ -419,8 +442,9 @@ func (c *Client) PreviewACLHuJSONForIPPort(ctx context.Context, acl ACLHuJSON, i
}
return &ACLPreview{
Matches: b.Matches,
IPPort: b.PreviewFor,
Matches: b.Matches,
IPPort: b.PreviewFor,
Postures: b.Postures,
}, nil
}

View File

@@ -49,3 +49,11 @@ type ReloadConfigResponse struct {
Reloaded bool // whether the config was reloaded
Err string // any error message
}
// ExitNodeSuggestionResponse is the response to a LocalAPI suggest-exit-node GET request.
// It returns the StableNodeID, name, and location of a suggested exit node for the client making the request.
type ExitNodeSuggestionResponse struct {
ID tailcfg.StableNodeID
Name string
Location tailcfg.LocationView `json:",omitempty"`
}

View File

@@ -28,6 +28,7 @@ import (
"go4.org/mem"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/drive"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -35,7 +36,6 @@ import (
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
"tailscale.com/tailfs"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
@@ -1418,44 +1418,62 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
return &cv, nil
}
// TailFSSetFileServerAddr instructs TailFS to use the server at addr to access
// SetUseExitNode toggles the use of an exit node on or off.
// To turn it on, there must have been a previously used exit node.
// The most previously used one is reused.
// This is a convenience method for GUIs. To select an actual one, update the prefs.
func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
return err
}
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// TailFS know to use the file server running in the GUI app.
func (lc *LocalClient) TailFSSetFileServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/fileserver-address", http.StatusCreated, strings.NewReader(addr))
// Taildrive know to use the file server running in the GUI app.
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err
}
// TailFSShareAdd adds the given share to the list of shares that TailFS will
// serve to remote nodes. If a share with the same name already exists, the
// existing share is replaced/updated.
func (lc *LocalClient) TailFSShareAdd(ctx context.Context, share *tailfs.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/tailfs/shares", http.StatusCreated, jsonBody(share))
// DriveShareSet adds or updates the given share in the list of shares that
// Taildrive will serve to remote nodes. If a share with the same name already
// exists, the existing share is replaced/updated.
func (lc *LocalClient) DriveShareSet(ctx context.Context, share *drive.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
return err
}
// TailFSShareRemove removes the share with the given name from the list of
// shares that TailFS will serve to remote nodes.
func (lc *LocalClient) TailFSShareRemove(ctx context.Context, name string) error {
// DriveShareRemove removes the share with the given name from the list of
// shares that Taildrive will serve to remote nodes.
func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error {
_, err := lc.send(
ctx,
"DELETE",
"/localapi/v0/tailfs/shares",
"/localapi/v0/drive/shares",
http.StatusNoContent,
jsonBody(&tailfs.Share{
Name: name,
}))
strings.NewReader(name))
return err
}
// TailFSShareList returns the list of shares that TailFS is currently serving
// DriveShareRename renames the share from old to new name.
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
_, err := lc.send(
ctx,
"POST",
"/localapi/v0/drive/shares",
http.StatusNoContent,
jsonBody([2]string{oldName, newName}))
return err
}
// DriveShareList returns the list of shares that drive is currently serving
// to remote nodes.
func (lc *LocalClient) TailFSShareList(ctx context.Context) (map[string]*tailfs.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/tailfs/shares")
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
if err != nil {
return nil, err
}
var shares map[string]*tailfs.Share
var shares []*drive.Share
err = json.Unmarshal(result, &shares)
return shares, err
}
@@ -1496,3 +1514,12 @@ func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
}
return n, nil
}
// SuggestExitNode requests an exit node suggestion and returns the exit node's details.
func (lc *LocalClient) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/suggest-exit-node")
if err != nil {
return apitype.ExitNodeSuggestionResponse{}, err
}
return decodeJSON[apitype.ExitNodeSuggestionResponse](body)
}

View File

@@ -34,9 +34,9 @@ func TestDeps(t *testing.T) {
deptest.DepChecker{
BadDeps: map[string]string{
// Make sure we don't again accidentally bring in a dependency on
// TailFS or its transitive dependencies
"tailscale.com/tailfs/tailfsimpl": "https://github.com/tailscale/tailscale/pull/10631",
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
// drive or its transitive dependencies
"tailscale.com/drive/driveimpl": "https://github.com/tailscale/tailscale/pull/10631",
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
},
}.Check(t)
}

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"time"
@@ -222,7 +223,7 @@ func (s *Server) awaitUserAuth(ctx context.Context, session *browserSession) err
func (s *Server) newSessionID() (string, error) {
raw := make([]byte, 16)
for i := 0; i < 5; i++ {
for range 5 {
if _, err := rand.Read(raw); err != nil {
return "", err
}
@@ -234,7 +235,12 @@ func (s *Server) newSessionID() (string, error) {
return "", errors.New("too many collisions generating new session; please refresh page")
}
type peerCapabilities map[capFeature]bool // value is true if the peer can edit the given feature
// peerCapabilities holds information about what a source
// peer is allowed to edit via the web UI.
//
// map value is true if the peer can edit the given feature.
// Only capFeatures included in validCaps will be included.
type peerCapabilities map[capFeature]bool
// canEdit is true if the peerCapabilities grant edit access
// to the given feature.
@@ -248,21 +254,47 @@ func (p peerCapabilities) canEdit(feature capFeature) bool {
return p[feature]
}
// isEmpty is true if p is either nil or has no capabilities
// with value true.
func (p peerCapabilities) isEmpty() bool {
if p == nil {
return true
}
for _, v := range p {
if v == true {
return false
}
}
return true
}
type capFeature string
const (
// The following values should not be edited.
// New caps can be added, but existing ones should not be changed,
// as these exact values are used by users in tailnet policy files.
//
// IMPORTANT: When adding a new cap, also update validCaps slice below.
capFeatureAll capFeature = "*" // grants peer management of all features
capFeatureFunnel capFeature = "funnel" // grants peer serve/funnel management
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnet capFeature = "subnet" // grants peer subnet routes management
capFeatureExitNode capFeature = "exitnode" // grants peer ability to advertise-as and use exit nodes
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
capFeatureAll capFeature = "*" // grants peer management of all features
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnets capFeature = "subnets" // grants peer subnet routes management
capFeatureExitNodes capFeature = "exitnodes" // grants peer ability to advertise-as and use exit nodes
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
)
// validCaps contains the list of valid capabilities used in the web client.
// Any capabilities included in a peer's grants that do not fall into this
// list will be ignored.
var validCaps []capFeature = []capFeature{
capFeatureAll,
capFeatureSSH,
capFeatureSubnets,
capFeatureExitNodes,
capFeatureAccount,
}
type capRule struct {
CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
}
@@ -270,7 +302,13 @@ type capRule struct {
// toPeerCapabilities parses out the web ui capabilities from the
// given whois response.
func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) (peerCapabilities, error) {
if whois == nil {
if whois == nil || status == nil {
return peerCapabilities{}, nil
}
if whois.Node.IsTagged() {
// We don't allow management *from* tagged nodes, so ignore caps.
// The web client auth flow relies on having a true user identity
// that can be verified through login.
return peerCapabilities{}, nil
}
@@ -291,7 +329,10 @@ func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) (
}
for _, c := range rules {
for _, f := range c.CanEdit {
caps[capFeature(strings.ToLower(f))] = true
cap := capFeature(strings.ToLower(f))
if slices.Contains(validCaps, cap) {
caps[cap] = true
}
}
}
return caps, nil

View File

@@ -6,6 +6,7 @@
"node": "18.16.1",
"yarn": "1.22.19"
},
"type": "module",
"private": true,
"dependencies": {
"@radix-ui/react-collapsible": "^1.0.3",
@@ -32,12 +33,16 @@
"prettier": "^2.5.1",
"prettier-plugin-organize-imports": "^3.2.2",
"tailwindcss": "^3.3.3",
"typescript": "^4.7.4",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^3.5.0",
"vitest": "^1.3.1"
},
"resolutions": {
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1"
},
"scripts": {
"build": "vite build",
"start": "vite",

View File

@@ -11,8 +11,8 @@ import LoginView from "src/components/views/login-view"
import SSHView from "src/components/views/ssh-view"
import SubnetRouterView from "src/components/views/subnet-router-view"
import { UpdatingView } from "src/components/views/updating-view"
import useAuth, { AuthResponse } from "src/hooks/auth"
import { Feature, featureDescription, NodeData } from "src/types"
import useAuth, { AuthResponse, canEdit } from "src/hooks/auth"
import { Feature, NodeData, featureDescription } from "src/types"
import Card from "src/ui/card"
import EmptyState from "src/ui/empty-state"
import LoadingDots from "src/ui/loading-dots"
@@ -56,16 +56,19 @@ function WebClient({
<Header node={node} auth={auth} newSession={newSession} />
<Switch>
<Route path="/">
<HomeView readonly={!auth.canManageNode} node={node} />
<HomeView node={node} auth={auth} />
</Route>
<Route path="/details">
<DeviceDetailsView readonly={!auth.canManageNode} node={node} />
<DeviceDetailsView node={node} auth={auth} />
</Route>
<FeatureRoute path="/subnets" feature="advertise-routes" node={node}>
<SubnetRouterView readonly={!auth.canManageNode} node={node} />
<SubnetRouterView
readonly={!canEdit("subnets", auth)}
node={node}
/>
</FeatureRoute>
<FeatureRoute path="/ssh" feature="ssh" node={node}>
<SSHView readonly={!auth.canManageNode} node={node} />
<SSHView readonly={!canEdit("ssh", auth)} node={node} />
</FeatureRoute>
{/* <Route path="/serve">Share local content</Route> */}
<FeatureRoute path="/update" feature="auto-update" node={node}>

View File

@@ -2,15 +2,17 @@
// SPDX-License-Identifier: BSD-3-Clause
import cx from "classnames"
import React, { useCallback, useEffect, useState } from "react"
import React, { useCallback, useMemo, useState } from "react"
import ChevronDown from "src/assets/icons/chevron-down.svg?react"
import Eye from "src/assets/icons/eye.svg?react"
import User from "src/assets/icons/user.svg?react"
import { AuthResponse, AuthType } from "src/hooks/auth"
import { AuthResponse, hasAnyEditCapabilities } from "src/hooks/auth"
import { useTSWebConnected } from "src/hooks/ts-web-connected"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Popover from "src/ui/popover"
import ProfilePic from "src/ui/profile-pic"
import { assertNever, isHTTPS } from "src/utils/util"
export default function LoginToggle({
node,
@@ -22,12 +24,29 @@ export default function LoginToggle({
newSession: () => Promise<void>
}) {
const [open, setOpen] = useState<boolean>(false)
const { tsWebConnected, checkTSWebConnection } = useTSWebConnected(
auth.serverMode,
node.IPv4
)
return (
<Popover
className="p-3 bg-white rounded-lg shadow flex flex-col gap-2 max-w-[317px]"
className="p-3 bg-white rounded-lg shadow flex flex-col max-w-[317px]"
content={
<LoginPopoverContent node={node} auth={auth} newSession={newSession} />
auth.serverMode === "readonly" ? (
<ReadonlyModeContent auth={auth} />
) : auth.serverMode === "login" ? (
<LoginModeContent
auth={auth}
node={node}
tsWebConnected={tsWebConnected}
checkTSWebConnection={checkTSWebConnection}
/>
) : auth.serverMode === "manage" ? (
<ManageModeContent auth={auth} node={node} newSession={newSession} />
) : (
assertNever(auth.serverMode)
)
}
side="bottom"
align="end"
@@ -35,228 +54,303 @@ export default function LoginToggle({
onOpenChange={setOpen}
asChild
>
{!auth.canManageNode ? (
<button
className={cx(
"pl-3 py-1 bg-gray-700 rounded-full flex justify-start items-center h-[34px]",
{ "pr-1": auth.viewerIdentity, "pr-3": !auth.viewerIdentity }
)}
onClick={() => setOpen(!open)}
>
<Eye />
<div className="text-white leading-snug ml-2 mr-1">Viewing</div>
<ChevronDown className="stroke-white w-[15px] h-[15px]" />
{auth.viewerIdentity && (
<ProfilePic
className="ml-2"
size="medium"
url={auth.viewerIdentity.profilePicUrl}
/>
)}
</button>
) : (
<div
className={cx(
"w-[34px] h-[34px] p-1 rounded-full justify-center items-center inline-flex hover:bg-gray-300",
{
"bg-transparent": !open,
"bg-gray-300": open,
}
)}
>
<button onClick={() => setOpen(!open)}>
<ProfilePic
size="medium"
url={auth.viewerIdentity?.profilePicUrl}
/>
</button>
</div>
)}
<div>
{auth.authorized ? (
<TriggerWhenManaging auth={auth} open={open} setOpen={setOpen} />
) : (
<TriggerWhenReading auth={auth} open={open} setOpen={setOpen} />
)}
</div>
</Popover>
)
}
function LoginPopoverContent({
/**
* TriggerWhenManaging is displayed as the trigger for the login popover
* when the user has an active authorized managment session.
*/
function TriggerWhenManaging({
auth,
open,
setOpen,
}: {
auth: AuthResponse
open: boolean
setOpen: (next: boolean) => void
}) {
return (
<div
className={cx(
"w-[34px] h-[34px] p-1 rounded-full justify-center items-center inline-flex hover:bg-gray-300",
{
"bg-transparent": !open,
"bg-gray-300": open,
}
)}
>
<button onClick={() => setOpen(!open)}>
<ProfilePic size="medium" url={auth.viewerIdentity?.profilePicUrl} />
</button>
</div>
)
}
/**
* TriggerWhenReading is displayed as the trigger for the login popover
* when the user is currently in read mode (doesn't have an authorized
* management session).
*/
function TriggerWhenReading({
auth,
open,
setOpen,
}: {
auth: AuthResponse
open: boolean
setOpen: (next: boolean) => void
}) {
return (
<button
className={cx(
"pl-3 py-1 bg-gray-700 rounded-full flex justify-start items-center h-[34px]",
{ "pr-1": auth.viewerIdentity, "pr-3": !auth.viewerIdentity }
)}
onClick={() => setOpen(!open)}
>
<Eye />
<div className="text-white leading-snug ml-2 mr-1">Viewing</div>
<ChevronDown className="stroke-white w-[15px] h-[15px]" />
{auth.viewerIdentity && (
<ProfilePic
className="ml-2"
size="medium"
url={auth.viewerIdentity.profilePicUrl}
/>
)}
</button>
)
}
/**
* PopoverContentHeader is the header for the login popover.
*/
function PopoverContentHeader({ auth }: { auth: AuthResponse }) {
return (
<div className="text-black text-sm font-medium leading-tight mb-1">
{auth.authorized ? "Managing" : "Viewing"}
{auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
</div>
)
}
/**
* PopoverContentFooter is the footer for the login popover.
*/
function PopoverContentFooter({ auth }: { auth: AuthResponse }) {
return auth.viewerIdentity ? (
<>
<hr className="my-2" />
<div className="flex items-center">
<User className="flex-shrink-0" />
<p className="text-gray-500 text-xs ml-2">
We recognize you because you are accessing this page from{" "}
<span className="font-medium">
{auth.viewerIdentity.nodeName || auth.viewerIdentity.nodeIP}
</span>
</p>
</div>
</>
) : null
}
/**
* ReadonlyModeContent is the body of the login popover when the web
* client is being run in "readonly" server mode.
*/
function ReadonlyModeContent({ auth }: { auth: AuthResponse }) {
return (
<>
<PopoverContentHeader auth={auth} />
<p className="text-gray-500 text-xs">
This web interface is running in read-only mode.{" "}
<a
href="https://tailscale.com/s/web-client-read-only"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
<PopoverContentFooter auth={auth} />
</>
)
}
/**
* LoginModeContent is the body of the login popover when the web
* client is being run in "login" server mode.
*/
function LoginModeContent({
node,
auth,
tsWebConnected,
checkTSWebConnection,
}: {
node: NodeData
auth: AuthResponse
tsWebConnected: boolean
checkTSWebConnection: () => void
}) {
const https = isHTTPS()
// We can't run the ts web connection test when the webpage is loaded
// over HTTPS. So in this case, we default to presenting a login button
// with some helper text reminding the user to check their connection
// themselves.
const hasACLAccess = https || tsWebConnected
const hasEditCaps = useMemo(() => {
if (!auth.viewerIdentity) {
// If not connected to login client over tailscale, we won't know the viewer's
// identity. So we must assume they may be able to edit something and have the
// management client handle permissions once the user gets there.
return true
}
return hasAnyEditCapabilities(auth)
}, [auth])
const handleLogin = useCallback(() => {
// Must be connected over Tailscale to log in.
// Send user to Tailscale IP and start check mode
const manageURL = `http://${node.IPv4}:5252/?check=now`
if (window.self !== window.top) {
// If we're inside an iframe, open management client in new window.
window.open(manageURL, "_blank")
} else {
window.location.href = manageURL
}
}, [node.IPv4])
return (
<div
onMouseEnter={
hasEditCaps && !hasACLAccess ? checkTSWebConnection : undefined
}
>
<PopoverContentHeader auth={auth} />
{!hasACLAccess || !hasEditCaps ? (
<>
<p className="text-gray-500 text-xs">
{!hasEditCaps ? (
// ACLs allow access, but user isn't allowed to edit any features,
// restricted to readonly. No point in sending them over to the
// tailscaleIP:5252 address.
<>
You dont have permission to make changes to this device, but
you can view most of its details.
</>
) : !node.ACLAllowsAnyIncomingTraffic ? (
// Tailnet ACLs don't allow access to anyone.
<>
The current tailnet policy file does not allow connecting to
this device.
</>
) : (
// ACLs don't allow access to this user specifically.
<>
Cannot access this devices Tailscale IP. Make sure you are
connected to your tailnet, and that your policy file allows
access.
</>
)}{" "}
<a
href="https://tailscale.com/s/web-client-access"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
</>
) : (
// User can connect to Tailcale IP; sign in when ready.
<>
<p className="text-gray-500 text-xs">
You can see most of this devices details. To make changes, you need
to sign in.
</p>
{https && (
// we don't know if the user can connect over TS, so
// provide extra tips in case they have trouble.
<p className="text-gray-500 text-xs font-semibold pt-2">
Make sure you are connected to your tailnet, and that your policy
file allows access.
</p>
)}
<SignInButton auth={auth} onClick={handleLogin} />
</>
)}
<PopoverContentFooter auth={auth} />
</div>
)
}
/**
* ManageModeContent is the body of the login popover when the web
* client is being run in "manage" server mode.
*/
function ManageModeContent({
auth,
newSession,
}: {
node: NodeData
auth: AuthResponse
newSession: () => Promise<void>
newSession: () => void
}) {
/**
* canConnectOverTS indicates whether the current viewer
* is able to hit the node's web client that's being served
* at http://${node.IP}:5252. If false, this means that the
* viewer must connect to the correct tailnet before being
* able to sign in.
*/
const [canConnectOverTS, setCanConnectOverTS] = useState<boolean>(false)
const [isRunningCheck, setIsRunningCheck] = useState<boolean>(false)
// Whether the current page is loaded over HTTPS.
// If it is, then the connectivity check to the management client
// will fail with a mixed-content error.
const isHTTPS = window.location.protocol === "https:"
const checkTSConnection = useCallback(() => {
if (auth.viewerIdentity || isHTTPS) {
// Skip the connectivity check if we either already know we're connected over Tailscale,
// or know the connectivity check will fail because the current page is loaded over HTTPS.
setCanConnectOverTS(true)
return
}
// Otherwise, test connection to the ts IP.
if (isRunningCheck) {
return // already checking
}
setIsRunningCheck(true)
fetch(`http://${node.IPv4}:5252/ok`, { mode: "no-cors" })
.then(() => {
setCanConnectOverTS(true)
setIsRunningCheck(false)
})
.catch(() => setIsRunningCheck(false))
}, [auth.viewerIdentity, isRunningCheck, node.IPv4, isHTTPS])
/**
* Checking connection for first time on page load.
*
* While not connected, we check again whenever the mouse
* enters the popover component, to pick up on the user
* leaving to turn on Tailscale then returning to the view.
* See `onMouseEnter` on the div below.
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => checkTSConnection(), [])
const handleSignInClick = useCallback(() => {
if (auth.viewerIdentity && auth.serverMode === "manage") {
if (window.self !== window.top) {
// if we're inside an iframe, start session in new window
let url = new URL(window.location.href)
url.searchParams.set("check", "now")
window.open(url, "_blank")
} else {
newSession()
}
const handleLogin = useCallback(() => {
if (window.self !== window.top) {
// If we're inside an iframe, start session in new window.
let url = new URL(window.location.href)
url.searchParams.set("check", "now")
window.open(url, "_blank")
} else {
// Must be connected over Tailscale to log in.
// Send user to Tailscale IP and start check mode
const manageURL = `http://${node.IPv4}:5252/?check=now`
if (window.self !== window.top) {
// if we're inside an iframe, open management client in new window
window.open(manageURL, "_blank")
} else {
window.location.href = manageURL
}
newSession()
}
}, [auth.viewerIdentity, auth.serverMode, newSession, node.IPv4])
}, [newSession])
const hasAnyPermissions = useMemo(() => hasAnyEditCapabilities(auth), [auth])
return (
<div onMouseEnter={!canConnectOverTS ? checkTSConnection : undefined}>
<div className="text-black text-sm font-medium leading-tight mb-1">
{!auth.canManageNode ? "Viewing" : "Managing"}
{auth.viewerIdentity && ` as ${auth.viewerIdentity.loginName}`}
</div>
{!auth.canManageNode && (
<>
{auth.serverMode === "readonly" ? (
<>
<PopoverContentHeader auth={auth} />
{!auth.authorized &&
(hasAnyPermissions ? (
// User is connected over Tailscale, but needs to complete check mode.
<>
<p className="text-gray-500 text-xs">
This web interface is running in read-only mode.{" "}
<a
href="https://tailscale.com/s/web-client-read-only"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
To make changes, sign in to confirm your identity. This extra step
helps us keep your device secure.
</p>
) : !auth.viewerIdentity ? (
// User is not connected over Tailscale.
// These states are only possible on the login client.
<>
{!canConnectOverTS ? (
<>
<p className="text-gray-500 text-xs">
{!node.ACLAllowsAnyIncomingTraffic ? (
// Tailnet ACLs don't allow access.
<>
The current tailnet policy file does not allow
connecting to this device.
</>
) : (
// ACLs allow access, but user can't connect.
<>
Cannot access this devices Tailscale IP. Make sure you
are connected to your tailnet, and that your policy file
allows access.
</>
)}{" "}
<a
href="https://tailscale.com/s/web-client-connection"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
</>
) : (
// User can connect to Tailcale IP; sign in when ready.
<>
<p className="text-gray-500 text-xs">
You can see most of this devices details. To make changes,
you need to sign in.
</p>
{isHTTPS && (
// we don't know if the user can connect over TS, so
// provide extra tips in case they have trouble.
<p className="text-gray-500 text-xs font-semibold pt-2">
Make sure you are connected to your tailnet, and that your
policy file allows access.
</p>
)}
<SignInButton auth={auth} onClick={handleSignInClick} />
</>
)}
</>
) : auth.authNeeded === AuthType.tailscale ? (
// User is connected over Tailscale, but needs to complete check mode.
<>
<p className="text-gray-500 text-xs">
To make changes, sign in to confirm your identity. This extra
step helps us keep your device secure.
</p>
<SignInButton auth={auth} onClick={handleSignInClick} />
</>
) : (
// User is connected over tailscale, but doesn't have permission to manage.
<p className="text-gray-500 text-xs">
You dont have permission to make changes to this device, but you
can view most of its details.
</p>
)}
</>
)}
{auth.viewerIdentity && (
<>
<hr className="my-2" />
<div className="flex items-center">
<User className="flex-shrink-0" />
<p className="text-gray-500 text-xs ml-2">
We recognize you because you are accessing this page from{" "}
<span className="font-medium">
{auth.viewerIdentity.nodeName || auth.viewerIdentity.nodeIP}
</span>
</p>
</div>
</>
)}
</div>
<SignInButton auth={auth} onClick={handleLogin} />
</>
) : (
// User is connected over tailscale, but doesn't have permission to manage.
<p className="text-gray-500 text-xs">
You dont have permission to make changes to this device, but you
can view most of its details.{" "}
<a
href="https://tailscale.com/s/web-client-access"
className="text-blue-700"
target="_blank"
rel="noreferrer"
>
Learn more &rarr;
</a>
</p>
))}
<PopoverContentFooter auth={auth} />
</>
)
}

View File

@@ -8,6 +8,7 @@ import ACLTag from "src/components/acl-tag"
import * as Control from "src/components/control-components"
import NiceIP from "src/components/nice-ip"
import { UpdateAvailableNotification } from "src/components/update-available"
import { AuthResponse, canEdit } from "src/hooks/auth"
import { NodeData } from "src/types"
import Button from "src/ui/button"
import Card from "src/ui/card"
@@ -16,11 +17,11 @@ import QuickCopy from "src/ui/quick-copy"
import { useLocation } from "wouter"
export default function DeviceDetailsView({
readonly,
node,
auth,
}: {
readonly: boolean
node: NodeData
auth: AuthResponse
}) {
return (
<>
@@ -37,11 +38,11 @@ export default function DeviceDetailsView({
})}
/>
</div>
{!readonly && <DisconnectDialog />}
{canEdit("account", auth) && <DisconnectDialog />}
</div>
</Card>
{node.Features["auto-update"] &&
!readonly &&
canEdit("account", auth) &&
node.ClientVersion &&
!node.ClientVersion.RunningLatest && (
<UpdateAvailableNotification details={node.ClientVersion} />

View File

@@ -8,17 +8,18 @@ import ArrowRight from "src/assets/icons/arrow-right.svg?react"
import Machine from "src/assets/icons/machine.svg?react"
import AddressCard from "src/components/address-copy-card"
import ExitNodeSelector from "src/components/exit-node-selector"
import { AuthResponse, canEdit } from "src/hooks/auth"
import { NodeData } from "src/types"
import Card from "src/ui/card"
import { pluralize } from "src/utils/util"
import { Link, useLocation } from "wouter"
export default function HomeView({
readonly,
node,
auth,
}: {
readonly: boolean
node: NodeData
auth: AuthResponse
}) {
const [allSubnetRoutes, pendingSubnetRoutes] = useMemo(
() => [
@@ -63,7 +64,11 @@ export default function HomeView({
</div>
{(node.Features["advertise-exit-node"] ||
node.Features["use-exit-node"]) && (
<ExitNodeSelector className="mb-5" node={node} disabled={readonly} />
<ExitNodeSelector
className="mb-5"
node={node}
disabled={!canEdit("exitnodes", auth)}
/>
)}
<Link
className="link font-medium"

View File

@@ -4,25 +4,50 @@
import { useCallback, useEffect, useState } from "react"
import { apiFetch, setSynoToken } from "src/api"
export enum AuthType {
synology = "synology",
tailscale = "tailscale",
}
export type AuthResponse = {
authNeeded?: AuthType
canManageNode: boolean
serverMode: "login" | "readonly" | "manage"
serverMode: AuthServerMode
authorized: boolean
viewerIdentity?: {
loginName: string
nodeName: string
nodeIP: string
profilePicUrl?: string
capabilities: { [key in PeerCapability]: boolean }
}
needsSynoAuth?: boolean
}
// useAuth reports and refreshes Tailscale auth status
// for the web client.
export type AuthServerMode = "login" | "readonly" | "manage"
export type PeerCapability = "*" | "ssh" | "subnets" | "exitnodes" | "account"
/**
* canEdit reports whether the given auth response specifies that the viewer
* has the ability to edit the given capability.
*/
export function canEdit(cap: PeerCapability, auth: AuthResponse): boolean {
if (!auth.authorized || !auth.viewerIdentity) {
return false
}
if (auth.viewerIdentity.capabilities["*"] === true) {
return true // can edit all features
}
return auth.viewerIdentity.capabilities[cap] === true
}
/**
* hasAnyEditCapabilities reports whether the given auth response specifies
* that the viewer has at least one edit capability. If this is true, the
* user is able to go through the auth flow to authenticate a management
* session.
*/
export function hasAnyEditCapabilities(auth: AuthResponse): boolean {
return Object.values(auth.viewerIdentity?.capabilities || {}).includes(true)
}
/**
* useAuth reports and refreshes Tailscale auth status for the web client.
*/
export default function useAuth() {
const [data, setData] = useState<AuthResponse>()
const [loading, setLoading] = useState<boolean>(true)
@@ -33,18 +58,16 @@ export default function useAuth() {
return apiFetch<AuthResponse>("/auth", "GET")
.then((d) => {
setData(d)
switch (d.authNeeded) {
case AuthType.synology:
fetch("/webman/login.cgi")
.then((r) => r.json())
.then((a) => {
setSynoToken(a.SynoToken)
setRanSynoAuth(true)
setLoading(false)
})
break
default:
setLoading(false)
if (d.needsSynoAuth) {
fetch("/webman/login.cgi")
.then((r) => r.json())
.then((a) => {
setSynoToken(a.SynoToken)
setRanSynoAuth(true)
setLoading(false)
})
} else {
setLoading(false)
}
return d
})
@@ -72,8 +95,13 @@ export default function useAuth() {
useEffect(() => {
loadAuth().then((d) => {
if (!d) {
return
}
if (
!d?.canManageNode &&
!d.authorized &&
hasAnyEditCapabilities(d) &&
// Start auth flow immediately if browser has requested it.
new URLSearchParams(window.location.search).get("check") === "now"
) {
newSession()

View File

@@ -0,0 +1,46 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import { useCallback, useEffect, useState } from "react"
import { isHTTPS } from "src/utils/util"
import { AuthServerMode } from "./auth"
/**
* useTSWebConnected hook is used to check whether the browser is able to
* connect to the web client served at http://${nodeIPv4}:5252
*/
export function useTSWebConnected(mode: AuthServerMode, nodeIPv4: string) {
const [tsWebConnected, setTSWebConnected] = useState<boolean>(
mode === "manage" // browser already on the web client
)
const [isLoading, setIsLoading] = useState<boolean>(false)
const checkTSWebConnection = useCallback(() => {
if (mode === "manage") {
// Already connected to the web client.
setTSWebConnected(true)
return
}
if (isHTTPS()) {
// When page is loaded over HTTPS, the connectivity check will always
// fail with a mixed-content error. In this case don't bother doing
// the check.
return
}
if (isLoading) {
return // already checking
}
setIsLoading(true)
fetch(`http://${nodeIPv4}:5252/ok`, { mode: "no-cors" })
.then(() => {
setTSWebConnected(true)
setIsLoading(false)
})
.catch(() => setIsLoading(false))
}, [isLoading, mode, nodeIPv4])
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => checkTSWebConnection(), []) // checking connection for first time on page load
return { tsWebConnected, checkTSWebConnection, isLoading }
}

View File

@@ -49,3 +49,10 @@ export function isPromise<T = unknown>(val: unknown): val is Promise<T> {
}
return typeof val === "object" && "then" in val
}
/**
* isHTTPS reports whether the current page is loaded over HTTPS.
*/
export function isHTTPS() {
return window.location.protocol === "https:"
}

View File

@@ -1,7 +1,7 @@
const plugin = require("tailwindcss/plugin")
const styles = require("./styles.json")
import plugin from "tailwindcss/plugin"
import styles from "./styles.json"
module.exports = {
const config = {
theme: {
screens: {
sm: "420px",
@@ -96,20 +96,22 @@ module.exports = {
plugins: [
plugin(function ({ addVariant }) {
addVariant("state-open", [
'&[data-state="open"]',
'[data-state="open"] &',
"&[data-state=open”]",
"[data-state=open] &",
])
addVariant("state-closed", [
'&[data-state="closed"]',
'[data-state="closed"] &',
"&[data-state=closed”]",
"[data-state=closed] &",
])
addVariant("state-delayed-open", [
'&[data-state="delayed-open"]',
'[data-state="delayed-open"] &',
"&[data-state=delayed-open”]",
"[data-state=delayed-open] &",
])
addVariant("state-active", ['&[data-state="active"]'])
addVariant("state-inactive", ['&[data-state="inactive"]'])
addVariant("state-active", ["&[data-state=active”]"])
addVariant("state-inactive", ["&[data-state=inactive”]"])
}),
],
content: ["./src/**/*.html", "./src/**/*.{ts,tsx}", "./index.html"],
}
export default config

View File

@@ -445,18 +445,188 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
}
}
type authType string
type apiHandler[data any] struct {
s *Server
w http.ResponseWriter
r *http.Request
var (
synoAuth authType = "synology" // user needs a SynoToken for subsequent API calls
tailscaleAuth authType = "tailscale" // user needs to complete Tailscale check mode
)
// permissionCheck allows for defining whether a requesting peer's
// capabilities grant them access to make the given data update.
// If permissionCheck reports false, the request fails as unauthorized.
permissionCheck func(data data, peer peerCapabilities) bool
}
// newHandler constructs a new api handler which restricts the given request
// to the specified permission check. If the permission check fails for
// the peer associated with the request, an unauthorized error is returned
// to the client.
func newHandler[data any](s *Server, w http.ResponseWriter, r *http.Request, permissionCheck func(data data, peer peerCapabilities) bool) *apiHandler[data] {
return &apiHandler[data]{
s: s,
w: w,
r: r,
permissionCheck: permissionCheck,
}
}
// alwaysAllowed can be passed as the permissionCheck argument to newHandler
// for requests that are always allowed to complete regardless of a peer's
// capabilities.
func alwaysAllowed[data any](_ data, _ peerCapabilities) bool { return true }
func (a *apiHandler[data]) getPeer() (peerCapabilities, error) {
// TODO(tailscale/corp#16695,sonia): We also call StatusWithoutPeers and
// WhoIs when originally checking for a session from authorizeRequest.
// Would be nice if we could pipe those through to here so we don't end
// up having to re-call them to grab the peer capabilities.
status, err := a.s.lc.StatusWithoutPeers(a.r.Context())
if err != nil {
return nil, err
}
whois, err := a.s.lc.WhoIs(a.r.Context(), a.r.RemoteAddr)
if err != nil {
return nil, err
}
peer, err := toPeerCapabilities(status, whois)
if err != nil {
return nil, err
}
return peer, nil
}
type noBodyData any // empty type, for use from serveAPI for endpoints with empty body
// handle runs the given handler if the source peer satisfies the
// constraints for running this request.
//
// handle is expected for use when `data` type is empty, or set to
// `noBodyData` in practice. For requests that expect JSON body data
// to be attached, use handleJSON instead.
func (a *apiHandler[data]) handle(h http.HandlerFunc) {
peer, err := a.getPeer()
if err != nil {
http.Error(a.w, err.Error(), http.StatusInternalServerError)
return
}
var body data // not used
if !a.permissionCheck(body, peer) {
http.Error(a.w, "not allowed", http.StatusUnauthorized)
return
}
h(a.w, a.r)
}
// handleJSON manages decoding the request's body JSON and passing
// it on to the provided function if the source peer satisfies the
// constraints for running this request.
func (a *apiHandler[data]) handleJSON(h func(ctx context.Context, data data) error) {
defer a.r.Body.Close()
var body data
if err := json.NewDecoder(a.r.Body).Decode(&body); err != nil {
http.Error(a.w, err.Error(), http.StatusInternalServerError)
return
}
peer, err := a.getPeer()
if err != nil {
http.Error(a.w, err.Error(), http.StatusInternalServerError)
return
}
if !a.permissionCheck(body, peer) {
http.Error(a.w, "not allowed", http.StatusUnauthorized)
return
}
if err := h(a.r.Context(), body); err != nil {
http.Error(a.w, err.Error(), http.StatusInternalServerError)
return
}
a.w.WriteHeader(http.StatusOK)
}
// serveAPI serves requests for the web client api.
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
// which protects the handler using gorilla csrf.
func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
if r.Method == httpm.PATCH {
// Enforce that PATCH requests are always application/json.
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
}
w.Header().Set("X-CSRF-Token", csrf.Token(r))
path := strings.TrimPrefix(r.URL.Path, "/api")
switch {
case path == "/data" && r.Method == httpm.GET:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.serveGetNodeData)
return
case path == "/exit-nodes" && r.Method == httpm.GET:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.serveGetExitNodes)
return
case path == "/routes" && r.Method == httpm.POST:
peerAllowed := func(d postRoutesRequest, p peerCapabilities) bool {
if d.SetExitNode && !p.canEdit(capFeatureExitNodes) {
return false
} else if d.SetRoutes && !p.canEdit(capFeatureSubnets) {
return false
}
return true
}
newHandler[postRoutesRequest](s, w, r, peerAllowed).
handleJSON(s.servePostRoutes)
return
case path == "/device-details-click" && r.Method == httpm.POST:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.serveDeviceDetailsClick)
return
case path == "/local/v0/logout" && r.Method == httpm.POST:
peerAllowed := func(_ noBodyData, peer peerCapabilities) bool {
return peer.canEdit(capFeatureAccount)
}
newHandler[noBodyData](s, w, r, peerAllowed).
handle(s.proxyRequestToLocalAPI)
return
case path == "/local/v0/prefs" && r.Method == httpm.PATCH:
peerAllowed := func(data maskedPrefs, peer peerCapabilities) bool {
if data.RunSSHSet && !peer.canEdit(capFeatureSSH) {
return false
}
return true
}
newHandler[maskedPrefs](s, w, r, peerAllowed).
handleJSON(s.serveUpdatePrefs)
return
case path == "/local/v0/update/check" && r.Method == httpm.GET:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.proxyRequestToLocalAPI)
return
case path == "/local/v0/update/check" && r.Method == httpm.POST:
peerAllowed := func(_ noBodyData, peer peerCapabilities) bool {
return peer.canEdit(capFeatureAccount)
}
newHandler[noBodyData](s, w, r, peerAllowed).
handle(s.proxyRequestToLocalAPI)
return
case path == "/local/v0/update/progress" && r.Method == httpm.POST:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.proxyRequestToLocalAPI)
return
case path == "/local/v0/upload-client-metrics" && r.Method == httpm.POST:
newHandler[noBodyData](s, w, r, alwaysAllowed).
handle(s.proxyRequestToLocalAPI)
return
}
http.Error(w, "invalid endpoint", http.StatusNotFound)
}
type authResponse struct {
AuthNeeded authType `json:"authNeeded,omitempty"` // filled when user needs to complete a specific type of auth
CanManageNode bool `json:"canManageNode"`
ViewerIdentity *viewerIdentity `json:"viewerIdentity,omitempty"`
ServerMode ServerMode `json:"serverMode"`
Authorized bool `json:"authorized"` // has an authorized management session
ViewerIdentity *viewerIdentity `json:"viewerIdentity,omitempty"`
NeedsSynoAuth bool `json:"needsSynoAuth,omitempty"`
}
// viewerIdentity is the Tailscale identity of the source node
@@ -475,9 +645,11 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
var resp authResponse
resp.ServerMode = s.mode
session, whois, status, sErr := s.getSession(r)
var caps peerCapabilities
if whois != nil {
caps, err := toPeerCapabilities(status, whois)
var err error
caps, err = toPeerCapabilities(status, whois)
if err != nil {
http.Error(w, sErr.Error(), http.StatusInternalServerError)
return
@@ -504,7 +676,7 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
return
}
if !authorized {
resp.AuthNeeded = synoAuth
resp.NeedsSynoAuth = true
writeJSON(w, resp)
return
}
@@ -520,21 +692,17 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
switch {
case sErr != nil && errors.Is(sErr, errNotUsingTailscale):
// Restricted to the readonly view, no auth action to take.
s.lc.IncrementCounter(r.Context(), "web_client_viewing_local", 1)
resp.AuthNeeded = ""
resp.Authorized = false // restricted to the readonly view
case sErr != nil && errors.Is(sErr, errNotOwner):
// Restricted to the readonly view, no auth action to take.
s.lc.IncrementCounter(r.Context(), "web_client_viewing_not_owner", 1)
resp.AuthNeeded = ""
resp.Authorized = false // restricted to the readonly view
case sErr != nil && errors.Is(sErr, errTaggedLocalSource):
// Restricted to the readonly view, no auth action to take.
s.lc.IncrementCounter(r.Context(), "web_client_viewing_local_tag", 1)
resp.AuthNeeded = ""
resp.Authorized = false // restricted to the readonly view
case sErr != nil && errors.Is(sErr, errTaggedRemoteSource):
// Restricted to the readonly view, no auth action to take.
s.lc.IncrementCounter(r.Context(), "web_client_viewing_remote_tag", 1)
resp.AuthNeeded = ""
resp.Authorized = false // restricted to the readonly view
case sErr != nil && !errors.Is(sErr, errNoSession):
// Any other error.
http.Error(w, sErr.Error(), http.StatusInternalServerError)
@@ -545,16 +713,26 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
} else {
s.lc.IncrementCounter(r.Context(), "web_client_managing_remote", 1)
}
resp.CanManageNode = true
resp.AuthNeeded = ""
// User has a valid session. They're now authorized to edit if they
// have any edit capabilities. In practice, they won't be sent through
// the auth flow if they don't have edit caps, but their ACL granted
// permissions may change at any time. The frontend views and backend
// endpoints are always restricted to their current capabilities in
// addition to a valid session.
//
// But, we also check the caps here for a better user experience on
// the frontend login toggle, which uses resp.Authorized to display
// "viewing" vs "managing" copy. If they don't have caps, we want to
// display "viewing" even if they have a valid session.
resp.Authorized = !caps.isEmpty()
default:
// whois being nil implies local as the request did not come over Tailscale
if whois == nil || (whois.Node.StableID == status.Self.ID) {
// whois being nil implies local as the request did not come over Tailscale.
s.lc.IncrementCounter(r.Context(), "web_client_viewing_local", 1)
} else {
s.lc.IncrementCounter(r.Context(), "web_client_viewing_remote", 1)
}
resp.AuthNeeded = tailscaleAuth
resp.Authorized = false // not yet authorized
}
writeJSON(w, resp)
@@ -618,32 +796,6 @@ func (s *Server) serveAPIAuthSessionWait(w http.ResponseWriter, r *http.Request)
}
}
// serveAPI serves requests for the web client api.
// It should only be called by Server.ServeHTTP, via Server.apiHandler,
// which protects the handler using gorilla csrf.
func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-CSRF-Token", csrf.Token(r))
path := strings.TrimPrefix(r.URL.Path, "/api")
switch {
case path == "/data" && r.Method == httpm.GET:
s.serveGetNodeData(w, r)
return
case path == "/exit-nodes" && r.Method == httpm.GET:
s.serveGetExitNodes(w, r)
return
case path == "/routes" && r.Method == httpm.POST:
s.servePostRoutes(w, r)
return
case path == "/device-details-click" && r.Method == httpm.POST:
s.serveDeviceDetailsClick(w, r)
return
case strings.HasPrefix(path, "/local/"):
s.proxyRequestToLocalAPI(w, r)
return
}
http.Error(w, "invalid endpoint", http.StatusNotFound)
}
type nodeData struct {
ID tailcfg.StableNodeID
Status string
@@ -880,6 +1032,23 @@ func (s *Server) serveGetExitNodes(w http.ResponseWriter, r *http.Request) {
writeJSON(w, exitNodes)
}
// maskedPrefs is the subset of ipn.MaskedPrefs that are
// allowed to be editable via the web UI.
type maskedPrefs struct {
RunSSHSet bool
RunSSH bool
}
func (s *Server) serveUpdatePrefs(ctx context.Context, prefs maskedPrefs) error {
_, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
RunSSHSet: prefs.RunSSHSet,
Prefs: ipn.Prefs{
RunSSH: prefs.RunSSH,
},
})
return err
}
type postRoutesRequest struct {
SetExitNode bool // when set, UseExitNode and AdvertiseExitNode values are applied
SetRoutes bool // when set, AdvertiseRoutes value is applied
@@ -888,18 +1057,10 @@ type postRoutesRequest struct {
AdvertiseRoutes []string
}
func (s *Server) servePostRoutes(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var data postRoutesRequest
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
prefs, err := s.lc.GetPrefs(r.Context())
func (s *Server) servePostRoutes(ctx context.Context, data postRoutesRequest) error {
prefs, err := s.lc.GetPrefs(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
return err
}
var currNonExitRoutes []string
var currAdvertisingExitNode bool
@@ -922,8 +1083,7 @@ func (s *Server) servePostRoutes(w http.ResponseWriter, r *http.Request) {
routesStr := strings.Join(data.AdvertiseRoutes, ",")
routes, err := netutil.CalcAdvertiseRoutes(routesStr, data.AdvertiseExitNode)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
return err
}
hasExitNodeRoute := func(all []netip.Prefix) bool {
@@ -932,8 +1092,7 @@ func (s *Server) servePostRoutes(w http.ResponseWriter, r *http.Request) {
}
if !data.UseExitNode.IsZero() && hasExitNodeRoute(routes) {
http.Error(w, "cannot use and advertise exit node at same time", http.StatusBadRequest)
return
return errors.New("cannot use and advertise exit node at same time")
}
// Make prefs update.
@@ -945,12 +1104,8 @@ func (s *Server) servePostRoutes(w http.ResponseWriter, r *http.Request) {
AdvertiseRoutes: routes,
},
}
if _, err := s.lc.EditPrefs(r.Context(), p); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
_, err = s.lc.EditPrefs(ctx, p)
return err
}
// tailscaleUp starts the daemon with the provided options.
@@ -1089,26 +1244,12 @@ func (s *Server) serveDeviceDetailsClick(w http.ResponseWriter, r *http.Request)
//
// The web API request path is expected to exactly match a localapi path,
// with prefix /api/local/ rather than /localapi/.
//
// If the localapi path is not included in localapiAllowlist,
// the request is rejected.
func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/local")
if r.URL.Path == path { // missing prefix
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
if r.Method == httpm.PATCH {
// enforce that PATCH requests are always application/json
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
}
if !slices.Contains(localapiAllowlist, path) {
http.Error(w, fmt.Sprintf("%s not allowed from localapi proxy", path), http.StatusForbidden)
return
}
localAPIURL := "http://" + apitype.LocalAPIHost + "/localapi" + path
req, err := http.NewRequestWithContext(r.Context(), r.Method, localAPIURL, r.Body)
@@ -1133,21 +1274,6 @@ func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request)
}
}
// localapiAllowlist is an allowlist of localapi endpoints the
// web client is allowed to proxy to the client's localapi.
//
// Rather than exposing all localapi endpoints over the proxy,
// this limits to just the ones actually used from the web
// client frontend.
var localapiAllowlist = []string{
"/v0/logout",
"/v0/prefs",
"/v0/update/check",
"/v0/update/install",
"/v0/update/progress",
"/v0/upload-client-metrics",
}
// csrfKey returns a key that can be used for CSRF protection.
// If an error occurs during key creation, the error is logged and the active process terminated.
// If the server is running in CGI mode, the key is cached to disk and reused between requests.

View File

@@ -4,6 +4,7 @@
package web
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -86,75 +87,172 @@ func TestQnapAuthnURL(t *testing.T) {
// TestServeAPI tests the web client api's handling of
// 1. invalid endpoint errors
// 2. localapi proxy allowlist
// 2. permissioning of api endpoints based on node capabilities
func TestServeAPI(t *testing.T) {
selfTags := views.SliceOf([]string{"tag:server"})
self := &ipnstate.PeerStatus{ID: "self", Tags: &selfTags}
prefs := &ipn.Prefs{}
remoteUser := &tailcfg.UserProfile{ID: tailcfg.UserID(1)}
remoteIPWithAllCapabilities := "100.100.100.101"
remoteIPWithNoCapabilities := "100.100.100.102"
lal := memnet.Listen("local-tailscaled.sock:80")
defer lal.Close()
// Serve dummy localapi. Just returns "success".
localapi := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "success")
})}
localapi := mockLocalAPI(t,
map[string]*apitype.WhoIsResponse{
remoteIPWithAllCapabilities: {
Node: &tailcfg.Node{StableID: "node1"},
UserProfile: remoteUser,
CapMap: tailcfg.PeerCapMap{tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{"{\"canEdit\":[\"*\"]}"}},
},
remoteIPWithNoCapabilities: {
Node: &tailcfg.Node{StableID: "node2"},
UserProfile: remoteUser,
},
},
func() *ipnstate.PeerStatus { return self },
func() *ipn.Prefs { return prefs },
nil,
)
defer localapi.Close()
go localapi.Serve(lal)
s := &Server{lc: &tailscale.LocalClient{Dial: lal.Dial}}
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
timeNow: time.Now,
}
type requestTest struct {
remoteIP string
wantResponse string
wantStatus int
}
tests := []struct {
name string
reqMethod string
reqPath string
reqMethod string
reqContentType string
wantResp string
wantStatus int
reqBody string
tests []requestTest
}{{
name: "invalid_endpoint",
reqMethod: httpm.POST,
reqPath: "/not-an-endpoint",
wantResp: "invalid endpoint",
wantStatus: http.StatusNotFound,
reqPath: "/not-an-endpoint",
reqMethod: httpm.POST,
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "invalid endpoint",
wantStatus: http.StatusNotFound,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantResponse: "invalid endpoint",
wantStatus: http.StatusNotFound,
}},
}, {
name: "not_in_localapi_allowlist",
reqMethod: httpm.POST,
reqPath: "/local/v0/not-allowlisted",
wantResp: "/v0/not-allowlisted not allowed from localapi proxy",
wantStatus: http.StatusForbidden,
reqPath: "/local/v0/not-an-endpoint",
reqMethod: httpm.POST,
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "invalid endpoint",
wantStatus: http.StatusNotFound,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantResponse: "invalid endpoint",
wantStatus: http.StatusNotFound,
}},
}, {
name: "in_localapi_allowlist",
reqMethod: httpm.POST,
reqPath: "/local/v0/logout",
wantResp: "success", // Successfully allowed to hit localapi.
wantStatus: http.StatusOK,
reqPath: "/local/v0/logout",
reqMethod: httpm.POST,
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "not allowed", // requesting node has insufficient permissions
wantStatus: http.StatusUnauthorized,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantResponse: "success", // requesting node has sufficient permissions
wantStatus: http.StatusOK,
}},
}, {
reqPath: "/exit-nodes",
reqMethod: httpm.GET,
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "null",
wantStatus: http.StatusOK, // allowed, no additional capabilities required
}, {
remoteIP: remoteIPWithAllCapabilities,
wantResponse: "null",
wantStatus: http.StatusOK,
}},
}, {
reqPath: "/routes",
reqMethod: httpm.POST,
reqBody: "{\"setExitNode\":true}",
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "not allowed",
wantStatus: http.StatusUnauthorized,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantStatus: http.StatusOK,
}},
}, {
name: "patch_bad_contenttype",
reqMethod: httpm.PATCH,
reqPath: "/local/v0/prefs",
reqMethod: httpm.PATCH,
reqBody: "{\"runSSHSet\":true}",
reqContentType: "application/json",
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "not allowed",
wantStatus: http.StatusUnauthorized,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantStatus: http.StatusOK,
}},
}, {
reqPath: "/local/v0/prefs",
reqMethod: httpm.PATCH,
reqContentType: "multipart/form-data",
wantResp: "invalid request",
wantStatus: http.StatusBadRequest,
tests: []requestTest{{
remoteIP: remoteIPWithNoCapabilities,
wantResponse: "invalid request",
wantStatus: http.StatusBadRequest,
}, {
remoteIP: remoteIPWithAllCapabilities,
wantResponse: "invalid request",
wantStatus: http.StatusBadRequest,
}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := httptest.NewRequest(tt.reqMethod, "/api"+tt.reqPath, nil)
if tt.reqContentType != "" {
r.Header.Add("Content-Type", tt.reqContentType)
}
w := httptest.NewRecorder()
for _, req := range tt.tests {
t.Run(req.remoteIP+"_requesting_"+tt.reqPath, func(t *testing.T) {
var reqBody io.Reader
if tt.reqBody != "" {
reqBody = bytes.NewBuffer([]byte(tt.reqBody))
}
r := httptest.NewRequest(tt.reqMethod, "/api"+tt.reqPath, reqBody)
r.RemoteAddr = req.remoteIP
if tt.reqContentType != "" {
r.Header.Add("Content-Type", tt.reqContentType)
}
w := httptest.NewRecorder()
s.serveAPI(w, r)
res := w.Result()
defer res.Body.Close()
if gotStatus := res.StatusCode; tt.wantStatus != gotStatus {
t.Errorf("wrong status; want=%v, got=%v", tt.wantStatus, gotStatus)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
gotResp := strings.TrimSuffix(string(body), "\n") // trim trailing newline
if tt.wantResp != gotResp {
t.Errorf("wrong response; want=%q, got=%q", tt.wantResp, gotResp)
}
})
s.serveAPI(w, r)
res := w.Result()
defer res.Body.Close()
if gotStatus := res.StatusCode; req.wantStatus != gotStatus {
t.Errorf("wrong status; want=%v, got=%v", req.wantStatus, gotStatus)
}
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
gotResp := strings.TrimSuffix(string(body), "\n") // trim trailing newline
if req.wantResponse != gotResp {
t.Errorf("wrong response; want=%q, got=%q", req.wantResponse, gotResp)
}
})
}
}
}
@@ -524,7 +622,7 @@ func TestServeAuth(t *testing.T) {
name: "no-session",
path: "/api/auth",
wantStatus: http.StatusOK,
wantResp: &authResponse{AuthNeeded: tailscaleAuth, ViewerIdentity: vi, ServerMode: ManageServerMode},
wantResp: &authResponse{ViewerIdentity: vi, ServerMode: ManageServerMode},
wantNewCookie: false,
wantSession: nil,
},
@@ -549,7 +647,7 @@ func TestServeAuth(t *testing.T) {
path: "/api/auth",
cookie: successCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{AuthNeeded: tailscaleAuth, ViewerIdentity: vi, ServerMode: ManageServerMode},
wantResp: &authResponse{ViewerIdentity: vi, ServerMode: ManageServerMode},
wantSession: &browserSession{
ID: successCookie,
SrcNode: remoteNode.Node.ID,
@@ -597,7 +695,7 @@ func TestServeAuth(t *testing.T) {
path: "/api/auth",
cookie: successCookie,
wantStatus: http.StatusOK,
wantResp: &authResponse{CanManageNode: true, ViewerIdentity: vi, ServerMode: ManageServerMode},
wantResp: &authResponse{Authorized: true, ViewerIdentity: vi, ServerMode: ManageServerMode},
wantSession: &browserSession{
ID: successCookie,
SrcNode: remoteNode.Node.ID,
@@ -1121,9 +1219,10 @@ func TestPeerCapabilities(t *testing.T) {
status: userOwnedStatus,
whois: &apitype.WhoIsResponse{
UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(2)},
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"ssh\",\"subnets\"]}",
},
},
},
@@ -1134,9 +1233,10 @@ func TestPeerCapabilities(t *testing.T) {
status: userOwnedStatus,
whois: &apitype.WhoIsResponse{
UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(1)},
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"ssh\",\"subnets\"]}",
},
},
},
@@ -1146,6 +1246,7 @@ func TestPeerCapabilities(t *testing.T) {
name: "tag-owned-no-webui-caps",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{},
},
@@ -1156,68 +1257,71 @@ func TestPeerCapabilities(t *testing.T) {
name: "tag-owned-one-webui-cap",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"ssh\",\"subnets\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureSSH: true,
capFeatureSubnets: true,
},
},
{
name: "tag-owned-multiple-webui-cap",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"subnet\",\"exitnode\",\"*\"]}",
"{\"canEdit\":[\"ssh\",\"subnets\"]}",
"{\"canEdit\":[\"subnets\",\"exitnodes\",\"*\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAll: true,
capFeatureSSH: true,
capFeatureSubnets: true,
capFeatureExitNodes: true,
capFeatureAll: true,
},
},
{
name: "tag-owned-case-insensitive-caps",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"SSH\",\"sUBnet\"]}",
"{\"canEdit\":[\"SSH\",\"sUBnets\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureSSH: true,
capFeatureSubnets: true,
},
},
{
name: "tag-owned-random-canEdit-contents-dont-error",
name: "tag-owned-random-canEdit-contents-get-dropped",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"unknown-feature\"]}",
},
},
},
wantCaps: peerCapabilities{
"unknown-feature": true,
},
wantCaps: peerCapabilities{},
},
{
name: "tag-owned-no-canEdit-section",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1)},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canDoSomething\":[\"*\"]}",
@@ -1226,6 +1330,19 @@ func TestPeerCapabilities(t *testing.T) {
},
wantCaps: peerCapabilities{},
},
{
name: "tagged-source-caps-ignored",
status: tagOwnedStatus,
whois: &apitype.WhoIsResponse{
Node: &tailcfg.Node{ID: tailcfg.NodeID(1), Tags: tags.AsSlice()},
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnets\"]}",
},
},
},
wantCaps: peerCapabilities{},
},
}
for _, tt := range toPeerCapsTests {
t.Run("toPeerCapabilities-"+tt.name, func(t *testing.T) {
@@ -1249,36 +1366,33 @@ func TestPeerCapabilities(t *testing.T) {
name: "empty-caps",
caps: nil,
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: false,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: false,
capFeatureAll: false,
capFeatureSSH: false,
capFeatureSubnets: false,
capFeatureExitNodes: false,
capFeatureAccount: false,
},
},
{
name: "some-caps",
caps: peerCapabilities{capFeatureSSH: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: true,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: true,
capFeatureAll: false,
capFeatureSSH: true,
capFeatureSubnets: false,
capFeatureExitNodes: false,
capFeatureAccount: true,
},
},
{
name: "wildcard-in-caps",
caps: peerCapabilities{capFeatureAll: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: true,
capFeatureFunnel: true,
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAccount: true,
capFeatureAll: true,
capFeatureSSH: true,
capFeatureSubnets: true,
capFeatureExitNodes: true,
capFeatureAccount: true,
},
},
}
@@ -1339,6 +1453,9 @@ func mockLocalAPI(t *testing.T, whoIs map[string]*apitype.WhoIsResponse, self fu
metricCapture(metricNames[0].Name)
writeJSON(w, struct{}{})
return
case "/localapi/v0/logout":
fmt.Fprintf(w, "success")
return
default:
t.Fatalf("unhandled localapi test endpoint %q, add to localapi handler func in test", r.URL.Path)
}

View File

@@ -20,23 +20,7 @@
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.10.tgz#1c20e612b768fefa75f6e90d6ecb86329247f0a3"
integrity sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==
dependencies:
"@babel/highlight" "^7.22.10"
chalk "^2.4.2"
"@babel/code-frame@^7.22.13":
version "7.22.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
dependencies:
"@babel/highlight" "^7.22.13"
chalk "^2.4.2"
"@babel/code-frame@^7.23.4":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.10", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.4.tgz#03ae5af150be94392cb5c7ccd97db5a19a5da6aa"
integrity sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==
@@ -44,17 +28,12 @@
"@babel/highlight" "^7.23.4"
chalk "^2.4.2"
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3":
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.3.tgz#3febd552541e62b5e883a25eb3effd7c7379db11"
integrity sha512-BmR4bWbDIoFJmJ9z2cZ8Gmm2MXgEDgjdWgpKmKWUt54UGFJdlj31ECtbaDvCG/qVdG3AQ1SfpZEs01lUFbzLOQ==
"@babel/compat-data@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730"
integrity sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==
"@babel/core@^7.16.0":
"@babel/core@^7.16.0", "@babel/core@^7.21.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.3.tgz#5ec09c8803b91f51cc887dedc2654a35852849c9"
integrity sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==
@@ -75,27 +54,6 @@
json5 "^2.2.3"
semver "^6.3.1"
"@babel/core@^7.21.3":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.10.tgz#aad442c7bcd1582252cb4576747ace35bc122f35"
integrity sha512-fTmqbbUBAwCcre6zPzNngvsI0aNrPZe77AeqvDxWM9Nm+04RrJ3CAmGHA9f7lJQY6ZMhRztNemy4uslDxTX4Qw==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.22.10"
"@babel/generator" "^7.22.10"
"@babel/helper-compilation-targets" "^7.22.10"
"@babel/helper-module-transforms" "^7.22.9"
"@babel/helpers" "^7.22.10"
"@babel/parser" "^7.22.10"
"@babel/template" "^7.22.5"
"@babel/traverse" "^7.22.10"
"@babel/types" "^7.22.10"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.2"
semver "^6.3.1"
"@babel/eslint-parser@^7.16.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.23.3.tgz#7bf0db1c53b54da0c8a12627373554a0828479ca"
@@ -105,27 +63,7 @@
eslint-visitor-keys "^2.1.0"
semver "^6.3.1"
"@babel/generator@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.10.tgz#c92254361f398e160645ac58831069707382b722"
integrity sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==
dependencies:
"@babel/types" "^7.22.10"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
dependencies:
"@babel/types" "^7.23.0"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/generator@^7.23.3", "@babel/generator@^7.23.4":
"@babel/generator@^7.22.10", "@babel/generator@^7.23.0", "@babel/generator@^7.23.3", "@babel/generator@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.4.tgz#4a41377d8566ec18f807f42962a7f3551de83d1c"
integrity sha512-esuS49Cga3HcThFNebGhlgsrVLkvhqvYDTzgjfFFlHJcIfLe5jFmRRfCQ1KuBfc4Jrtn3ndLgKWAKjBE+IraYQ==
@@ -149,18 +87,7 @@
dependencies:
"@babel/types" "^7.22.15"
"@babel/helper-compilation-targets@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz#01d648bbc25dd88f513d862ee0df27b7d4e67024"
integrity sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==
dependencies:
"@babel/compat-data" "^7.22.9"
"@babel/helper-validator-option" "^7.22.5"
browserslist "^4.21.9"
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6":
"@babel/helper-compilation-targets@^7.22.10", "@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.6":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52"
integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==
@@ -206,16 +133,11 @@
lodash.debounce "^4.0.8"
resolve "^1.14.2"
"@babel/helper-environment-visitor@^7.22.20":
"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
"@babel/helper-environment-visitor@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98"
integrity sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==
"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
@@ -238,32 +160,14 @@
dependencies:
"@babel/types" "^7.23.0"
"@babel/helper-module-imports@^7.22.15":
"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0"
integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==
dependencies:
"@babel/types" "^7.22.15"
"@babel/helper-module-imports@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz#1a8f4c9f4027d23f520bd76b364d44434a72660c"
integrity sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-module-transforms@^7.22.9":
version "7.22.9"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz#92dfcb1fbbb2bc62529024f72d942a8c97142129"
integrity sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==
dependencies:
"@babel/helper-environment-visitor" "^7.22.5"
"@babel/helper-module-imports" "^7.22.5"
"@babel/helper-simple-access" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/helper-validator-identifier" "^7.22.5"
"@babel/helper-module-transforms@^7.23.3":
"@babel/helper-module-transforms@^7.22.9", "@babel/helper-module-transforms@^7.23.3":
version "7.23.3"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1"
integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==
@@ -325,36 +229,21 @@
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-string-parser@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-string-parser@^7.23.4":
"@babel/helper-string-parser@^7.22.5", "@babel/helper-string-parser@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83"
integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==
"@babel/helper-validator-identifier@^7.22.20":
"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.22.5":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
"@babel/helper-validator-identifier@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193"
integrity sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==
"@babel/helper-validator-option@^7.22.15":
"@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.22.5":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040"
integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==
"@babel/helper-validator-option@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz#de52000a15a177413c8234fa3a8af4ee8102d0ac"
integrity sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==
"@babel/helper-wrap-function@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569"
@@ -364,16 +253,7 @@
"@babel/template" "^7.22.15"
"@babel/types" "^7.22.19"
"@babel/helpers@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.10.tgz#ae6005c539dfbcb5cd71fb51bfc8a52ba63bc37a"
integrity sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==
dependencies:
"@babel/template" "^7.22.5"
"@babel/traverse" "^7.22.10"
"@babel/types" "^7.22.10"
"@babel/helpers@^7.23.2":
"@babel/helpers@^7.22.10", "@babel/helpers@^7.23.2":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.4.tgz#7d2cfb969aa43222032193accd7329851facf3c1"
integrity sha512-HfcMizYz10cr3h29VqyfGL6ZWIjTwWfvYBMsBVGwpcbhNGe3wQ1ZXZRPzZoAHhd9OqHadHqjQ89iVKINXnbzuw==
@@ -382,25 +262,7 @@
"@babel/traverse" "^7.23.4"
"@babel/types" "^7.23.4"
"@babel/highlight@^7.22.10":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.10.tgz#02a3f6d8c1cb4521b2fd0ab0da8f4739936137d7"
integrity sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==
dependencies:
"@babel/helper-validator-identifier" "^7.22.5"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/highlight@^7.22.13":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/highlight@^7.23.4":
"@babel/highlight@^7.22.10", "@babel/highlight@^7.22.13", "@babel/highlight@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
@@ -409,17 +271,7 @@
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/parser@^7.22.10", "@babel/parser@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.10.tgz#e37634f9a12a1716136c44624ef54283cabd3f55"
integrity sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
"@babel/parser@^7.23.3", "@babel/parser@^7.23.4":
"@babel/parser@^7.22.10", "@babel/parser@^7.22.15", "@babel/parser@^7.22.5", "@babel/parser@^7.23.0", "@babel/parser@^7.23.3", "@babel/parser@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.4.tgz#409fbe690c333bb70187e2de4021e1e47a026661"
integrity sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==
@@ -1234,21 +1086,14 @@
resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4":
"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.23.2", "@babel/runtime@^7.8.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e"
integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.13.10":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.22.15":
"@babel/template@^7.22.15", "@babel/template@^7.22.5":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
@@ -1257,32 +1102,7 @@
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/template@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
integrity sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==
dependencies:
"@babel/code-frame" "^7.22.5"
"@babel/parser" "^7.22.5"
"@babel/types" "^7.22.5"
"@babel/traverse@^7.22.10":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/generator" "^7.23.0"
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-function-name" "^7.23.0"
"@babel/helper-hoist-variables" "^7.22.5"
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/parser" "^7.23.0"
"@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
"@babel/traverse@^7.23.3", "@babel/traverse@^7.23.4":
"@babel/traverse@^7.22.10", "@babel/traverse@^7.23.3", "@babel/traverse@^7.23.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.4.tgz#c2790f7edf106d059a0098770fe70801417f3f85"
integrity sha512-IYM8wSUwunWTB6tFC2dkKZhxbIjHoWemdK+3f8/wq8aKhbUscxD5MX72ubd90fxvFknaLPeGw5ycU84V1obHJg==
@@ -1298,25 +1118,7 @@
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.21.3", "@babel/types@^7.22.10", "@babel/types@^7.22.5":
version "7.22.10"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.10.tgz#4a9e76446048f2c66982d1a989dd12b8a2d2dc03"
integrity sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.5"
to-fast-properties "^2.0.0"
"@babel/types@^7.22.15", "@babel/types@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@babel/types@^7.22.19", "@babel/types@^7.23.3", "@babel/types@^7.23.4", "@babel/types@^7.4.4":
"@babel/types@^7.21.3", "@babel/types@^7.22.10", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.3", "@babel/types@^7.23.4", "@babel/types@^7.4.4":
version "7.23.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.4.tgz#7206a1810fc512a7f7f7d4dace4cb4c1c9dbfb8e"
integrity sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==
@@ -1445,14 +1247,14 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae"
integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==
"@eslint-community/eslint-utils@^4.2.0":
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1":
"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
version "4.10.0"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63"
integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==
@@ -2063,17 +1865,12 @@
resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a"
integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==
"@types/estree@1.0.5":
"@types/estree@1.0.5", "@types/estree@^1.0.0":
version "1.0.5"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
"@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/json-schema@^7.0.9":
"@types/json-schema@^7.0.12", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -2121,26 +1918,27 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@types/semver@^7.3.12":
version "7.5.6"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
"@types/semver@^7.3.12", "@types/semver@^7.5.0":
version "7.5.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==
"@typescript-eslint/eslint-plugin@^5.5.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db"
integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==
"@typescript-eslint/eslint-plugin@^5.5.0", "@typescript-eslint/eslint-plugin@^6.2.1":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/type-utils" "5.62.0"
"@typescript-eslint/utils" "5.62.0"
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.21.0"
"@typescript-eslint/type-utils" "6.21.0"
"@typescript-eslint/utils" "6.21.0"
"@typescript-eslint/visitor-keys" "6.21.0"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.0"
natural-compare-lite "^1.4.0"
semver "^7.3.7"
tsutils "^3.21.0"
ignore "^5.2.4"
natural-compare "^1.4.0"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/experimental-utils@^5.0.0":
version "5.62.0"
@@ -2149,14 +1947,15 @@
dependencies:
"@typescript-eslint/utils" "5.62.0"
"@typescript-eslint/parser@^5.5.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7"
integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==
"@typescript-eslint/parser@^5.5.0", "@typescript-eslint/parser@^6.2.1":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b"
integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==
dependencies:
"@typescript-eslint/scope-manager" "5.62.0"
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/typescript-estree" "5.62.0"
"@typescript-eslint/scope-manager" "6.21.0"
"@typescript-eslint/types" "6.21.0"
"@typescript-eslint/typescript-estree" "6.21.0"
"@typescript-eslint/visitor-keys" "6.21.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.62.0":
@@ -2167,21 +1966,34 @@
"@typescript-eslint/types" "5.62.0"
"@typescript-eslint/visitor-keys" "5.62.0"
"@typescript-eslint/type-utils@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==
"@typescript-eslint/scope-manager@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1"
integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==
dependencies:
"@typescript-eslint/typescript-estree" "5.62.0"
"@typescript-eslint/utils" "5.62.0"
"@typescript-eslint/types" "6.21.0"
"@typescript-eslint/visitor-keys" "6.21.0"
"@typescript-eslint/type-utils@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e"
integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==
dependencies:
"@typescript-eslint/typescript-estree" "6.21.0"
"@typescript-eslint/utils" "6.21.0"
debug "^4.3.4"
tsutils "^3.21.0"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f"
integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==
"@typescript-eslint/types@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d"
integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@@ -2195,6 +2007,20 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46"
integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==
dependencies:
"@typescript-eslint/types" "6.21.0"
"@typescript-eslint/visitor-keys" "6.21.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "9.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.58.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
@@ -2209,6 +2035,19 @@
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/utils@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134"
integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.21.0"
"@typescript-eslint/types" "6.21.0"
"@typescript-eslint/typescript-estree" "6.21.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e"
@@ -2217,6 +2056,14 @@
"@typescript-eslint/types" "5.62.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@6.21.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47"
integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==
dependencies:
"@typescript-eslint/types" "6.21.0"
eslint-visitor-keys "^3.4.1"
"@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@@ -2283,16 +2130,11 @@ acorn-walk@^8.3.2:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa"
integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==
acorn@^8.11.3:
acorn@^8.11.3, acorn@^8.9.0:
version "8.11.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a"
integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==
acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==
agent-base@^7.0.2, agent-base@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434"
@@ -2579,6 +2421,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
@@ -2586,17 +2435,7 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
browserslist@^4.21.10, browserslist@^4.21.9:
version "4.21.10"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0"
integrity sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==
dependencies:
caniuse-lite "^1.0.30001517"
electron-to-chromium "^1.4.477"
node-releases "^2.0.13"
update-browserslist-db "^1.0.11"
browserslist@^4.22.1:
browserslist@^4.21.10, browserslist@^4.21.9, browserslist@^4.22.1:
version "4.22.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619"
integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==
@@ -2635,17 +2474,7 @@ camelcase@^6.2.0:
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
caniuse-lite@^1.0.30001517:
version "1.0.30001519"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601"
integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==
caniuse-lite@^1.0.30001520:
version "1.0.30001520"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001520.tgz#62e2b7a1c7b35269594cf296a80bdf8cb9565006"
integrity sha512-tahF5O9EiiTzwTUqAeFjIZbn4Dnqxzz7ktrgGlMYNLH43Ul26IgTMH/zvL3DG0lZxBYnlT04axvInszUsZULdA==
caniuse-lite@^1.0.30001541:
caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001520, caniuse-lite@^1.0.30001541:
version "1.0.30001565"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz#a528b253c8a2d95d2b415e11d8b9942acc100c4f"
integrity sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==
@@ -2943,12 +2772,7 @@ dot-case@^3.0.4:
no-case "^3.0.4"
tslib "^2.0.3"
electron-to-chromium@^1.4.477:
version "1.4.490"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz#d99286f6e915667fa18ea4554def1aa60eb4d5f1"
integrity sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==
electron-to-chromium@^1.4.535:
electron-to-chromium@^1.4.477, electron-to-chromium@^1.4.535:
version "1.4.596"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.596.tgz#6752d1aa795d942d49dfc5d3764d6ea283fab1d7"
integrity sha512-zW3zbZ40Icb2BCWjm47nxwcFGYlIgdXkAx85XDO7cyky9J4QQfq8t0W19/TLZqq3JPQXtlv8BPIGmfa9Jb4scg==
@@ -3379,18 +3203,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.12:
version "3.3.1"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4"
integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-glob@^3.2.9:
fast-glob@^3.2.12, fast-glob@^3.2.9:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
@@ -3480,22 +3293,12 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fsevents@~2.3.3:
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
function-bind@^1.1.2:
function-bind@^1.1.1, function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
@@ -3732,10 +3535,10 @@ iconv-lite@0.6.3:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
ignore@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
ignore@^5.2.0, ignore@^5.2.4:
version "5.3.1"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef"
integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==
import-fresh@^3.2.1:
version "3.3.0"
@@ -3827,14 +3630,7 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-core-module@^2.13.0:
version "2.13.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db"
integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==
dependencies:
has "^1.0.3"
is-core-module@^2.13.1:
is-core-module@^2.13.0, is-core-module@^2.13.1:
version "2.13.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
@@ -4173,14 +3969,7 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
loupe@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53"
integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==
dependencies:
get-func-name "^2.0.0"
loupe@^2.3.7:
loupe@^2.3.6, loupe@^2.3.7:
version "2.3.7"
resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697"
integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==
@@ -4250,6 +4039,13 @@ mimic-fn@^4.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
minimatch@9.0.3:
version "9.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825"
integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
@@ -4262,17 +4058,7 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
mlly@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b"
integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==
dependencies:
acorn "^8.9.0"
pathe "^1.1.1"
pkg-types "^1.0.3"
ufo "^1.1.2"
mlly@^1.4.2:
mlly@^1.2.0, mlly@^1.4.2:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.6.0.tgz#0ecfbddc706857f5e170ccd28c6b0b9c81d3f548"
integrity sha512-YOvg9hfYQmnaB56Yb+KrJE2u0Yzz5zR+sLejEvF4fzwzV1Al6hkf2vyHTwqCRyv0hCi9rVCqVoXpyYevQIRwLQ==
@@ -4301,21 +4087,11 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nanoid@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^3.3.7:
nanoid@^3.3.6, nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -4532,12 +4308,7 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pathe@^1.1.0, pathe@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a"
integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==
pathe@^1.1.2:
pathe@^1.1.0, pathe@^1.1.1, pathe@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec"
integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==
@@ -4620,16 +4391,7 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.23, postcss@^8.4.31:
version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies:
nanoid "^3.3.6"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.4.35:
postcss@^8.4.23, postcss@^8.4.31, postcss@^8.4.35:
version "8.4.35"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7"
integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==
@@ -4843,16 +4605,7 @@ resolve-from@^4.0.0:
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
resolve@^1.1.7, resolve@^1.22.2:
version "1.22.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34"
integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.4:
resolve@^1.1.7, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.22.2, resolve@^1.22.4:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
@@ -4959,10 +4712,10 @@ semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.3.7:
version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==
semver@^7.3.7, semver@^7.5.4:
version "7.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
@@ -5261,6 +5014,11 @@ tr46@^5.0.0:
dependencies:
punycode "^2.3.1"
ts-api-utils@^1.0.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b"
integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
@@ -5358,17 +5116,12 @@ typed-array-length@^1.0.4:
for-each "^0.3.3"
is-typed-array "^1.1.9"
typescript@^4.7.4:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
ufo@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.2.0.tgz#28d127a087a46729133fdc89cb1358508b3f80ba"
integrity sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==
ufo@^1.3.2:
ufo@^1.1.2, ufo@^1.3.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.4.0.tgz#39845b31be81b4f319ab1d99fd20c56cac528d32"
integrity sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==
@@ -5416,15 +5169,7 @@ universalify@^0.2.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
update-browserslist-db@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940"
integrity sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
update-browserslist-db@^1.0.13:
update-browserslist-db@^1.0.11, update-browserslist-db@^1.0.13:
version "1.0.13"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4"
integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==

View File

@@ -436,7 +436,7 @@ func (up *Updater) updateDebLike() error {
return fmt.Errorf("apt-get update failed: %w; output:\n%s", err, out)
}
for i := 0; i < 2; i++ {
for range 2 {
out, err := exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver).CombinedOutput()
if err != nil {
if !bytes.Contains(out, []byte(`dpkg was interrupted`)) {
@@ -665,6 +665,7 @@ func (up *Updater) updateAlpineLike() (err error) {
func parseAlpinePackageVersion(out []byte) (string, error) {
s := bufio.NewScanner(bytes.NewReader(out))
var maxVer string
for s.Scan() {
// The line should look like this:
// tailscale-1.44.2-r0 description:
@@ -676,7 +677,13 @@ func parseAlpinePackageVersion(out []byte) (string, error) {
if len(parts) < 3 {
return "", fmt.Errorf("malformed info line: %q", line)
}
return parts[1], nil
ver := parts[1]
if cmpver.Compare(ver, maxVer) == 1 {
maxVer = ver
}
}
if maxVer != "" {
return maxVer, nil
}
return "", errors.New("tailscale version not found in output")
}
@@ -829,7 +836,7 @@ func (up *Updater) switchOutputToFile() (io.Closer, error) {
func (up *Updater) installMSI(msi string) error {
var err error
for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/norestart", "/qn")
cmd.Dir = filepath.Dir(msi)
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr

View File

@@ -251,6 +251,29 @@ tailscale installed size:
out: "",
wantErr: true,
},
{
desc: "multiple versions",
out: `
tailscale-1.54.1-r0 description:
The easiest, most secure way to use WireGuard and 2FA
tailscale-1.54.1-r0 webpage:
https://tailscale.com/
tailscale-1.54.1-r0 installed size:
34 MiB
tailscale-1.58.2-r0 description:
The easiest, most secure way to use WireGuard and 2FA
tailscale-1.58.2-r0 webpage:
https://tailscale.com/
tailscale-1.58.2-r0 installed size:
35 MiB
`,
want: "1.58.2",
},
}
for _, tt := range tests {
@@ -640,7 +663,7 @@ func genTarball(t *testing.T, path string, files map[string]string) {
func TestWriteFileOverwrite(t *testing.T) {
path := filepath.Join(t.TempDir(), "test")
for i := 0; i < 2; i++ {
for i := range 2 {
content := fmt.Sprintf("content %d", i)
if err := writeFile(strings.NewReader(content), path, 0600); err != nil {
t.Fatal(err)

View File

@@ -445,7 +445,7 @@ type testServer struct {
func newTestServer(t *testing.T) *testServer {
var roots []rootKeyPair
for i := 0; i < 3; i++ {
for range 3 {
roots = append(roots, newRootKeyPair(t))
}

View File

@@ -19,11 +19,11 @@ func restartSystemdUnit(ctx context.Context) error {
}
defer c.Close()
if err := c.ReloadContext(ctx); err != nil {
return fmt.Errorf("failed to reload tailsacled.service: %w", err)
return fmt.Errorf("failed to reload tailscaled.service: %w", err)
}
ch := make(chan string, 1)
if _, err := c.RestartUnitContext(ctx, "tailscaled.service", "replace", ch); err != nil {
return fmt.Errorf("failed to restart tailsacled.service: %w", err)
return fmt.Errorf("failed to restart tailscaled.service: %w", err)
}
select {
case res := <-ch:

View File

@@ -102,7 +102,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
writef("}")
writef("dst := new(%s)", name)
writef("*dst = *src")
for i := 0; i < t.NumFields(); i++ {
for i := range t.NumFields() {
fname := t.Field(i).Name()
ft := t.Field(i).Type()
if !codegen.ContainsPointers(ft) || codegen.HasNoClone(t.Tag(i)) {

View File

@@ -17,7 +17,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/tsweb
github.com/google/uuid from tailscale.com/util/fastuuid
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
@@ -86,6 +86,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/derp from tailscale.com/cmd/derper+
tailscale.com/derp/derphttp from tailscale.com/cmd/derper
tailscale.com/disco from tailscale.com/derp
tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/health from tailscale.com/net/tlsdial
tailscale.com/hostinfo from tailscale.com/net/interfaces+
@@ -114,7 +115,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailfs from tailscale.com/client/tailscale
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/derp+
@@ -143,6 +143,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/fastuuid from tailscale.com/tsweb
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
@@ -250,6 +251,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
math/rand/v2 from tailscale.com/util/fastuuid
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart

View File

@@ -26,7 +26,6 @@ import (
"syscall"
"time"
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/atomicfile"
"tailscale.com/derp"
@@ -36,19 +35,22 @@ import (
"tailscale.com/net/stunserver"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
versionFlag = flag.Bool("version", false, "print version and exit")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
@@ -129,6 +131,10 @@ func writeNewConfig() config {
func main() {
flag.Parse()
if *versionFlag {
fmt.Println(version.Long())
return
}
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
@@ -235,7 +241,7 @@ func main() {
KeepAlive: *tcpKeepAlive,
}
quietLogger := log.New(logFilter{}, "", 0)
quietLogger := log.New(logger.HTTPServerLogFilter{Inner: log.Printf}, "", 0)
httpsrv := &http.Server{
Addr: *addr,
Handler: mux,
@@ -452,22 +458,3 @@ func (l *rateLimitedListener) Accept() (net.Conn, error) {
l.numAccepts.Add(1)
return cn, nil
}
// logFilter is used to filter out useless error logs that are logged to
// the net/http.Server.ErrorLog logger.
type logFilter struct{}
func (logFilter) Write(p []byte) (int, error) {
b := mem.B(p)
if mem.HasSuffix(b, mem.S(": EOF\n")) ||
mem.HasSuffix(b, mem.S(": i/o timeout\n")) ||
mem.HasSuffix(b, mem.S(": read: connection reset by peer\n")) ||
mem.HasSuffix(b, mem.S(": remote error: tls: bad certificate\n")) ||
mem.HasSuffix(b, mem.S(": tls: first record does not look like a TLS handshake\n")) {
// Skip this log message, but say that we processed it
return len(p), nil
}
log.Printf("%s", p)
return len(p), nil
}

View File

@@ -16,21 +16,40 @@ import (
"tailscale.com/prober"
"tailscale.com/tsweb"
"tailscale.com/version"
)
var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
spread = flag.Bool("spread", true, "whether to spread probing over time")
interval = flag.Duration("interval", 15*time.Second, "probe interval")
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
versionFlag = flag.Bool("version", false, "print version and exit")
listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
spread = flag.Bool("spread", true, "whether to spread probing over time")
interval = flag.Duration("interval", 15*time.Second, "probe interval")
meshInterval = flag.Duration("mesh-interval", 15*time.Second, "mesh probe interval")
stunInterval = flag.Duration("stun-interval", 15*time.Second, "STUN probe interval")
tlsInterval = flag.Duration("tls-interval", 15*time.Second, "TLS probe interval")
bwInterval = flag.Duration("bw-interval", 0, "bandwidth probe interval (0 = no bandwidth probing)")
bwSize = flag.Int64("bw-probe-size-bytes", 1_000_000, "bandwidth probe size")
)
func main() {
flag.Parse()
if *versionFlag {
fmt.Println(version.Long())
return
}
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
opts := []prober.DERPOpt{
prober.WithMeshProbing(*meshInterval),
prober.WithSTUNProbing(*stunInterval),
prober.WithTLSProbing(*tlsInterval),
}
if *bwInterval > 0 {
opts = append(opts, prober.WithBandwidthProbing(*bwInterval, *bwSize))
}
dp, err := prober.DERP(p, *derpMapURL, opts...)
if err != nil {
log.Fatal(err)
}
@@ -53,6 +72,7 @@ func main() {
mux := http.NewServeMux()
tsweb.Debugger(mux)
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
log.Printf("Listening on %s", *listen)
log.Fatal(http.ListenAndServe(*listen, mux))
}

View File

@@ -67,45 +67,40 @@ func TestConnector(t *testing.T) {
fullName, shortName := findGenName(t, fc, "", "test", "connector")
opts := configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
shouldUseDeclarativeConfig: true,
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Add another route to be advertised.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.40.0.0/14", "10.44.0.0/20"}
})
opts.subnetRoutes = "10.40.0.0/14,10.44.0.0/20"
opts.confFileHash = "fb6c4daf67425f983985750cd8d6f2beae77e614fcb34176604571f5623d6862"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Remove a route.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.44.0.0/20"}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Remove the subnet router.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter = nil
})
opts.subnetRoutes = ""
opts.confFileHash = "7c421a99128eb80e79a285a82702f19f8f720615542a15bd794858a6275d8079"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Re-add the subnet router.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
@@ -114,9 +109,8 @@ func TestConnector(t *testing.T) {
}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Delete the Connector.
if err = fc.Delete(context.Background(), cn); err != nil {
@@ -152,25 +146,22 @@ func TestConnector(t *testing.T) {
fullName, shortName = findGenName(t, fc, "", "test", "connector")
opts = configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
shouldUseDeclarativeConfig: true,
subnetRoutes: "10.40.0.0/14",
hostname: "test-connector",
confFileHash: "57d922331890c9b1c8c6ae664394cb254334c551d9cd9db14537b5d9da9fb17e",
stsName: shortName,
secretName: fullName,
parentType: "connector",
subnetRoutes: "10.40.0.0/14",
hostname: "test-connector",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Add an exit node.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.ExitNode = true
})
opts.isExitNode = true
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Delete the Connector.
if err = fc.Delete(context.Background(), cn); err != nil {
@@ -239,17 +230,15 @@ func TestConnectorWithProxyClass(t *testing.T) {
fullName, shortName := findGenName(t, fc, "", "test", "connector")
opts := configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
shouldUseDeclarativeConfig: true,
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 2. Update Connector to specify a ProxyClass. ProxyClass is not yet
// ready, so its configuration is NOT applied to the Connector
@@ -258,7 +247,7 @@ func TestConnectorWithProxyClass(t *testing.T) {
conn.Spec.ProxyClass = "custom-metadata"
})
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 3. ProxyClass is set to Ready by proxy-class reconciler. Connector
// get reconciled and configuration from the ProxyClass is applied to
@@ -272,12 +261,8 @@ func TestConnectorWithProxyClass(t *testing.T) {
}}}
})
opts.proxyClass = pc.Name
// We lose the auth key on second reconcile, because in code it's set to
// StringData, but is actually read from Data. This works with a real
// API server, but not with our test setup here.
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 4. Connector.spec.proxyClass field is unset, Connector gets
// reconciled and configuration from the ProxyClass is removed from the
@@ -287,5 +272,5 @@ func TestConnectorWithProxyClass(t *testing.T) {
})
opts.proxyClass = ""
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
}

View File

@@ -12,7 +12,7 @@ oauth: {}
# of chart installation. We do not use Helm's CRD installation mechanism as that
# does not allow for upgrading CRDs.
# https://helm.sh/docs/chart_best_practices/custom_resource_definitions/
installCRDs: "true"
installCRDs: true
operatorConfig:
# ACL tag that operator will be tagged with. Operator must be made owner of

View File

@@ -31,6 +31,7 @@ spec:
name: v1alpha1
schema:
openAPIV3Schema:
description: 'Connector defines a Tailscale node that will be deployed in the cluster. The node can be configured to act as a Tailscale subnet router and/or a Tailscale exit node. Connector is a cluster-scoped resource. More info: https://tailscale.com/kb/1236/kubernetes-operator#deploying-exit-nodes-and-subnet-routers-on-kubernetes-using-connector-custom-resource'
type: object
required:
- spec
@@ -44,7 +45,7 @@ spec:
metadata:
type: object
spec:
description: ConnectorSpec describes the desired Tailscale component.
description: 'ConnectorSpec describes the desired Tailscale component. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status'
type: object
properties:
exitNode:

View File

@@ -21,6 +21,7 @@ spec:
name: v1alpha1
schema:
openAPIV3Schema:
description: 'ProxyClass describes a set of configuration parameters that can be applied to proxy resources created by the Tailscale Kubernetes operator. To apply a given ProxyClass to resources created for a tailscale Ingress or Service, use tailscale.com/proxy-class=<proxyclass-name> label. To apply a given ProxyClass to resources created for a Connector, use connector.spec.proxyClass field. ProxyClass is a cluster scoped resource. More info: https://tailscale.com/kb/1236/kubernetes-operator#cluster-resource-customization-using-proxyclass-custom-resource.'
type: object
required:
- spec
@@ -34,12 +35,13 @@ spec:
metadata:
type: object
spec:
description: Specification of the desired state of the ProxyClass resource. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
type: object
required:
- statefulSet
properties:
statefulSet:
description: Proxy's StatefulSet spec.
description: Configuration parameters for the proxy's StatefulSet. Tailscale Kubernetes operator deploys a StatefulSet for each of the user configured proxies (Tailscale Ingress, Tailscale Service, Connector).
type: object
properties:
annotations:
@@ -177,6 +179,21 @@ spec:
description: Configuration for the proxy container running tailscale.
type: object
properties:
env:
description: List of environment variables to set in the container. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables Note that environment variables provided here will take precedence over Tailscale-specific environment variables set by the operator, however running proxies with custom values for Tailscale environment variables (i.e TS_USERSPACE) is not recommended and might break in the future.
type: array
items:
type: object
required:
- name
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
pattern: ^[-._a-zA-Z][-._a-zA-Z0-9]*$
value:
description: 'Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".'
type: string
resources:
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
type: object
@@ -305,6 +322,21 @@ spec:
description: Configuration for the proxy init container that enables forwarding.
type: object
properties:
env:
description: List of environment variables to set in the container. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables Note that environment variables provided here will take precedence over Tailscale-specific environment variables set by the operator, however running proxies with custom values for Tailscale environment variables (i.e TS_USERSPACE) is not recommended and might break in the future.
type: array
items:
type: object
required:
- name
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
type: string
pattern: ^[-._a-zA-Z][-._a-zA-Z0-9]*$
value:
description: 'Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".'
type: string
resources:
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
type: object
@@ -453,6 +485,7 @@ spec:
description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
status:
description: Status of the ProxyClass. This is set and managed automatically. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
type: object
properties:
conditions:

View File

@@ -60,6 +60,7 @@ spec:
name: v1alpha1
schema:
openAPIV3Schema:
description: 'Connector defines a Tailscale node that will be deployed in the cluster. The node can be configured to act as a Tailscale subnet router and/or a Tailscale exit node. Connector is a cluster-scoped resource. More info: https://tailscale.com/kb/1236/kubernetes-operator#deploying-exit-nodes-and-subnet-routers-on-kubernetes-using-connector-custom-resource'
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
@@ -70,7 +71,7 @@ spec:
metadata:
type: object
spec:
description: ConnectorSpec describes the desired Tailscale component.
description: 'ConnectorSpec describes the desired Tailscale component. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status'
properties:
exitNode:
description: ExitNode defines whether the Connector node should act as a Tailscale exit node. Defaults to false. https://tailscale.com/kb/1103/exit-nodes
@@ -179,6 +180,7 @@ spec:
name: v1alpha1
schema:
openAPIV3Schema:
description: 'ProxyClass describes a set of configuration parameters that can be applied to proxy resources created by the Tailscale Kubernetes operator. To apply a given ProxyClass to resources created for a tailscale Ingress or Service, use tailscale.com/proxy-class=<proxyclass-name> label. To apply a given ProxyClass to resources created for a Connector, use connector.spec.proxyClass field. ProxyClass is a cluster scoped resource. More info: https://tailscale.com/kb/1236/kubernetes-operator#cluster-resource-customization-using-proxyclass-custom-resource.'
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
@@ -189,9 +191,10 @@ spec:
metadata:
type: object
spec:
description: Specification of the desired state of the ProxyClass resource. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
statefulSet:
description: Proxy's StatefulSet spec.
description: Configuration parameters for the proxy's StatefulSet. Tailscale Kubernetes operator deploys a StatefulSet for each of the user configured proxies (Tailscale Ingress, Tailscale Service, Connector).
properties:
annotations:
additionalProperties:
@@ -326,6 +329,21 @@ spec:
tailscaleContainer:
description: Configuration for the proxy container running tailscale.
properties:
env:
description: List of environment variables to set in the container. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables Note that environment variables provided here will take precedence over Tailscale-specific environment variables set by the operator, however running proxies with custom values for Tailscale environment variables (i.e TS_USERSPACE) is not recommended and might break in the future.
items:
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
pattern: ^[-._a-zA-Z][-._a-zA-Z0-9]*$
type: string
value:
description: 'Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".'
type: string
required:
- name
type: object
type: array
resources:
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
properties:
@@ -454,6 +472,21 @@ spec:
tailscaleInitContainer:
description: Configuration for the proxy init container that enables forwarding.
properties:
env:
description: List of environment variables to set in the container. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#environment-variables Note that environment variables provided here will take precedence over Tailscale-specific environment variables set by the operator, however running proxies with custom values for Tailscale environment variables (i.e TS_USERSPACE) is not recommended and might break in the future.
items:
properties:
name:
description: Name of the environment variable. Must be a C_IDENTIFIER.
pattern: ^[-._a-zA-Z][-._a-zA-Z0-9]*$
type: string
value:
description: 'Variable references $(VAR_NAME) are expanded using the previously defined environment variables in the container and any service environment variables. If a variable cannot be resolved, the reference in the input string will be unchanged. Double $$ are reduced to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". Escaped references will never be expanded, regardless of whether the variable exists or not. Defaults to "".'
type: string
required:
- name
type: object
type: array
resources:
description: Container resource requirements. By default Tailscale Kubernetes operator does not apply any resource requirements. The amount of resources required wil depend on the amount of resources the operator needs to parse, usage patterns and cluster size. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#resources
properties:
@@ -608,6 +641,7 @@ spec:
- statefulSet
type: object
status:
description: Status of the ProxyClass. This is set and managed automatically. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
properties:
conditions:
description: List of status conditions to indicate the status of the ProxyClass. Known condition types are `ProxyClassReady`.

View File

@@ -28,8 +28,6 @@ spec:
env:
- name: TS_USERSPACE
value: "false"
- name: TS_AUTH_ONCE
value: "true"
- name: POD_IP
valueFrom:
fieldRef:

View File

@@ -20,5 +20,3 @@ spec:
env:
- name: TS_USERSPACE
value: "true"
- name: TS_AUTH_ONCE
value: "true"

View File

@@ -100,9 +100,9 @@ func TestTailscaleIngress(t *testing.T) {
}
opts.serveConfig = serveConfig
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"), nil)
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation)
// 2. Ingress status gets updated with ingress proxy's MagicDNS name
// once that becomes available.
@@ -117,7 +117,7 @@ func TestTailscaleIngress(t *testing.T) {
{Hostname: "foo.tailnetxyz.ts.net", Ports: []networkingv1.IngressPortStatus{{Port: 443, Protocol: "TCP"}}},
},
}
expectEqual(t, fc, ing)
expectEqual(t, fc, ing, nil)
// 3. Resources get created for Ingress that should allow forwarding
// cluster traffic
@@ -126,7 +126,7 @@ func TestTailscaleIngress(t *testing.T) {
})
opts.shouldEnableForwardingClusterTrafficViaIngress = true
expectReconciled(t, ingR, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 4. Resources get cleaned up when Ingress class is unset
mustUpdate(t, fc, "default", "test", func(ing *networkingv1.Ingress) {
@@ -231,9 +231,9 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
}
opts.serveConfig = serveConfig
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "ingress"), nil)
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation)
// 2. Ingress is updated to specify a ProxyClass, ProxyClass is not yet
// ready, so proxy resource configuration does not change.
@@ -241,7 +241,7 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
mak.Set(&ing.ObjectMeta.Labels, LabelProxyClass, "custom-metadata")
})
expectReconciled(t, ingR, "default", "test")
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation)
// 3. ProxyClass is set to Ready by proxy-class reconciler. Ingress get
// reconciled and configuration from the ProxyClass is applied to the
@@ -256,7 +256,7 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
})
expectReconciled(t, ingR, "default", "test")
opts.proxyClass = pc.Name
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation)
// 4. tailscale.com/proxy-class label is removed from the Ingress, the
// Ingress gets reconciled and the custom ProxyClass configuration is
@@ -266,5 +266,5 @@ func TestTailscaleIngressWithProxyClass(t *testing.T) {
})
expectReconciled(t, ingR, "default", "test")
opts.proxyClass = ""
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts))
expectEqual(t, fc, expectedSTSUserspace(t, fc, opts), removeHashAnnotation)
}

View File

@@ -47,9 +47,12 @@ import (
// Generate static manifests for deploying Tailscale operator on Kubernetes from the operator's Helm chart.
//go:generate go run tailscale.com/cmd/k8s-operator/generate staticmanifests
// Generate Connector CustomResourceDefinition yaml from its Go types.
// Generate Connector and ProxyClass CustomResourceDefinition yamls from their Go types.
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen crd schemapatch:manifests=./deploy/crds output:dir=./deploy/crds paths=../../k8s-operator/apis/...
// Generate CRD docs from the yamls
//go:generate go run fybrik.io/crdoc --resources=./deploy/crds --output=../../k8s-operator/api.md
func main() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
@@ -269,12 +272,14 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
// If a ProxyClassChanges, enqueue all Ingresses labeled with that
// ProxyClass's name.
proxyClassFilterForIngress := handler.EnqueueRequestsFromMapFunc(proxyClassHandlerForIngress(mgr.GetClient(), startlog))
// Enque Ingress if a managed Service or backend Service associated with a tailscale Ingress changes.
svcHandlerForIngress := handler.EnqueueRequestsFromMapFunc(serviceHandlerForIngress(mgr.GetClient(), startlog))
err = builder.
ControllerManagedBy(mgr).
For(&networkingv1.Ingress{}).
Watches(&appsv1.StatefulSet{}, ingressChildFilter).
Watches(&corev1.Secret{}, ingressChildFilter).
Watches(&corev1.Service{}, ingressChildFilter).
Watches(&corev1.Service{}, svcHandlerForIngress).
Watches(&tsapi.ProxyClass{}, proxyClassFilterForIngress).
Complete(&IngressReconciler{
ssr: ssr,
@@ -419,6 +424,46 @@ func proxyClassHandlerForConnector(cl client.Client, logger *zap.SugaredLogger)
}
}
// serviceHandlerForIngress returns a handler for Service events for ingress
// reconciler that ensures that if the Service associated with an event is of
// interest to the reconciler, the associated Ingress(es) gets be reconciled.
// The Services of interest are backend Services for tailscale Ingress and
// managed Services for an StatefulSet for a proxy configured for tailscale
// Ingress
func serviceHandlerForIngress(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
if isManagedByType(o, "ingress") {
ingName := parentFromObjectLabels(o)
return []reconcile.Request{{NamespacedName: ingName}}
}
ingList := networkingv1.IngressList{}
if err := cl.List(ctx, &ingList, client.InNamespace(o.GetNamespace())); err != nil {
logger.Debugf("error listing Ingresses: %v", err)
return nil
}
reqs := make([]reconcile.Request, 0)
for _, ing := range ingList.Items {
if ing.Spec.IngressClassName == nil || *ing.Spec.IngressClassName != tailscaleIngressClassName {
return nil
}
if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil && ing.Spec.DefaultBackend.Service.Name == o.GetName() {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&ing)})
}
for _, rule := range ing.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Service != nil && path.Backend.Service.Name == o.GetName() {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&ing)})
}
}
}
}
return reqs
}
}
func serviceHandler(_ context.Context, o client.Object) []reconcile.Request {
if isManagedByType(o, "svc") {
// If this is a Service managed by a Service we want to enqueue its parent
@@ -437,7 +482,6 @@ func serviceHandler(_ context.Context, o client.Object) []reconcile.Request {
},
},
}
}
// isMagicDNSName reports whether name is a full tailnet node FQDN (with or

View File

@@ -6,15 +6,19 @@
package main
import (
"context"
"fmt"
"testing"
"github.com/google/go-cmp/cmp"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
@@ -69,9 +73,9 @@ func TestLoadBalancerClass(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -114,7 +118,7 @@ func TestLoadBalancerClass(t *testing.T) {
},
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
// Turn the service back into a ClusterIP service, which should make the
// operator clean up.
@@ -153,7 +157,7 @@ func TestLoadBalancerClass(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
}
func TestTailnetTargetFQDNAnnotation(t *testing.T) {
@@ -210,9 +214,9 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
hostname: "default-test",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -233,10 +237,10 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
Selector: nil,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, want, nil)
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
// Change the tailscale-target-fqdn annotation which should update the
// StatefulSet
@@ -320,9 +324,9 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
hostname: "default-test",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -343,10 +347,10 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
Selector: nil,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, want, nil)
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
// Change the tailscale-target-ip annotation which should update the
// StatefulSet
@@ -427,9 +431,9 @@ func TestAnnotations(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -449,7 +453,7 @@ func TestAnnotations(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
// Turn the service back into a ClusterIP service, which should make the
// operator clean up.
@@ -481,7 +485,7 @@ func TestAnnotations(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
}
func TestAnnotationIntoLB(t *testing.T) {
@@ -535,9 +539,9 @@ func TestAnnotationIntoLB(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, since it would have normally happened at
@@ -570,7 +574,7 @@ func TestAnnotationIntoLB(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
// Remove Tailscale's annotation, and at the same time convert the service
// into a tailscale LoadBalancer.
@@ -581,8 +585,8 @@ func TestAnnotationIntoLB(t *testing.T) {
})
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
// ... but the service should have a LoadBalancer status.
want = &corev1.Service{
@@ -614,7 +618,7 @@ func TestAnnotationIntoLB(t *testing.T) {
},
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
}
func TestLBIntoAnnotation(t *testing.T) {
@@ -666,9 +670,9 @@ func TestLBIntoAnnotation(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -711,7 +715,7 @@ func TestLBIntoAnnotation(t *testing.T) {
},
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
// Turn the service back into a ClusterIP service, but also add the
// tailscale annotation.
@@ -730,8 +734,8 @@ func TestLBIntoAnnotation(t *testing.T) {
})
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -752,7 +756,7 @@ func TestLBIntoAnnotation(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
}
func TestCustomHostname(t *testing.T) {
@@ -807,9 +811,9 @@ func TestCustomHostname(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSecret(t, o), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -830,7 +834,7 @@ func TestCustomHostname(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
// Turn the service back into a ClusterIP service, which should make the
// operator clean up.
@@ -865,7 +869,7 @@ func TestCustomHostname(t *testing.T) {
Type: corev1.ServiceTypeClusterIP,
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, want, nil)
}
func TestCustomPriorityClassName(t *testing.T) {
@@ -922,7 +926,7 @@ func TestCustomPriorityClassName(t *testing.T) {
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
}
func TestProxyClassForService(t *testing.T) {
@@ -983,9 +987,9 @@ func TestProxyClassForService(t *testing.T) {
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSecret(t, opts), nil)
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 2. The Service gets updated with tailscale.com/proxy-class label
// pointing at the 'custom-metadata' ProxyClass. The ProxyClass is not
@@ -994,7 +998,7 @@ func TestProxyClassForService(t *testing.T) {
mak.Set(&svc.Labels, LabelProxyClass, "custom-metadata")
})
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 3. ProxyClass is set to Ready, the Service gets reconciled by the
// services-reconciler and the customization from the ProxyClass is
@@ -1009,7 +1013,7 @@ func TestProxyClassForService(t *testing.T) {
})
opts.proxyClass = pc.Name
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
// 4. tailscale.com/proxy-class label is removed from the Service, the
// configuration from the ProxyClass is removed from the cluster
@@ -1019,7 +1023,7 @@ func TestProxyClassForService(t *testing.T) {
})
opts.proxyClass = ""
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, opts))
expectEqual(t, fc, expectedSTS(t, fc, opts), removeHashAnnotation)
}
func TestDefaultLoadBalancer(t *testing.T) {
@@ -1063,7 +1067,7 @@ func TestDefaultLoadBalancer(t *testing.T) {
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"))
expectEqual(t, fc, expectedHeadlessService(shortName, "svc"), nil)
o := configOpts{
stsName: shortName,
secretName: fullName,
@@ -1072,7 +1076,8 @@ func TestDefaultLoadBalancer(t *testing.T) {
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
}
func TestProxyFirewallMode(t *testing.T) {
@@ -1125,8 +1130,69 @@ func TestProxyFirewallMode(t *testing.T) {
firewallMode: "nftables",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(t, fc, o))
expectEqual(t, fc, expectedSTS(t, fc, o), removeHashAnnotation)
}
func TestTailscaledConfigfileHash(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
logger: zl.Sugar(),
isDefaultLoadBalancer: true,
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeLoadBalancer,
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
}
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
// 2. Hostname gets changed, configfile is updated and a new hash value
// is produced.
mustUpdate(t, fc, "default", "test", func(svc *corev1.Service) {
mak.Set(&svc.Annotations, AnnotationHostname, "another-test")
})
o.hostname = "another-test"
o.confFileHash = "1a087f887825d2b75d3673c7c2b0131f8ec1f0b1cb761d33e236dd28350dfe23"
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
}
func Test_isMagicDNSName(t *testing.T) {
@@ -1155,3 +1221,134 @@ func Test_isMagicDNSName(t *testing.T) {
})
}
}
func Test_serviceHandlerForIngress(t *testing.T) {
fc := fake.NewFakeClient()
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
// 1. An event on a headless Service for a tailscale Ingress results in
// the Ingress being reconciled.
mustCreate(t, fc, &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ing-1",
Namespace: "ns-1",
},
Spec: networkingv1.IngressSpec{IngressClassName: ptr.To(tailscaleIngressClassName)},
})
svc1 := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "headless-1",
Namespace: "tailscale",
Labels: map[string]string{
LabelManaged: "true",
LabelParentName: "ing-1",
LabelParentNamespace: "ns-1",
LabelParentType: "ingress",
},
},
}
mustCreate(t, fc, svc1)
wantReqs := []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-1", Name: "ing-1"}}}
gotReqs := serviceHandlerForIngress(fc, zl.Sugar())(context.Background(), svc1)
if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
}
// 2. An event on a Service that is the default backend for a tailscale
// Ingress results in the Ingress being reconciled.
mustCreate(t, fc, &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ing-2",
Namespace: "ns-2",
},
Spec: networkingv1.IngressSpec{
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{Name: "def-backend"},
},
IngressClassName: ptr.To(tailscaleIngressClassName),
},
})
backendSvc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "def-backend",
Namespace: "ns-2",
},
}
mustCreate(t, fc, backendSvc)
wantReqs = []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-2", Name: "ing-2"}}}
gotReqs = serviceHandlerForIngress(fc, zl.Sugar())(context.Background(), backendSvc)
if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
}
// 3. An event on a Service that is one of the non-default backends for
// a tailscale Ingress results in the Ingress being reconciled.
mustCreate(t, fc, &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ing-3",
Namespace: "ns-3",
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To(tailscaleIngressClassName),
Rules: []networkingv1.IngressRule{{IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{Backend: networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "backend"}}}},
}}}},
},
})
backendSvc2 := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "backend",
Namespace: "ns-3",
},
}
mustCreate(t, fc, backendSvc2)
wantReqs = []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: "ns-3", Name: "ing-3"}}}
gotReqs = serviceHandlerForIngress(fc, zl.Sugar())(context.Background(), backendSvc2)
if diff := cmp.Diff(gotReqs, wantReqs); diff != "" {
t.Fatalf("unexpected reconcile requests (-got +want):\n%s", diff)
}
// 4. An event on a Service that is a backend for an Ingress that is not
// tailscale Ingress does not result in an Ingress reconcile.
mustCreate(t, fc, &networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "ing-4",
Namespace: "ns-4",
},
Spec: networkingv1.IngressSpec{
Rules: []networkingv1.IngressRule{{IngressRuleValue: networkingv1.IngressRuleValue{HTTP: &networkingv1.HTTPIngressRuleValue{
Paths: []networkingv1.HTTPIngressPath{
{Backend: networkingv1.IngressBackend{Service: &networkingv1.IngressServiceBackend{Name: "non-ts-backend"}}}},
}}}},
},
})
nonTSBackend := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "non-ts-backend",
Namespace: "ns-4",
},
}
mustCreate(t, fc, nonTSBackend)
gotReqs = serviceHandlerForIngress(fc, zl.Sugar())(context.Background(), nonTSBackend)
if len(gotReqs) > 0 {
t.Errorf("unexpected reconcile request for a Service that does not belong to a Tailscale Ingress: %#+v\n", gotReqs)
}
// 5. An event on a Service not related to any Ingress does not result
// in an Ingress reconcile.
someSvc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "some-svc",
Namespace: "ns-4",
},
}
mustCreate(t, fc, someSvc)
gotReqs = serviceHandlerForIngress(fc, zl.Sugar())(context.Background(), someSvc)
if len(gotReqs) > 0 {
t.Errorf("unexpected reconcile request for a Service that does not belong to any Ingress: %#+v\n", gotReqs)
}
}

View File

@@ -10,6 +10,7 @@ package main
import (
"context"
"fmt"
"strings"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
@@ -30,7 +31,9 @@ import (
const (
reasonProxyClassInvalid = "ProxyClassInvalid"
reasonProxyClassValid = "ProxyClassValid"
reasonCustomTSEnvVar = "CustomTSEnvVar"
messageProxyClassInvalid = "ProxyClass is not valid: %v"
messageCustomTSEnvVar = "ProxyClass overrides the default value for %s env var for %s container. Running with custom values for Tailscale env vars is not recommended and might break in the future."
)
type ProxyClassReconciler struct {
@@ -98,6 +101,19 @@ func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.
violations = append(violations, errs...)
}
}
if tc := pod.TailscaleContainer; tc != nil {
for _, e := range tc.Env {
if strings.HasPrefix(string(e.Name), "TS_") {
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
}
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_TS_CONFIGFILE_PATH") {
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
}
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS") {
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
}
}
}
}
}
// We do not validate embedded fields (security context, resource

View File

@@ -36,8 +36,9 @@ func TestProxyClass(t *testing.T) {
Labels: map[string]string{"foo": "bar", "xyz1234": "abc567"},
Annotations: map[string]string{"foo.io/bar": "{'key': 'val1232'}"},
Pod: &tsapi.Pod{
Labels: map[string]string{"foo": "bar", "xyz1234": "abc567"},
Annotations: map[string]string{"foo.io/bar": "{'key': 'val1232'}"},
Labels: map[string]string{"foo": "bar", "xyz1234": "abc567"},
Annotations: map[string]string{"foo.io/bar": "{'key': 'val1232'}"},
TailscaleContainer: &tsapi.Container{Env: []tsapi.Env{{Name: "FOO", Value: "BAR"}}},
},
},
},
@@ -51,16 +52,17 @@ func TestProxyClass(t *testing.T) {
if err != nil {
t.Fatal(err)
}
fr := record.NewFakeRecorder(3) // bump this if you expect a test case to throw more events
cl := tstest.NewClock(tstest.ClockOpts{})
pcr := &ProxyClassReconciler{
Client: fc,
logger: zl.Sugar(),
clock: cl,
recorder: record.NewFakeRecorder(1),
recorder: fr,
}
expectReconciled(t, pcr, "", "test")
// 1. A valid ProxyClass resource gets its status updated to Ready.
expectReconciled(t, pcr, "", "test")
pc.Status.Conditions = append(pc.Status.Conditions, tsapi.ConnectorCondition{
Type: tsapi.ProxyClassready,
Status: metav1.ConditionTrue,
@@ -69,7 +71,7 @@ func TestProxyClass(t *testing.T) {
LastTransitionTime: &metav1.Time{Time: cl.Now().Truncate(time.Second)},
})
expectEqual(t, fc, pc)
expectEqual(t, fc, pc, nil)
// 2. An invalid ProxyClass resource gets its status updated to Invalid.
pc.Spec.StatefulSet.Labels["foo"] = "?!someVal"
@@ -79,5 +81,18 @@ func TestProxyClass(t *testing.T) {
expectReconciled(t, pcr, "", "test")
msg := `ProxyClass is not valid: .spec.statefulSet.labels: Invalid value: "?!someVal": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')`
tsoperator.SetProxyClassCondition(pc, tsapi.ProxyClassready, metav1.ConditionFalse, reasonProxyClassInvalid, msg, 0, cl, zl.Sugar())
expectEqual(t, fc, pc)
expectEqual(t, fc, pc, nil)
expectedEvent := "Warning ProxyClassInvalid ProxyClass is not valid: .spec.statefulSet.labels: Invalid value: \"?!someVal\": a valid label must be an empty string or consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyValue', or 'my_value', or '12345', regex used for validation is '(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?')"
expectEvents(t, fr, []string{expectedEvent})
// 2. An valid ProxyClass but with a Tailscale env vars set results in warning events.
mustUpdate(t, fc, "", "test", func(proxyClass *tsapi.ProxyClass) {
proxyClass.Spec.StatefulSet.Labels = nil // unset invalid labels from the previous test
proxyClass.Spec.StatefulSet.Pod.TailscaleContainer.Env = []tsapi.Env{{Name: "TS_USERSPACE", Value: "true"}, {Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH"}, {Name: "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS"}}
})
expectedEvents := []string{"Warning CustomTSEnvVar ProxyClass overrides the default value for TS_USERSPACE env var for tailscale container. Running with custom values for Tailscale env vars is not recommended and might break in the future.",
"Warning CustomTSEnvVar ProxyClass overrides the default value for EXPERIMENTAL_TS_CONFIGFILE_PATH env var for tailscale container. Running with custom values for Tailscale env vars is not recommended and might break in the future.",
"Warning CustomTSEnvVar ProxyClass overrides the default value for EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS env var for tailscale container. Running with custom values for Tailscale env vars is not recommended and might break in the future."}
expectReconciled(t, pcr, "", "test")
expectEvents(t, fr, expectedEvents)
}

View File

@@ -34,6 +34,7 @@ import (
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
)
@@ -86,7 +87,6 @@ const (
// ensure that it does not get removed when a ProxyClass configuration
// is applied.
podAnnotationLastSetClusterIP = "tailscale.com/operator-last-set-cluster-ip"
podAnnotationLastSetHostname = "tailscale.com/operator-last-set-hostname"
podAnnotationLastSetTailnetTargetIP = "tailscale.com/operator-last-set-ts-tailnet-target-ip"
podAnnotationLastSetTailnetTargetFQDN = "tailscale.com/operator-last-set-ts-tailnet-target-fqdn"
// podAnnotationLastSetConfigFileHash is sha256 hash of the current tailscaled configuration contents.
@@ -101,7 +101,7 @@ var (
// tailscaleManagedLabels are label keys that tailscale operator sets on StatefulSets and Pods.
tailscaleManagedLabels = []string{LabelManaged, LabelParentType, LabelParentName, LabelParentNamespace, "app"}
// tailscaleManagedAnnotations are annotation keys that tailscale operator sets on StatefulSets and Pods.
tailscaleManagedAnnotations = []string{podAnnotationLastSetClusterIP, podAnnotationLastSetHostname, podAnnotationLastSetTailnetTargetIP, podAnnotationLastSetTailnetTargetFQDN, podAnnotationLastSetConfigFileHash}
tailscaleManagedAnnotations = []string{podAnnotationLastSetClusterIP, podAnnotationLastSetTailnetTargetIP, podAnnotationLastSetTailnetTargetFQDN, podAnnotationLastSetConfigFileHash}
)
type tailscaleSTSConfig struct {
@@ -312,9 +312,9 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
authKey, hash string
)
if orig == nil {
// Secret doesn't exist yet, create one. Initially it contains
// only the Tailscale authkey, but once Tailscale starts it'll
// also store the daemon state.
// Initially it contains only tailscaled config, but when the
// proxy starts, it will also store there the state, certs and
// ACME account key.
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, stsC.ChildResourceLabels)
if err != nil {
return "", "", err
@@ -337,17 +337,13 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
return "", "", err
}
}
if !shouldDoTailscaledDeclarativeConfig(stsC) && authKey != "" {
mak.Set(&secret.StringData, "authkey", authKey)
}
if shouldDoTailscaledDeclarativeConfig(stsC) {
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
if err != nil {
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
}
hash = h
mak.Set(&secret.StringData, tailscaledConfigKey, string(confFileBytes))
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
if err != nil {
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
}
hash = h
mak.Set(&secret.StringData, tailscaledConfigKey, string(confFileBytes))
if stsC.ServeConfig != nil {
j, err := json.Marshal(stsC.ServeConfig)
if err != nil {
@@ -357,12 +353,12 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
}
if orig != nil {
logger.Debugf("patching existing state Secret with values %s", secret.Data[tailscaledConfigKey])
logger.Debugf("patching the existing proxy Secret with tailscaled config %s", sanitizeConfigBytes(secret.Data[tailscaledConfigKey]))
if err := a.Patch(ctx, secret, client.MergeFrom(orig)); err != nil {
return "", "", err
}
} else {
logger.Debugf("creating new state Secret with authkey %s", secret.Data[tailscaledConfigKey])
logger.Debugf("creating a new Secret for the proxy with tailscaled config %s", sanitizeConfigBytes([]byte(secret.StringData[tailscaledConfigKey])))
if err := a.Create(ctx, secret); err != nil {
return "", "", err
}
@@ -370,6 +366,23 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
return secret.Name, hash, nil
}
// sanitizeConfigBytes returns ipn.ConfigVAlpha in string form with redacted
// auth key.
func sanitizeConfigBytes(bs []byte) string {
c := &ipn.ConfigVAlpha{}
if err := json.Unmarshal(bs, c); err != nil {
return "invalid config"
}
if c.AuthKey != nil {
c.AuthKey = ptr.To("**redacted**")
}
sanitizedBytes, err := json.Marshal(c)
if err != nil {
return "invalid config"
}
return string(sanitizedBytes)
}
// DeviceInfo returns the device ID and hostname for the Tailscale device
// associated with the given labels.
func (a *tailscaleSTSReconciler) DeviceInfo(ctx context.Context, childLabels map[string]string) (id tailcfg.StableNodeID, hostname string, ips []string, err error) {
@@ -477,6 +490,10 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
Name: "TS_KUBE_SECRET",
Value: proxySecret,
},
corev1.EnvVar{
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
Value: "/etc/tsconfig/tailscaled",
},
)
if sts.ForwardClusterTrafficViaL7IngressProxy {
container.Env = append(container.Env, corev1.EnvVar{
@@ -484,42 +501,25 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
Value: "true",
})
}
if !shouldDoTailscaledDeclarativeConfig(sts) {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_HOSTNAME",
Value: sts.Hostname,
})
// containerboot currently doesn't have a way to re-read the hostname/ip as
// it is passed via an environment variable. So we need to restart the
// container when the value changes. We do this by adding an annotation to
// the pod template that contains the last value we set.
mak.Set(&pod.Annotations, podAnnotationLastSetHostname, sts.Hostname)
}
// Configure containeboot to run tailscaled with a configfile read from the state Secret.
if shouldDoTailscaledDeclarativeConfig(sts) {
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: proxySecret,
Items: []corev1.KeyToPath{{
Key: tailscaledConfigKey,
Path: tailscaledConfigKey,
}},
},
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: proxySecret,
Items: []corev1.KeyToPath{{
Key: tailscaledConfigKey,
Path: tailscaledConfigKey,
}},
},
})
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
})
container.Env = append(container.Env, corev1.EnvVar{
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
Value: "/etc/tsconfig/tailscaled",
})
}
},
})
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
})
if a.tsFirewallMode != "" {
container.Env = append(container.Env, corev1.EnvVar{
@@ -644,6 +644,15 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet)
base.SecurityContext = overlay.SecurityContext
}
base.Resources = overlay.Resources
for _, e := range overlay.Env {
// Env vars configured via ProxyClass might override env
// vars that have been specified by the operator, i.e
// TS_USERSPACE. The intended behaviour is to allow this
// and in practice it works without explicitly removing
// the operator configured value here as a later value
// in the env var list overrides an earlier one.
base.Env = append(base.Env, corev1.EnvVar{Name: string(e.Name), Value: e.Value})
}
return base
}
for i, c := range ss.Spec.Template.Spec.Containers {
@@ -668,10 +677,11 @@ func applyProxyClassToStatefulSet(pc *tsapi.ProxyClass, ss *appsv1.StatefulSet)
// produces returns tailscaled configuration and a hash of that configuration.
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret) ([]byte, string, error) {
conf := ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Locked: "false",
Hostname: &stsC.Hostname,
Version: "alpha0",
AcceptDNS: "false",
AcceptRoutes: "false", // AcceptRoutes defaults to true
Locked: "false",
Hostname: &stsC.Hostname,
}
if stsC.Connector != nil {
routes, err := netutil.CalcAdvertiseRoutes(stsC.Connector.routes, stsC.Connector.isExitNode)
@@ -828,10 +838,3 @@ func nameForService(svc *corev1.Service) (string, error) {
func isValidFirewallMode(m string) bool {
return m == "auto" || m == "nftables" || m == "iptables"
}
// shouldDoTailscaledDeclarativeConfig determines whether the proxy instance
// should be configured to run tailscaled only with a all config opts passed to
// tailscaled.
func shouldDoTailscaledDeclarativeConfig(stsC *tailscaleSTSConfig) bool {
return stsC.Connector != nil
}

View File

@@ -75,6 +75,7 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("128Mi")},
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
},
Env: []tsapi.Env{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}},
},
TailscaleInitContainer: &tsapi.Container{
SecurityContext: &corev1.SecurityContext{
@@ -85,6 +86,7 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
Limits: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1000m"), corev1.ResourceMemory: resource.MustParse("128Mi")},
Requests: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("64Mi")},
},
Env: []tsapi.Env{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}},
},
},
},
@@ -142,6 +144,8 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
wantSS.Spec.Template.Spec.InitContainers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleInitContainer.SecurityContext
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
wantSS.Spec.Template.Spec.InitContainers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleInitContainer.Resources
wantSS.Spec.Template.Spec.InitContainers[0].Env = append(wantSS.Spec.Template.Spec.InitContainers[0].Env, []corev1.EnvVar{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}}...)
wantSS.Spec.Template.Spec.Containers[0].Env = append(wantSS.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}}...)
gotSS := applyProxyClassToStatefulSet(proxyClassAllOpts, nonUserspaceProxySS.DeepCopy())
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
@@ -175,6 +179,7 @@ func Test_applyProxyClassToStatefulSet(t *testing.T) {
wantSS.Spec.Template.Spec.Tolerations = proxyClassAllOpts.Spec.StatefulSet.Pod.Tolerations
wantSS.Spec.Template.Spec.Containers[0].SecurityContext = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.SecurityContext
wantSS.Spec.Template.Spec.Containers[0].Resources = proxyClassAllOpts.Spec.StatefulSet.Pod.TailscaleContainer.Resources
wantSS.Spec.Template.Spec.Containers[0].Env = append(wantSS.Spec.Template.Spec.Containers[0].Env, []corev1.EnvVar{{Name: "foo", Value: "bar"}, {Name: "TS_USERSPACE", Value: "true"}, {Name: "bar"}}...)
gotSS = applyProxyClassToStatefulSet(proxyClassAllOpts, userspaceProxySS.DeepCopy())
if diff := cmp.Diff(gotSS, wantSS); diff != "" {
t.Fatalf("Unexpected result applying ProxyClass with custom labels and annotations to a StatefulSet for a userspace proxy (-got +want):\n%s", diff)
@@ -247,28 +252,28 @@ func Test_mergeStatefulSetLabelsOrAnnots(t *testing.T) {
},
{
name: "no custom annots specified and none present in current annots, return current annots",
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "no custom annots specified, but some present in current annots, return tailscale managed annots only from the current annots",
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
want: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "custom annots specified, current annots only contain tailscale managed annots, return a union of both",
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
current: map[string]string{podAnnotationLastSetClusterIP: "1.2.3.4"},
custom: map[string]string{"foo": "bar", "something.io/foo": "bar"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
want: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{
name: "custom annots specified, current annots contain tailscale managed annots and custom annots, some of which are not present in the new custom annots, return a union of managed annots and the desired custom annots",
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
current: map[string]string{"foo": "bar", "something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
custom: map[string]string{"something.io/foo": "bar"},
want: map[string]string{"something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4", podAnnotationLastSetHostname: "foo"},
want: map[string]string{"something.io/foo": "bar", podAnnotationLastSetClusterIP: "1.2.3.4"},
managed: tailscaleManagedAnnotations,
},
{

View File

@@ -20,6 +20,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
@@ -44,7 +45,6 @@ type configOpts struct {
clusterTargetIP string
subnetRoutes string
isExitNode bool
shouldUseDeclarativeConfig bool // tailscaled in proxy should be configured using config file
confFileHash string
serveConfig *ipn.ServeConfig
shouldEnableForwardingClusterTrafficViaIngress bool
@@ -58,9 +58,9 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
Image: "tailscale/tailscale",
Env: []corev1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
{Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH", Value: "/etc/tsconfig/tailscaled"},
},
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
@@ -77,36 +77,29 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
}
annots := make(map[string]string)
var volumes []corev1.Volume
if opts.shouldUseDeclarativeConfig {
volumes = []corev1.Volume{
{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: opts.secretName,
Items: []corev1.KeyToPath{
{
Key: "tailscaled",
Path: "tailscaled",
},
volumes = []corev1.Volume{
{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: opts.secretName,
Items: []corev1.KeyToPath{
{
Key: "tailscaled",
Path: "tailscaled",
},
},
},
},
}
tsContainer.VolumeMounts = []corev1.VolumeMount{{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
}}
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
Value: "/etc/tsconfig/tailscaled",
})
},
}
tsContainer.VolumeMounts = []corev1.VolumeMount{{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
}}
if opts.confFileHash != "" {
annots["tailscale.com/operator-last-set-config-file-hash"] = opts.confFileHash
} else {
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{Name: "TS_HOSTNAME", Value: opts.hostname})
annots["tailscale.com/operator-last-set-hostname"] = opts.hostname
}
if opts.firewallMode != "" {
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
@@ -211,22 +204,43 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
}
func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *appsv1.StatefulSet {
t.Helper()
tsContainer := corev1.Container{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []corev1.EnvVar{
{Name: "TS_USERSPACE", Value: "true"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
{Name: "TS_HOSTNAME", Value: opts.hostname},
{Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH", Value: "/etc/tsconfig/tailscaled"},
{Name: "TS_SERVE_CONFIG", Value: "/etc/tailscaled/serve-config"},
},
ImagePullPolicy: "Always",
VolumeMounts: []corev1.VolumeMount{{Name: "serve-config", ReadOnly: true, MountPath: "/etc/tailscaled"}},
VolumeMounts: []corev1.VolumeMount{
{Name: "tailscaledconfig", ReadOnly: true, MountPath: "/etc/tsconfig"},
{Name: "serve-config", ReadOnly: true, MountPath: "/etc/tailscaled"},
},
}
volumes := []corev1.Volume{
{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: opts.secretName,
Items: []corev1.KeyToPath{
{
Key: "tailscaled",
Path: "tailscaled",
},
},
},
},
},
{Name: "serve-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName,
Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}},
},
}
annots := make(map[string]string)
volumes := []corev1.Volume{{Name: "serve-config", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}}}}
annots["tailscale.com/operator-last-set-hostname"] = opts.hostname
ss := &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
@@ -250,7 +264,6 @@ func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *apps
ServiceName: opts.stsName,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: annots,
DeletionGracePeriodSeconds: ptr.To[int64](10),
Labels: map[string]string{
"tailscale.com/managed": "true",
@@ -269,6 +282,10 @@ func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *apps
},
},
}
ss.Spec.Template.Annotations = map[string]string{}
if opts.confFileHash != "" {
ss.Spec.Template.Annotations["tailscale.com/operator-last-set-config-file-hash"] = opts.confFileHash
}
// If opts.proxyClass is set, retrieve the ProxyClass and apply
// configuration from that to the StatefulSet.
if opts.proxyClass != "" {
@@ -310,11 +327,6 @@ func expectedHeadlessService(name string, parentType string) *corev1.Service {
func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
t.Helper()
labels := map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-type": opts.parentType,
}
s := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
@@ -332,37 +344,41 @@ func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
}
mak.Set(&s.StringData, "serve-config", string(serveConfigBs))
}
if !opts.shouldUseDeclarativeConfig {
mak.Set(&s.StringData, "authkey", "secret-authkey")
labels["tailscale.com/parent-resource-ns"] = opts.namespace
} else {
conf := &ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Hostname: &opts.hostname,
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
conf := &ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Hostname: &opts.hostname,
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
AcceptRoutes: "false",
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {
r := opts.subnetRoutes
if opts.isExitNode {
r = "0.0.0.0/0,::/0," + r
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {
r := opts.subnetRoutes
if opts.isExitNode {
r = "0.0.0.0/0,::/0," + r
}
for _, rr := range strings.Split(r, ",") {
prefix, err := netip.ParsePrefix(rr)
if err != nil {
t.Fatal(err)
}
routes = append(routes, prefix)
for _, rr := range strings.Split(r, ",") {
prefix, err := netip.ParsePrefix(rr)
if err != nil {
t.Fatal(err)
}
routes = append(routes, prefix)
}
conf.AdvertiseRoutes = routes
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
mak.Set(&s.StringData, "tailscaled", string(b))
}
conf.AdvertiseRoutes = routes
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
mak.Set(&s.StringData, "tailscaled", string(b))
labels := map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": opts.parentType,
}
if opts.parentType == "connector" {
labels["tailscale.com/parent-resource-ns"] = "" // Connector is cluster scoped
}
s.Labels = labels
@@ -424,7 +440,13 @@ func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client,
}
}
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
// expectEqual accepts a Kubernetes object and a Kubernetes client. It tests
// whether an object with equivalent contents can be retrieved by the passed
// client. If you want to NOT test some object fields for equality, ensure that
// they are not present in the passed object and use the modify func to remove
// them from the cluster object. If no such modifications are needed, you can
// pass nil in place of the modify function.
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O, modify func(O)) {
t.Helper()
got := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
@@ -438,6 +460,9 @@ func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want
// so just remove it from both got and want.
got.SetResourceVersion("")
want.SetResourceVersion("")
if modify != nil {
modify(got)
}
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("unexpected object (-got +want):\n%s", diff)
}
@@ -491,6 +516,34 @@ func expectRequeue(t *testing.T, sr reconcile.Reconciler, ns, name string) {
}
}
// expectEvents accepts a test recorder and a list of events, tests that expected
// events are sent down the recorder's channel. Waits for 5s for each event.
func expectEvents(t *testing.T, rec *record.FakeRecorder, wantsEvents []string) {
t.Helper()
// Events are not expected to arrive in order.
seenEvents := make([]string, 0)
for range len(wantsEvents) {
timer := time.NewTimer(time.Second * 5)
defer timer.Stop()
select {
case gotEvent := <-rec.Events:
found := false
for _, wantEvent := range wantsEvents {
if wantEvent == gotEvent {
found = true
seenEvents = append(seenEvents, gotEvent)
break
}
}
if !found {
t.Errorf("got unexpected event %q, expected events: %+#v", gotEvent, wantsEvents)
}
case <-timer.C:
t.Errorf("timeout waiting for an event, wants events %#+v, got events %+#v", wantsEvents, seenEvents)
}
}
}
type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
@@ -534,3 +587,11 @@ func (c *fakeTSClient) Deleted() []string {
defer c.Unlock()
return c.deleted
}
// removeHashAnnotation can be used to remove declarative tailscaled config hash
// annotation from proxy StatefulSets to make the tests more maintainable (so
// that we don't have to change the annotation in each test case after any
// change to the configfile contents).
func removeHashAnnotation(sts *appsv1.StatefulSet) {
delete(sts.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash)
}

View File

@@ -314,7 +314,7 @@ func mustMakeNamesByAddr() map[netip.Addr]string {
seen := make(map[string]bool)
namesByAddr := make(map[netip.Addr]string)
retry:
for i := 0; i < 10; i++ {
for i := range 10 {
clear(seen)
clear(namesByAddr)
for _, d := range m.Devices {
@@ -354,7 +354,7 @@ func fieldPrefix(s string, n int) string {
}
func appendRepeatByte(b []byte, c byte, n int) []byte {
for i := 0; i < n; i++ {
for range n {
b = append(b, c)
}
return b

View File

@@ -88,7 +88,7 @@ func main() {
go func() {
// wait for tailscale to start before trying to fetch cert names
for i := 0; i < 60; i++ {
for range 60 {
st, err := localClient.Status(context.Background())
if err != nil {
log.Printf("error retrieving tailscale status; retrying: %v", err)

View File

@@ -158,7 +158,7 @@ func TestSNIProxyWithNetmapConfig(t *testing.T) {
t.Fatal(err)
}
gotConfigured := false
for i := 0; i < 100; i++ {
for range 100 {
s, err := l.StatusWithoutPeers(ctx)
if err != nil {
t.Fatal(err)

View File

@@ -2,7 +2,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/google/uuid from tailscale.com/tsweb
github.com/google/uuid from tailscale.com/util/fastuuid
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
@@ -65,6 +65,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/fastuuid from tailscale.com/tsweb
tailscale.com/util/lineread from tailscale.com/version/distro
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/slicesx from tailscale.com/tailcfg
@@ -151,6 +152,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from math/big+
math/rand/v2 from tailscale.com/util/fastuuid
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart

View File

@@ -17,7 +17,7 @@ var bugReportCmd = &ffcli.Command{
Name: "bugreport",
Exec: runBugReport,
ShortHelp: "Print a shareable identifier to help diagnose issues",
ShortUsage: "bugreport [note]",
ShortUsage: "tailscale bugreport [note]",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("bugreport")
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")

View File

@@ -28,7 +28,7 @@ var certCmd = &ffcli.Command{
Name: "cert",
Exec: runCert,
ShortHelp: "Get TLS certs",
ShortUsage: "cert [flags] <domain>",
ShortUsage: "tailscale cert [flags] <domain>",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("cert")
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")

View File

@@ -14,11 +14,12 @@ import (
"log"
"os"
"runtime"
"slices"
"strings"
"sync"
"text/tabwriter"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
@@ -93,6 +94,68 @@ func Run(args []string) (err error) {
})
})
rootCmd := newRootCmd()
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
if noexec := (ffcli.NoExecError{}); errors.As(err, &noexec) {
// When the user enters an unknown subcommand, ffcli tries to run
// the closest valid parent subcommand with everything else as args,
// returning NoExecError if it doesn't have an Exec function.
cmd := noexec.Command
args := cmd.FlagSet.Args()
if len(cmd.Subcommands) > 0 {
if len(args) > 0 {
return fmt.Errorf("%s: unknown subcommand: %s", fullCmd(rootCmd, cmd), args[0])
}
subs := make([]string, 0, len(cmd.Subcommands))
for _, sub := range cmd.Subcommands {
subs = append(subs, sub.Name)
}
return fmt.Errorf("%s: missing subcommand: %s", fullCmd(rootCmd, cmd), strings.Join(subs, ", "))
}
}
return err
}
if envknob.Bool("TS_DUMP_HELP") {
walkCommands(rootCmd, func(w cmdWalk) bool {
fmt.Println("===")
// UsageFuncs are typically called during Command.Run which ensures
// FlagSet is not nil.
c := w.Command
if c.FlagSet == nil {
c.FlagSet = flag.NewFlagSet(c.Name, flag.ContinueOnError)
}
if c.UsageFunc != nil {
fmt.Println(c.UsageFunc(c))
} else {
fmt.Println(ffcli.DefaultUsageFunc(c))
}
return true
})
return
}
localClient.Socket = rootArgs.socket
rootCmd.FlagSet.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
}
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
func newRootCmd() *ffcli.Command {
rootfs := newFlagSet("tailscale")
rootfs.StringVar(&rootArgs.socket, "socket", paths.DefaultTailscaledSocket(), "path to tailscaled socket")
@@ -129,13 +192,19 @@ change in the future.
certCmd,
netlockCmd,
licensesCmd,
exitNodeCmd,
exitNodeCmd(),
updateCmd,
whoisCmd,
debugCmd,
driveCmd,
},
FlagSet: rootfs,
Exec: func(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("tailscale: unknown subcommand: %s", args[0])
}
return flag.ErrHelp
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands,
@@ -143,45 +212,17 @@ change in the future.
)
}
// Don't advertise these commands, but they're still explicitly available.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "share"):
rootCmd.Subcommands = append(rootCmd.Subcommands, shareCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
for _, c := range rootCmd.Subcommands {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
}
localClient.Socket = rootArgs.socket
rootfs.Visit(func(f *flag.Flag) {
if f.Name == "socket" {
localClient.UseSocketOnly = true
walkCommands(rootCmd, func(w cmdWalk) bool {
if w.UsageFunc == nil {
w.UsageFunc = usageFunc
}
return true
})
err = rootCmd.Run(context.Background())
if tailscale.IsAccessDeniedError(err) && os.Getuid() != 0 && runtime.GOOS != "windows" {
return fmt.Errorf("%v\n\nUse 'sudo tailscale %s' or 'tailscale up --operator=$USER' to not require root.", err, strings.Join(args, " "))
}
if errors.Is(err, flag.ErrHelp) {
return nil
}
return err
return rootCmd
}
func fatalf(format string, a ...any) {
@@ -200,6 +241,59 @@ var rootArgs struct {
socket string
}
type cmdWalk struct {
*ffcli.Command
parents []*ffcli.Command
}
func (w cmdWalk) Path() string {
if len(w.parents) == 0 {
return w.Name
}
var sb strings.Builder
for _, p := range w.parents {
sb.WriteString(p.Name)
sb.WriteString(" ")
}
sb.WriteString(w.Name)
return sb.String()
}
// walkCommands calls f for root and all of its nested subcommands until f
// returns false or all have been visited.
func walkCommands(root *ffcli.Command, f func(w cmdWalk) (more bool)) {
var walk func(cmd *ffcli.Command, parents []*ffcli.Command, f func(cmdWalk) bool) bool
walk = func(cmd *ffcli.Command, parents []*ffcli.Command, f func(cmdWalk) bool) bool {
if !f(cmdWalk{cmd, parents}) {
return false
}
parents = append(parents, cmd)
for _, sub := range cmd.Subcommands {
if !walk(sub, parents, f) {
return false
}
}
return true
}
walk(root, nil, f)
}
// fullCmd returns the full "tailscale ... cmd" invocation for a subcommand.
func fullCmd(root, cmd *ffcli.Command) (full string) {
walkCommands(root, func(w cmdWalk) bool {
if w.Command == cmd {
full = w.Path()
return false
}
return true
})
if full == "" {
return cmd.Name
}
return full
}
// usageFuncNoDefaultValues is like usageFunc but doesn't print default values.
func usageFuncNoDefaultValues(c *ffcli.Command) string {
return usageFuncOpt(c, false)
@@ -211,23 +305,32 @@ func usageFunc(c *ffcli.Command) string {
func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
var b strings.Builder
const hiddenPrefix = "HIDDEN: "
if c.ShortHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.ShortHelp)
}
fmt.Fprintf(&b, "USAGE\n")
if c.ShortUsage != "" {
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
fmt.Fprintf(&b, " %s\n", strings.ReplaceAll(c.ShortUsage, "\n", "\n "))
} else {
fmt.Fprintf(&b, " %s\n", c.Name)
}
fmt.Fprintf(&b, "\n")
if c.LongHelp != "" {
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
help, _ := strings.CutPrefix(c.LongHelp, hiddenPrefix)
fmt.Fprintf(&b, "%s\n\n", help)
}
if len(c.Subcommands) > 0 {
fmt.Fprintf(&b, "SUBCOMMANDS\n")
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
for _, subcommand := range c.Subcommands {
if strings.HasPrefix(subcommand.LongHelp, hiddenPrefix) {
continue
}
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
}
tw.Flush()
@@ -240,7 +343,7 @@ func usageFuncOpt(c *ffcli.Command, withDefaults bool) string {
c.FlagSet.VisitAll(func(f *flag.Flag) {
var s string
name, usage := flag.UnquoteUsage(f)
if strings.HasPrefix(usage, "HIDDEN: ") {
if strings.HasPrefix(usage, hiddenPrefix) {
return
}
if isBoolFlag(f) {
@@ -287,3 +390,17 @@ func countFlags(fs *flag.FlagSet) (n int) {
fs.VisitAll(func(*flag.Flag) { n++ })
return n
}
// colorableOutput returns a colorable writer if stdout is a terminal (not, say,
// redirected to a file or pipe), the Stdout writer is os.Stdout (we're not
// embedding the CLI in wasm or a mobile app), and NO_COLOR is not set (see
// https://no-color.org/). If any of those is not the case, ok is false
// and w is Stdout.
func colorableOutput() (w io.Writer, ok bool) {
if Stdout != os.Stdout ||
os.Getenv("NO_COLOR") != "" ||
!isatty.IsTerminal(os.Stdout.Fd()) {
return Stdout, false
}
return colorable.NewColorableStdout(), true
}

View File

@@ -16,6 +16,7 @@ import (
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp"
"tailscale.com/envknob"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -28,6 +29,110 @@ import (
"tailscale.com/version/distro"
)
func TestPanicIfAnyEnvCheckedInInit(t *testing.T) {
envknob.PanicIfAnyEnvCheckedInInit()
}
func TestShortUsage(t *testing.T) {
t.Setenv("TAILSCALE_USE_WIP_CODE", "1")
if !envknob.UseWIPCode() {
t.Fatal("expected envknob.UseWIPCode() to be true")
}
walkCommands(newRootCmd(), func(w cmdWalk) bool {
c, parents := w.Command, w.parents
// Words that we expect to be in the usage.
words := make([]string, len(parents)+1)
for i, parent := range parents {
words[i] = parent.Name
}
words[len(parents)] = c.Name
// Check the ShortHelp starts with a capital letter.
if prefix, help := trimPrefixes(c.ShortHelp, "HIDDEN: ", "[ALPHA] ", "[BETA] "); help != "" {
if 'a' <= help[0] && help[0] <= 'z' {
if len(help) > 20 {
help = help[:20] + "…"
}
caphelp := string(help[0]-'a'+'A') + help[1:]
t.Errorf("command: %s: ShortHelp %q should start with a capital letter %q", strings.Join(words, " "), prefix+help, prefix+caphelp)
}
}
// Check all words appear in the usage.
usage := c.ShortUsage
for _, word := range words {
var ok bool
usage, ok = cutWord(usage, word)
if !ok {
full := strings.Join(words, " ")
t.Errorf("command: %s: usage %q should contain the full path %q", full, c.ShortUsage, full)
return true
}
}
return true
})
}
func trimPrefixes(full string, prefixes ...string) (trimmed, remaining string) {
s := full
start:
for _, p := range prefixes {
var ok bool
s, ok = strings.CutPrefix(s, p)
if ok {
goto start
}
}
return full[:len(full)-len(s)], s
}
// cutWord("tailscale debug scale 123", "scale") returns (" 123", true).
func cutWord(s, w string) (after string, ok bool) {
var p string
for {
p, s, ok = strings.Cut(s, w)
if !ok {
return "", false
}
if p != "" && isWordChar(p[len(p)-1]) {
continue
}
if s != "" && isWordChar(s[0]) {
continue
}
return s, true
}
}
func isWordChar(r byte) bool {
return r == '_' ||
('0' <= r && r <= '9') ||
('A' <= r && r <= 'Z') ||
('a' <= r && r <= 'z')
}
func TestCutWord(t *testing.T) {
tests := []struct {
in string
word string
out string
ok bool
}{
{"tailscale debug", "debug", "", true},
{"tailscale debug", "bug", "", false},
{"tailscale debug", "tail", "", false},
{"tailscale debug scaley scale 123", "scale", " 123", true},
}
for _, test := range tests {
out, ok := cutWord(test.in, test.word)
if out != test.out || ok != test.ok {
t.Errorf("cutWord(%q, %q) = (%q, %t), wanted (%q, %t)", test.in, test.word, out, ok, test.out, test.ok)
}
}
}
// geese is a collection of gooses. It need not be complete.
// But it should include anything handled specially (e.g. linux, windows)
// and at least one thing that's not (darwin, freebsd).
@@ -803,7 +908,7 @@ func TestPrefFlagMapping(t *testing.T) {
}
prefType := reflect.TypeFor[ipn.Prefs]()
for i := 0; i < prefType.NumField(); i++ {
for i := range prefType.NumField() {
prefName := prefType.Field(i).Name
if prefHasFlag[prefName] {
continue
@@ -829,6 +934,14 @@ func TestPrefFlagMapping(t *testing.T) {
// Handled by TS_DEBUG_FIREWALL_MODE env var, we don't want to have
// a CLI flag for this. The Pref is used by c2n.
continue
case "DriveShares":
// Handled by the tailscale share subcommand, we don't want a CLI
// flag for this.
continue
case "InternalExitNodePrior":
// Used internally by LocalBackend as part of exit node usage toggling.
// No CLI flag for this.
continue
}
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
}

View File

@@ -27,7 +27,7 @@ func init() {
var configureKubeconfigCmd = &ffcli.Command{
Name: "kubeconfig",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortUsage: "kubeconfig <hostname-or-fqdn>",
ShortUsage: "tailscale configure kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(`
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
@@ -43,7 +43,20 @@ See: https://tailscale.com/s/k8s-auth-proxy
}
// kubeconfigPath returns the path to the kubeconfig file for the current user.
func kubeconfigPath() string {
func kubeconfigPath() (string, error) {
if kubeconfig := os.Getenv("KUBECONFIG"); kubeconfig != "" {
if version.IsSandboxedMacOS() {
return "", errors.New("$KUBECONFIG is incompatible with the App Store version")
}
var out string
for _, out = range filepath.SplitList(kubeconfig) {
if info, err := os.Stat(out); !os.IsNotExist(err) && !info.IsDir() {
break
}
}
return out, nil
}
var dir string
if version.IsSandboxedMacOS() {
// The HOME environment variable in macOS sandboxed apps is set to
@@ -55,7 +68,7 @@ func kubeconfigPath() string {
} else {
dir = homedir.HomeDir()
}
return filepath.Join(dir, ".kube", "config")
return filepath.Join(dir, ".kube", "config"), nil
}
func runConfigureKubeconfig(ctx context.Context, args []string) error {
@@ -76,7 +89,11 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
return fmt.Errorf("no peer found with hostname %q", hostOrFQDN)
}
targetFQDN = strings.TrimSuffix(targetFQDN, ".")
if err := setKubeconfigForPeer(targetFQDN, kubeconfigPath()); err != nil {
var kubeconfig string
if kubeconfig, err = kubeconfigPath(); err != nil {
return err
}
if err = setKubeconfigForPeer(targetFQDN, kubeconfig); err != nil {
return err
}
printf("kubeconfig configured for %q\n", hostOrFQDN)
@@ -119,7 +136,7 @@ func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
var clusters []any
if cm, ok := cfg["clusters"]; ok {
clusters = cm.([]any)
clusters, _ = cm.([]any)
}
cfg["clusters"] = appendOrSetNamed(clusters, fqdn, map[string]any{
"name": fqdn,
@@ -130,7 +147,7 @@ func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
var users []any
if um, ok := cfg["users"]; ok {
users = um.([]any)
users, _ = um.([]any)
}
cfg["users"] = appendOrSetNamed(users, "tailscale-auth", map[string]any{
// We just need one of these, and can reuse it for all clusters.
@@ -144,7 +161,7 @@ func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
var contexts []any
if cm, ok := cfg["contexts"]; ok {
contexts = cm.([]any)
contexts, _ = cm.([]any)
}
cfg["contexts"] = appendOrSetNamed(contexts, fqdn, map[string]any{
"name": fqdn,

View File

@@ -48,6 +48,31 @@ contexts:
current-context: foo.tail-scale.ts.net
kind: Config
users:
- name: tailscale-auth
user:
token: unused`,
},
{
name: "all configs, clusters, users have been deleted",
in: `apiVersion: v1
clusters: null
contexts: null
kind: Config
current-context: some-non-existent-cluster
users: null`,
want: `apiVersion: v1
clusters:
- cluster:
server: https://foo.tail-scale.ts.net
name: foo.tail-scale.ts.net
contexts:
- context:
cluster: foo.tail-scale.ts.net
user: tailscale-auth
name: foo.tail-scale.ts.net
current-context: foo.tail-scale.ts.net
kind: Config
users:
- name: tailscale-auth
user:
token: unused`,

View File

@@ -0,0 +1,220 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path"
"runtime"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/version/distro"
)
var synologyConfigureCertCmd = &ffcli.Command{
Name: "synology-cert",
Exec: runConfigureSynologyCert,
ShortHelp: "Configure Synology with a TLS certificate for your tailnet",
ShortUsage: "synology-cert [--domain <domain>]",
LongHelp: strings.TrimSpace(`
This command is intended to run periodically as root on a Synology device to
create or refresh the TLS certificate for the tailnet domain.
See: https://tailscale.com/kb/1153/enabling-https
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("synology-cert")
fs.StringVar(&synologyConfigureCertArgs.domain, "domain", "", "Tailnet domain to create or refresh certificates for. Ignored if only one domain exists.")
return fs
})(),
}
var synologyConfigureCertArgs struct {
domain string
}
func runConfigureSynologyCert(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
return errors.New("only implemented on Synology")
}
if uid := os.Getuid(); uid != 0 {
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
}
hi := hostinfo.New()
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
if !isDSM6 && !isDSM7 {
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
}
domain := synologyConfigureCertArgs.domain
if st, err := localClient.Status(ctx); err == nil {
if st.BackendState != ipn.Running.String() {
return fmt.Errorf("Tailscale is not running.")
} else if len(st.CertDomains) == 0 {
return fmt.Errorf("TLS certificate support is not enabled/configured for your tailnet.")
} else if len(st.CertDomains) == 1 {
if domain != "" && domain != st.CertDomains[0] {
log.Printf("Ignoring supplied domain %q, TLS certificate will be created for %q.\n", domain, st.CertDomains[0])
}
domain = st.CertDomains[0]
} else {
var found bool
for _, d := range st.CertDomains {
if d == domain {
found = true
break
}
}
if !found {
return fmt.Errorf("Domain %q was not one of the valid domain options: %q.", domain, st.CertDomains)
}
}
}
// Check for an existing certificate, and replace it if it already exists
var id string
certs, err := listCerts(ctx, synowebapiCommand{})
if err != nil {
return err
}
for _, c := range certs {
if c.Subject.CommonName == domain {
id = c.ID
break
}
}
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
if err != nil {
return err
}
// Certs have to be written to file for the upload command to work.
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return fmt.Errorf("can't create temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
keyFile := path.Join(tmpDir, "key.pem")
os.WriteFile(keyFile, keyPEM, 0600)
certFile := path.Join(tmpDir, "cert.pem")
os.WriteFile(certFile, certPEM, 0600)
if err := uploadCert(ctx, synowebapiCommand{}, certFile, keyFile, id); err != nil {
return err
}
return nil
}
type subject struct {
CommonName string `json:"common_name"`
}
type certificateInfo struct {
ID string `json:"id"`
Desc string `json:"desc"`
Subject subject `json:"subject"`
}
// listCerts fetches a list of the certificates that DSM knows about
func listCerts(ctx context.Context, c synoAPICaller) ([]certificateInfo, error) {
rawData, err := c.Call(ctx, "SYNO.Core.Certificate.CRT", "list", nil)
if err != nil {
return nil, err
}
var payload struct {
Certificates []certificateInfo `json:"certificates"`
}
if err := json.Unmarshal(rawData, &payload); err != nil {
return nil, fmt.Errorf("decoding certificate list response payload: %w", err)
}
return payload.Certificates, nil
}
// uploadCert creates or replaces a certificate. If id is given, it will attempt to replace the certificate with that ID.
func uploadCert(ctx context.Context, c synoAPICaller, certFile, keyFile string, id string) error {
params := map[string]string{
"key_tmp": keyFile,
"cert_tmp": certFile,
"desc": "Tailnet Certificate",
}
if id != "" {
params["id"] = id
}
rawData, err := c.Call(ctx, "SYNO.Core.Certificate", "import", params)
if err != nil {
return err
}
var payload struct {
NewID string `json:"id"`
}
if err := json.Unmarshal(rawData, &payload); err != nil {
return fmt.Errorf("decoding certificate upload response payload: %w", err)
}
log.Printf("Tailnet Certificate uploaded with ID %q.", payload.NewID)
return nil
}
type synoAPICaller interface {
Call(context.Context, string, string, map[string]string) (json.RawMessage, error)
}
type apiResponse struct {
Success bool `json:"success"`
Error *apiError `json:"error,omitempty"`
Data json.RawMessage `json:"data"`
}
type apiError struct {
Code int64 `json:"code"`
Errors string `json:"errors"`
}
// synowebapiCommand implements synoAPICaller using the /usr/syno/bin/synowebapi binary. Must be run as root.
type synowebapiCommand struct{}
func (s synowebapiCommand) Call(ctx context.Context, api, method string, params map[string]string) (json.RawMessage, error) {
args := []string{"--exec", fmt.Sprintf("api=%s", api), fmt.Sprintf("method=%s", method)}
for k, v := range params {
args = append(args, fmt.Sprintf("%s=%q", k, v))
}
out, err := exec.CommandContext(ctx, "/usr/syno/bin/synowebapi", args...).Output()
if err != nil {
return nil, fmt.Errorf("calling %q method of %q API: %v, %s", method, api, err, out)
}
var payload apiResponse
if err := json.Unmarshal(out, &payload); err != nil {
return nil, fmt.Errorf("decoding response json from %q method of %q API: %w", method, api, err)
}
if payload.Error != nil {
return nil, fmt.Errorf("error response from %q method of %q API: %v", method, api, payload.Error)
}
return payload.Data, nil
}

View File

@@ -0,0 +1,140 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"encoding/json"
"fmt"
"reflect"
"testing"
)
type fakeAPICaller struct {
Data json.RawMessage
Error error
}
func (c fakeAPICaller) Call(_ context.Context, _, _ string, _ map[string]string) (json.RawMessage, error) {
return c.Data, c.Error
}
func Test_listCerts(t *testing.T) {
tests := []struct {
name string
caller synoAPICaller
want []certificateInfo
wantErr bool
}{
{
name: "normal response",
caller: fakeAPICaller{
Data: json.RawMessage(`{
"certificates" : [
{
"desc" : "Tailnet Certificate",
"id" : "cG2XBt",
"is_broken" : false,
"is_default" : false,
"issuer" : {
"common_name" : "R3",
"country" : "US",
"organization" : "Let's Encrypt"
},
"key_types" : "ECC",
"renewable" : false,
"services" : [
{
"display_name" : "DSM Desktop Service",
"display_name_i18n" : "common:web_desktop",
"isPkg" : false,
"multiple_cert" : true,
"owner" : "root",
"service" : "default",
"subscriber" : "system",
"user_setable" : true
}
],
"signature_algorithm" : "sha256WithRSAEncryption",
"subject" : {
"common_name" : "foo.tailscale.ts.net",
"sub_alt_name" : [ "foo.tailscale.ts.net" ]
},
"user_deletable" : true,
"valid_from" : "Sep 26 11:39:43 2023 GMT",
"valid_till" : "Dec 25 11:39:42 2023 GMT"
},
{
"desc" : "",
"id" : "sgmnpb",
"is_broken" : false,
"is_default" : false,
"issuer" : {
"city" : "Taipei",
"common_name" : "Synology Inc. CA",
"country" : "TW",
"organization" : "Synology Inc."
},
"key_types" : "",
"renewable" : false,
"self_signed_cacrt_info" : {
"issuer" : {
"city" : "Taipei",
"common_name" : "Synology Inc. CA",
"country" : "TW",
"organization" : "Synology Inc."
},
"subject" : {
"city" : "Taipei",
"common_name" : "Synology Inc. CA",
"country" : "TW",
"organization" : "Synology Inc."
}
},
"services" : [],
"signature_algorithm" : "sha256WithRSAEncryption",
"subject" : {
"city" : "Taipei",
"common_name" : "synology.com",
"country" : "TW",
"organization" : "Synology Inc.",
"sub_alt_name" : []
},
"user_deletable" : true,
"valid_from" : "May 27 00:23:19 2019 GMT",
"valid_till" : "Feb 11 00:23:19 2039 GMT"
}
]
}`),
Error: nil,
},
want: []certificateInfo{
{Desc: "Tailnet Certificate", ID: "cG2XBt", Subject: subject{CommonName: "foo.tailscale.ts.net"}},
{Desc: "", ID: "sgmnpb", Subject: subject{CommonName: "synology.com"}},
},
},
{
name: "call error",
caller: fakeAPICaller{nil, fmt.Errorf("caller failed")},
wantErr: true,
},
{
name: "payload decode error",
caller: fakeAPICaller{json.RawMessage("This isn't JSON!"), nil},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := listCerts(context.Background(), tt.caller)
if (err != nil) != tt.wantErr {
t.Errorf("listCerts() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("listCerts() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -22,10 +22,11 @@ import (
// used to configure Synology devices, but is now a compatibility alias to
// "tailscale configure synology".
var configureHostCmd = &ffcli.Command{
Name: "configure-host",
Exec: runConfigureSynology,
ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: synologyConfigureCmd.LongHelp,
Name: "configure-host",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure-host",
ShortHelp: synologyConfigureCmd.ShortHelp,
LongHelp: synologyConfigureCmd.LongHelp,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure-host")
return fs
@@ -33,9 +34,10 @@ var configureHostCmd = &ffcli.Command{
}
var synologyConfigureCmd = &ffcli.Command{
Name: "synology",
Exec: runConfigureSynology,
ShortHelp: "Configure Synology to enable outbound connections",
Name: "synology",
Exec: runConfigureSynology,
ShortUsage: "tailscale configure synology",
ShortHelp: "Configure Synology to enable outbound connections",
LongHelp: strings.TrimSpace(`
This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission

View File

@@ -4,7 +4,6 @@
package cli
import (
"context"
"flag"
"runtime"
"strings"
@@ -14,8 +13,9 @@ import (
)
var configureCmd = &ffcli.Command{
Name: "configure",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
Name: "configure",
ShortUsage: "tailscale configure <subcommand>",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways.
@@ -25,14 +25,12 @@ services on the host to use Tailscale in more ways.
return fs
})(),
Subcommands: configureSubcommands(),
Exec: func(ctx context.Context, args []string) error {
return flag.ErrHelp
},
}
func configureSubcommands() (out []*ffcli.Command) {
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
out = append(out, synologyConfigureCmd)
out = append(out, synologyConfigureCertCmd)
}
return out
}

View File

@@ -45,9 +45,10 @@ import (
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
LongHelp: `"tailscale debug" contains misc debug facilities; it is not a stable interface.`,
Name: "debug",
Exec: runDebug,
ShortUsage: "tailscale debug <debug-flags | subcommand>",
LongHelp: `HIDDEN: "tailscale debug" contains misc debug facilities; it is not a stable interface.`,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("debug")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
@@ -58,15 +59,16 @@ var debugCmd = &ffcli.Command{
})(),
Subcommands: []*ffcli.Command{
{
Name: "derp-map",
Exec: runDERPMap,
ShortHelp: "print DERP map",
Name: "derp-map",
ShortUsage: "tailscale debug derp-map",
Exec: runDERPMap,
ShortHelp: "Print DERP map",
},
{
Name: "component-logs",
Exec: runDebugComponentLogs,
ShortHelp: "enable/disable debug logs for a component",
ShortUsage: "tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]",
Exec: runDebugComponentLogs,
ShortHelp: "Enable/disable debug logs for a component",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("component-logs")
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
@@ -74,14 +76,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "print tailscaled's goroutines",
Name: "daemon-goroutines",
ShortUsage: "tailscale debug daemon-goroutines",
Exec: runDaemonGoroutines,
ShortHelp: "Print tailscaled's goroutines",
},
{
Name: "daemon-logs",
Exec: runDaemonLogs,
ShortHelp: "watch tailscaled's server logs",
Name: "daemon-logs",
ShortUsage: "tailscale debug daemon-logs",
Exec: runDaemonLogs,
ShortHelp: "Watch tailscaled's server logs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("daemon-logs")
fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level")
@@ -90,9 +94,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "metrics",
Exec: runDaemonMetrics,
ShortHelp: "print tailscaled's metrics",
Name: "metrics",
ShortUsage: "tailscale debug metrics",
Exec: runDaemonMetrics,
ShortHelp: "Print tailscaled's metrics",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("metrics")
fs.BoolVar(&metricsArgs.watch, "watch", false, "print JSON dump of delta values")
@@ -100,80 +105,95 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "env",
Exec: runEnv,
ShortHelp: "print cmd/tailscale environment",
Name: "env",
ShortUsage: "tailscale debug env",
Exec: runEnv,
ShortHelp: "Print cmd/tailscale environment",
},
{
Name: "stat",
Exec: runStat,
ShortHelp: "stat a file",
Name: "stat",
ShortUsage: "tailscale debug stat <files...>",
Exec: runStat,
ShortHelp: "Stat a file",
},
{
Name: "hostinfo",
Exec: runHostinfo,
ShortHelp: "print hostinfo",
Name: "hostinfo",
ShortUsage: "tailscale debug hostinfo",
Exec: runHostinfo,
ShortHelp: "Print hostinfo",
},
{
Name: "local-creds",
Exec: runLocalCreds,
ShortHelp: "print how to access Tailscale LocalAPI",
Name: "local-creds",
ShortUsage: "tailscale debug local-creds",
Exec: runLocalCreds,
ShortHelp: "Print how to access Tailscale LocalAPI",
},
{
Name: "restun",
Exec: localAPIAction("restun"),
ShortHelp: "force a magicsock restun",
Name: "restun",
ShortUsage: "tailscale debug restun",
Exec: localAPIAction("restun"),
ShortHelp: "Force a magicsock restun",
},
{
Name: "rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "force a magicsock rebind",
Name: "rebind",
ShortUsage: "tailscale debug rebind",
Exec: localAPIAction("rebind"),
ShortHelp: "Force a magicsock rebind",
},
{
Name: "derp-set-on-demand",
Exec: localAPIAction("derp-set-homeless"),
ShortHelp: "enable DERP on-demand mode (breaks reachability)",
Name: "derp-set-on-demand",
ShortUsage: "tailscale debug derp-set-on-demand",
Exec: localAPIAction("derp-set-homeless"),
ShortHelp: "Enable DERP on-demand mode (breaks reachability)",
},
{
Name: "derp-unset-on-demand",
Exec: localAPIAction("derp-unset-homeless"),
ShortHelp: "disable DERP on-demand mode",
Name: "derp-unset-on-demand",
ShortUsage: "tailscale debug derp-unset-on-demand",
Exec: localAPIAction("derp-unset-homeless"),
ShortHelp: "Disable DERP on-demand mode",
},
{
Name: "break-tcp-conns",
Exec: localAPIAction("break-tcp-conns"),
ShortHelp: "break any open TCP connections from the daemon",
Name: "break-tcp-conns",
ShortUsage: "tailscale debug break-tcp-conns",
Exec: localAPIAction("break-tcp-conns"),
ShortHelp: "Break any open TCP connections from the daemon",
},
{
Name: "break-derp-conns",
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
Name: "break-derp-conns",
ShortUsage: "tailscale debug break-derp-conns",
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "Break any open DERP connections from the daemon",
},
{
Name: "pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "switch to some other random DERP home region for a short time",
Name: "pick-new-derp",
ShortUsage: "tailscale debug pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "Switch to some other random DERP home region for a short time",
},
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "force a full no-op netmap update (for load testing)",
Name: "force-netmap-update",
ShortUsage: "tailscale debug force-netmap-update",
Exec: localAPIAction("force-netmap-update"),
ShortHelp: "Force a full no-op netmap update (for load testing)",
},
{
// TODO(bradfitz,maisem): eventually promote this out of debug
Name: "reload-config",
Exec: reloadConfig,
ShortHelp: "reload config",
Name: "reload-config",
ShortUsage: "tailscale debug reload-config",
Exec: reloadConfig,
ShortHelp: "Reload config",
},
{
Name: "control-knobs",
Exec: debugControlKnobs,
ShortHelp: "see current control knobs",
Name: "control-knobs",
ShortUsage: "tailscale debug control-knobs",
Exec: debugControlKnobs,
ShortHelp: "See current control knobs",
},
{
Name: "prefs",
Exec: runPrefs,
ShortHelp: "print prefs",
Name: "prefs",
ShortUsage: "tailscale debug prefs",
Exec: runPrefs,
ShortHelp: "Print prefs",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("prefs")
fs.BoolVar(&prefsArgs.pretty, "pretty", false, "If true, pretty-print output")
@@ -181,9 +201,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "watch-ipn",
Exec: runWatchIPN,
ShortHelp: "subscribe to IPN message bus",
Name: "watch-ipn",
ShortUsage: "tailscale debug watch-ipn",
Exec: runWatchIPN,
ShortHelp: "Subscribe to IPN message bus",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("watch-ipn")
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
@@ -194,9 +215,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "netmap",
Exec: runNetmap,
ShortHelp: "print the current network map",
Name: "netmap",
ShortUsage: "tailscale debug netmap",
Exec: runNetmap,
ShortHelp: "Print the current network map",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("netmap")
fs.BoolVar(&netmapArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
@@ -204,14 +226,17 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "via",
Name: "via",
ShortUsage: "tailscale debug via <site-id> <v4-cidr>\n" +
"tailscale debug via <v6-route>",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
ShortHelp: "Convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
{
Name: "ts2021",
Exec: runTS2021,
ShortHelp: "debug ts2021 protocol connectivity",
Name: "ts2021",
ShortUsage: "tailscale debug ts2021",
Exec: runTS2021,
ShortHelp: "Debug ts2021 protocol connectivity",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ts2021")
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
@@ -221,9 +246,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "set-expire",
Exec: runSetExpire,
ShortHelp: "manipulate node key expiry for testing",
Name: "set-expire",
ShortUsage: "tailscale debug set-expire --in=1m",
Exec: runSetExpire,
ShortHelp: "Manipulate node key expiry for testing",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("set-expire")
fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now")
@@ -231,9 +257,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "dev-store-set",
Exec: runDevStoreSet,
ShortHelp: "set a key/value pair during development",
Name: "dev-store-set",
ShortUsage: "tailscale debug dev-store-set",
Exec: runDevStoreSet,
ShortHelp: "Set a key/value pair during development",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("store-set")
fs.BoolVar(&devStoreSetArgs.danger, "danger", false, "accept danger")
@@ -241,14 +268,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "derp",
Exec: runDebugDERP,
ShortHelp: "test a DERP configuration",
Name: "derp",
ShortUsage: "tailscale debug derp",
Exec: runDebugDERP,
ShortHelp: "Test a DERP configuration",
},
{
Name: "capture",
Exec: runCapture,
ShortHelp: "streams pcaps for debugging",
Name: "capture",
ShortUsage: "tailscale debug capture",
Exec: runCapture,
ShortHelp: "Streams pcaps for debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("capture")
fs.StringVar(&captureArgs.outFile, "o", "", "path to stream the pcap (or - for stdout), leave empty to start wireshark")
@@ -256,9 +285,10 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "portmap",
Exec: debugPortmap,
ShortHelp: "run portmap debugging",
Name: "portmap",
ShortUsage: "tailscale debug portmap",
Exec: debugPortmap,
ShortHelp: "Run portmap debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
@@ -270,14 +300,16 @@ var debugCmd = &ffcli.Command{
})(),
},
{
Name: "peer-endpoint-changes",
Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes",
Name: "peer-endpoint-changes",
ShortUsage: "tailscale debug peer-endpoint-changes <hostname-or-IP>",
Exec: runPeerEndpointChanges,
ShortHelp: "Prints debug information about a peer's endpoint changes",
},
{
Name: "dial-types",
Exec: runDebugDialTypes,
ShortHelp: "prints debug information about connecting to a given host or IP",
Name: "dial-types",
ShortUsage: "tailscale debug dial-types <hostname-or-IP> <port>",
Exec: runDebugDialTypes,
ShortHelp: "Prints debug information about connecting to a given host or IP",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("dial-types")
fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`)
@@ -314,7 +346,7 @@ func outName(dst string) string {
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
return fmt.Errorf("tailscale debug: unknown subcommand: %s", args[0])
}
var usedFlag bool
if out := debugArgs.cpuFile; out != "" {
@@ -369,7 +401,7 @@ func runDebug(ctx context.Context, args []string) error {
// to subcommands.
return nil
}
return errors.New("see 'tailscale debug --help")
return errors.New("tailscale debug: subcommand or flag required")
}
func runLocalCreds(ctx context.Context, args []string) error {
@@ -453,7 +485,7 @@ func runWatchIPN(ctx context.Context, args []string) error {
return err
}
defer watcher.Close()
fmt.Fprintf(os.Stderr, "Connected.\n")
fmt.Fprintf(Stderr, "Connected.\n")
for seen := 0; watchIPNArgs.count == 0 || seen < watchIPNArgs.count; seen++ {
n, err := watcher.Next()
if err != nil {
@@ -563,7 +595,7 @@ func runStat(ctx context.Context, args []string) error {
func runHostinfo(ctx context.Context, args []string) error {
hi := hostinfo.New()
j, _ := json.MarshalIndent(hi, "", " ")
os.Stdout.Write(j)
Stdout.Write(j)
return nil
}
@@ -716,7 +748,7 @@ var ts2021Args struct {
}
func runTS2021(ctx context.Context, args []string) error {
log.SetOutput(os.Stdout)
log.SetOutput(Stdout)
log.SetFlags(log.Ltime | log.Lmicroseconds)
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
@@ -810,7 +842,7 @@ var debugComponentLogsArgs struct {
func runDebugComponentLogs(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]")
return errors.New("usage: tailscale debug component-logs [" + strings.Join(ipn.DebuggableComponents, "|") + "]")
}
component := args[0]
dur := debugComponentLogsArgs.forDur
@@ -833,7 +865,7 @@ var devStoreSetArgs struct {
func runDevStoreSet(ctx context.Context, args []string) error {
if len(args) != 2 {
return errors.New("usage: dev-store-set --danger <key> <value>")
return errors.New("usage: tailscale debug dev-store-set --danger <key> <value>")
}
if !devStoreSetArgs.danger {
return errors.New("this command is dangerous; use --danger to proceed")
@@ -851,7 +883,7 @@ func runDevStoreSet(ctx context.Context, args []string) error {
func runDebugDERP(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: debug derp <region>")
return errors.New("usage: tailscale debug derp <region>")
}
st, err := localClient.DebugDERPRegion(ctx, args[0])
if err != nil {
@@ -867,7 +899,7 @@ var setExpireArgs struct {
func runSetExpire(ctx context.Context, args []string) error {
if len(args) != 0 || setExpireArgs.in == 0 {
return errors.New("usage --in=<duration>")
return errors.New("usage: tailscale debug set-expire --in=<duration>")
}
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
}
@@ -885,7 +917,7 @@ func runCapture(ctx context.Context, args []string) error {
switch captureArgs.outFile {
case "-":
fmt.Fprintln(os.Stderr, "Press Ctrl-C to stop the capture.")
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(os.Stdout, stream)
return err
case "":
@@ -911,7 +943,7 @@ func runCapture(ctx context.Context, args []string) error {
return err
}
defer f.Close()
fmt.Fprintln(os.Stderr, "Press Ctrl-C to stop the capture.")
fmt.Fprintln(Stderr, "Press Ctrl-C to stop the capture.")
_, err = io.Copy(f, stream)
return err
}
@@ -966,7 +998,7 @@ func runPeerEndpointChanges(ctx context.Context, args []string) error {
}
if len(args) != 1 || args[0] == "" {
return errors.New("usage: peer-status <hostname-or-IP>")
return errors.New("usage: tailscale debug peer-endpoint-changes <hostname-or-IP>")
}
var ip string
@@ -1042,7 +1074,7 @@ func runDebugDialTypes(ctx context.Context, args []string) error {
}
if len(args) != 2 || args[0] == "" || args[1] == "" {
return errors.New("usage: dial-types <hostname-or-IP> <port>")
return errors.New("usage: tailscale debug dial-types <hostname-or-IP> <port>")
}
port, err := strconv.ParseUint(args[1], 10, 16)

View File

@@ -14,7 +14,7 @@ import (
var downCmd = &ffcli.Command{
Name: "down",
ShortUsage: "down",
ShortUsage: "tailscale down",
ShortHelp: "Disconnect from Tailscale",
Exec: runDown,

View File

@@ -5,106 +5,116 @@ package cli
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/tailfs"
"tailscale.com/drive"
)
const (
shareAddUsage = "share add <name> <path>"
shareRemoveUsage = "share remove <name>"
shareListUsage = "share list"
driveShareUsage = "tailscale drive share <name> <path>"
driveRenameUsage = "tailscale drive rename <oldname> <newname>"
driveUnshareUsage = "tailscale drive unshare <name>"
driveListUsage = "tailscale drive list"
)
var shareCmd = &ffcli.Command{
Name: "share",
var driveCmd = &ffcli.Command{
Name: "drive",
ShortHelp: "Share a directory with your tailnet",
ShortUsage: strings.Join([]string{
shareAddUsage,
shareRemoveUsage,
shareListUsage,
}, "\n "),
driveShareUsage,
driveRenameUsage,
driveUnshareUsage,
driveListUsage,
}, "\n"),
LongHelp: buildShareLongHelp(),
UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{
{
Name: "add",
Exec: runShareAdd,
ShortHelp: "[ALPHA] add a share",
UsageFunc: usageFunc,
Name: "share",
ShortUsage: driveShareUsage,
Exec: runDriveShare,
ShortHelp: "[ALPHA] Create or modify a share",
},
{
Name: "remove",
ShortHelp: "[ALPHA] remove a share",
Exec: runShareRemove,
UsageFunc: usageFunc,
Name: "rename",
ShortUsage: driveRenameUsage,
ShortHelp: "[ALPHA] Rename a share",
Exec: runDriveRename,
},
{
Name: "list",
ShortHelp: "[ALPHA] list current shares",
Exec: runShareList,
UsageFunc: usageFunc,
Name: "unshare",
ShortUsage: driveUnshareUsage,
ShortHelp: "[ALPHA] Remove a share",
Exec: runDriveUnshare,
},
{
Name: "list",
ShortUsage: driveListUsage,
ShortHelp: "[ALPHA] List current shares",
Exec: runDriveList,
},
},
Exec: func(context.Context, []string) error {
return errors.New("share subcommand required; run 'tailscale share -h' for details")
},
}
// runShareAdd is the entry point for the "tailscale share add" command.
func runShareAdd(ctx context.Context, args []string) error {
// runDriveShare is the entry point for the "tailscale drive share" command.
func runDriveShare(ctx context.Context, args []string) error {
if len(args) != 2 {
return fmt.Errorf("usage: tailscale %v", shareAddUsage)
return fmt.Errorf("usage: %s", driveShareUsage)
}
name, path := args[0], args[1]
err := localClient.TailFSShareAdd(ctx, &tailfs.Share{
err := localClient.DriveShareSet(ctx, &drive.Share{
Name: name,
Path: path,
})
if err == nil {
fmt.Printf("Added share %q at %q\n", name, path)
fmt.Printf("Sharing %q as %q\n", path, name)
}
return err
}
// runShareRemove is the entry point for the "tailscale share remove" command.
func runShareRemove(ctx context.Context, args []string) error {
// runDriveUnshare is the entry point for the "tailscale drive unshare" command.
func runDriveUnshare(ctx context.Context, args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: tailscale %v", shareRemoveUsage)
return fmt.Errorf("usage: %s", driveUnshareUsage)
}
name := args[0]
err := localClient.TailFSShareRemove(ctx, name)
err := localClient.DriveShareRemove(ctx, name)
if err == nil {
fmt.Printf("Removed share %q\n", name)
fmt.Printf("No longer sharing %q\n", name)
}
return err
}
// runShareList is the entry point for the "tailscale share list" command.
func runShareList(ctx context.Context, args []string) error {
// runDriveRename is the entry point for the "tailscale drive rename" command.
func runDriveRename(ctx context.Context, args []string) error {
if len(args) != 2 {
return fmt.Errorf("usage: %s", driveRenameUsage)
}
oldName := args[0]
newName := args[1]
err := localClient.DriveShareRename(ctx, oldName, newName)
if err == nil {
fmt.Printf("Renamed share %q to %q\n", oldName, newName)
}
return err
}
// runDriveList is the entry point for the "tailscale drive list" command.
func runDriveList(ctx context.Context, args []string) error {
if len(args) != 0 {
return fmt.Errorf("usage: tailscale %v", shareListUsage)
return fmt.Errorf("usage: %s", driveListUsage)
}
sharesMap, err := localClient.TailFSShareList(ctx)
shares, err := localClient.DriveShareList(ctx)
if err != nil {
return err
}
shares := make([]*tailfs.Share, 0, len(sharesMap))
for _, share := range sharesMap {
shares = append(shares, share)
}
sort.Slice(shares, func(i, j int) bool {
return shares[i].Name < shares[j].Name
})
longestName := 4 // "name"
longestPath := 4 // "path"
@@ -132,17 +142,17 @@ func runShareList(ctx context.Context, args []string) error {
func buildShareLongHelp() string {
longHelpAs := ""
if tailfs.AllowShareAs() {
if drive.AllowShareAs() {
longHelpAs = shareLongHelpAs
}
return fmt.Sprintf(shareLongHelpBase, longHelpAs)
}
var shareLongHelpBase = `Tailscale share allows you to share directories with other machines on your tailnet.
var shareLongHelpBase = `Taildrive allows you to share directories with other machines on your tailnet.
In order to share folders, your node needs to have the node attribute "tailfs:share".
In order to share folders, your node needs to have the node attribute "drive:share".
In order to access shares, your node needs to have the node attribute "tailfs:access".
In order to access shares, your node needs to have the node attribute "drive:access".
For example, to enable sharing and accessing shares for all member nodes:
@@ -150,14 +160,14 @@ For example, to enable sharing and accessing shares for all member nodes:
{
"target": ["autogroup:member"],
"attr": [
"tailfs:share",
"tailfs:access",
"drive:share",
"drive:access",
],
}]
Each share is identified by a name and points to a directory at a specific path. For example, to share the path /Users/me/Documents under the name "docs", you would run:
$ tailscale share add docs /Users/me/Documents
$ tailscale drive share docs /Users/me/Documents
Note that the system forces share names to lowercase to avoid problems with clients that don't support case-sensitive filenames.
@@ -178,7 +188,7 @@ Permissions to access shares are controlled via ACLs. For example, to give yours
"src": ["mylogin@domain.com"],
"dst": ["mylaptop's ip address"],
"app": {
"tailscale.com/cap/tailfs": [{
"tailscale.com/cap/drive": [{
"shares": ["docs"],
"access": "rw"
}]
@@ -188,7 +198,7 @@ Permissions to access shares are controlled via ACLs. For example, to give yours
"src": ["group:home"],
"dst": ["mylaptop"],
"app": {
"tailscale.com/cap/tailfs": [{
"tailscale.com/cap/drive": [{
"shares": ["docs"],
"access": "ro"
}]
@@ -202,7 +212,7 @@ To categorically give yourself access to all your shares, you can use the below
"src": ["autogroup:member"],
"dst": ["autogroup:self"],
"app": {
"tailscale.com/cap/tailfs": [{
"tailscale.com/cap/drive": [{
"shares": ["*"],
"access": "rw"
}]
@@ -211,16 +221,20 @@ To categorically give yourself access to all your shares, you can use the below
Whenever either you or anyone in the group "home" connects to the share, they connect as if they are using your local machine user. They'll be able to read the same files as your user and if they create files, those files will be owned by your user.%s
You can rename shares, for example you could rename the above share by running:
$ tailscale drive rename docs newdocs
You can remove shares by name, for example you could remove the above share by running:
$ tailscale share remove docs
$ tailscale drive unshare newdocs
You can get a list of currently published shares by running:
$ tailscale share list`
$ tailscale drive list`
var shareLongHelpAs = `
const shareLongHelpAs = `
If you want a share to be accessed as a different user, you can use sudo to accomplish this. For example, to create the aforementioned share as "theuser", you could run:
$ sudo -u theuser tailscale share add docs /Users/theuser/Documents`
$ sudo -u theuser tailscale drive share docs /Users/theuser/Documents`

View File

@@ -9,44 +9,85 @@ import (
"errors"
"flag"
"fmt"
"os"
"slices"
"strings"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
xmaps "golang.org/x/exp/maps"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
)
var exitNodeCmd = &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes",
LongHelp: "Show machines on your tailnet configured as exit nodes",
Subcommands: []*ffcli.Command{
{
Name: "list",
ShortUsage: "exit-node list [flags]",
ShortHelp: "Show exit nodes",
Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("list")
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
return fs
})(),
},
},
Exec: func(context.Context, []string) error {
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
},
func exitNodeCmd() *ffcli.Command {
return &ffcli.Command{
Name: "exit-node",
ShortUsage: "tailscale exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes",
Subcommands: append([]*ffcli.Command{
{
Name: "list",
ShortUsage: "tailscale exit-node list [flags]",
ShortHelp: "Show exit nodes",
Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("list")
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
return fs
})(),
},
{
Name: "suggest",
ShortUsage: "tailscale exit-node suggest",
ShortHelp: "Suggests the best available exit node",
Exec: runExitNodeSuggest,
}},
(func() []*ffcli.Command {
if !envknob.UseWIPCode() {
return nil
}
return []*ffcli.Command{
{
Name: "connect",
ShortUsage: "tailscale exit-node connect",
ShortHelp: "Connect to most recently used exit node",
Exec: exitNodeSetUse(true),
},
{
Name: "disconnect",
ShortUsage: "tailscale exit-node disconnect",
ShortHelp: "Disconnect from current exit node, if any",
Exec: exitNodeSetUse(false),
},
}
})()...),
}
}
var exitNodeArgs struct {
filter string
}
func exitNodeSetUse(wantOn bool) func(ctx context.Context, args []string) error {
return func(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected non-flag arguments")
}
err := localClient.SetUseExitNode(ctx, wantOn)
if err != nil {
if !wantOn {
pref, err := localClient.GetPrefs(ctx)
if err == nil && pref.ExitNodeID == "" {
// Two processes concurrently turned it off.
return nil
}
}
}
return err
}
}
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
// If the exit node has location and priority data, only the highest
// priority node for each city location is shown to the user.
@@ -70,7 +111,6 @@ func runExitNodeList(ctx context.Context, args []string) error {
// We only show exit nodes under the exit-node subcommand.
continue
}
peers = append(peers, ps)
}
@@ -84,24 +124,49 @@ func runExitNodeList(ctx context.Context, args []string) error {
return fmt.Errorf("no exit nodes found for %q", exitNodeArgs.filter)
}
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
w := tabwriter.NewWriter(Stdout, 10, 5, 5, ' ', 0)
defer w.Flush()
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", "IP", "HOSTNAME", "COUNTRY", "CITY", "STATUS")
for _, country := range filteredPeers.Countries {
for _, city := range country.Cities {
for _, peer := range city.Peers {
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer))
}
}
}
fmt.Fprintln(w)
fmt.Fprintln(w)
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP")
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP.")
if hasAnyExitNodeSuggestions(peers) {
fmt.Fprintln(w, "# To have Tailscale suggest an exit node, use `tailscale exit-node suggest`.")
}
return nil
}
// runExitNodeSuggest returns a suggested exit node ID to connect to and shows the chosen exit node tailcfg.StableNodeID.
// If there are no derp based exit nodes to choose from or there is a failure in finding a suggestion, the command will return an error indicating so.
func runExitNodeSuggest(ctx context.Context, args []string) error {
res, err := localClient.SuggestExitNode(ctx)
if err != nil {
return fmt.Errorf("suggest exit node: %w", err)
}
if res.ID == "" {
fmt.Println("No exit node suggestion is available.")
return nil
}
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", res.Name, res.ID)
return nil
}
func hasAnyExitNodeSuggestions(peers []*ipnstate.PeerStatus) bool {
for _, peer := range peers {
if peer.HasCap(tailcfg.NodeAttrSuggestExitNode) {
return true
}
}
return false
}
// peerStatus returns a string representing the current state of
// a peer. If there is no notable state, a - is returned.
func peerStatus(peer *ipnstate.PeerStatus) string {
@@ -137,46 +202,51 @@ type filteredCity struct {
const noLocationData = "-"
var noLocation = &tailcfg.Location{
Country: noLocationData,
CountryCode: noLocationData,
City: noLocationData,
CityCode: noLocationData,
}
// filterFormatAndSortExitNodes filters and sorts exit nodes into
// alphabetical order, by country, city and then by priority if
// present.
// If an exit node has location data, and the country has more than
// once city, an `Any` city is added to the country that contains the
// one city, an `Any` city is added to the country that contains the
// highest priority exit node within that country.
// For exit nodes without location data, their country fields are
// defined as '-' to indicate that the data is not available.
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
// first get peers into some fixed order, as code below doesn't break ties
// and our input comes from a random range-over-map.
slices.SortFunc(peers, func(a, b *ipnstate.PeerStatus) int {
return strings.Compare(a.DNSName, b.DNSName)
})
countries := make(map[string]*filteredCountry)
cities := make(map[string]*filteredCity)
for _, ps := range peers {
if ps.Location == nil {
ps.Location = &tailcfg.Location{
Country: noLocationData,
CountryCode: noLocationData,
City: noLocationData,
CityCode: noLocationData,
}
}
loc := cmp.Or(ps.Location, noLocation)
if filterBy != "" && ps.Location.Country != filterBy {
if filterBy != "" && loc.Country != filterBy {
continue
}
co, coOK := countries[ps.Location.CountryCode]
if !coOK {
co, ok := countries[loc.CountryCode]
if !ok {
co = &filteredCountry{
Name: ps.Location.Country,
Name: loc.Country,
}
countries[ps.Location.CountryCode] = co
countries[loc.CountryCode] = co
}
ci, ciOK := cities[ps.Location.CityCode]
if !ciOK {
ci, ok := cities[loc.CityCode]
if !ok {
ci = &filteredCity{
Name: ps.Location.City,
Name: loc.City,
}
cities[ps.Location.CityCode] = ci
cities[loc.CityCode] = ci
co.Cities = append(co.Cities, ci)
}
ci.Peers = append(ci.Peers, ps)
@@ -193,10 +263,10 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
continue
}
var countryANYPeer []*ipnstate.PeerStatus
var countryAnyPeer []*ipnstate.PeerStatus
for _, city := range country.Cities {
sortPeersByPriority(city.Peers)
countryANYPeer = append(countryANYPeer, city.Peers...)
countryAnyPeer = append(countryAnyPeer, city.Peers...)
var reducedCityPeers []*ipnstate.PeerStatus
for i, peer := range city.Peers {
if i == 0 || peer.ExitNode {
@@ -208,7 +278,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
city.Peers = reducedCityPeers
}
sortByCityName(country.Cities)
sortPeersByPriority(countryANYPeer)
sortPeersByPriority(countryAnyPeer)
if len(country.Cities) > 1 {
// For countries with more than one city, we want to return the
@@ -216,7 +286,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
country.Cities = append([]*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{countryANYPeer[0]},
Peers: []*ipnstate.PeerStatus{countryAnyPeer[0]},
},
}, country.Cities...)
}

View File

@@ -38,18 +38,12 @@ import (
var fileCmd = &ffcli.Command{
Name: "file",
ShortUsage: "file <cp|get> ...",
ShortUsage: "tailscale file <cp|get> ...",
ShortHelp: "Send or receive files",
Subcommands: []*ffcli.Command{
fileCpCmd,
fileGetCmd,
},
Exec: func(context.Context, []string) error {
// TODO(bradfitz): is there a better ffcli way to
// annotate subcommand-required commands that don't
// have an exec body of their own?
return errors.New("file subcommand required; run 'tailscale file -h' for details")
},
}
type countingReader struct {
@@ -65,7 +59,7 @@ func (c *countingReader) Read(buf []byte) (int, error) {
var fileCpCmd = &ffcli.Command{
Name: "cp",
ShortUsage: "file cp <files...> <target>:",
ShortUsage: "tailscale file cp <files...> <target>:",
ShortHelp: "Copy file(s) to a host",
Exec: runCp,
FlagSet: (func() *flag.FlagSet {
@@ -412,7 +406,7 @@ func (v *onConflict) Set(s string) error {
var fileGetCmd = &ffcli.Command{
Name: "get",
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortUsage: "tailscale file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
ShortHelp: "Move files out of the Tailscale file inbox",
Exec: runFileGet,
FlagSet: (func() *flag.FlagSet {
@@ -420,7 +414,7 @@ var fileGetCmd = &ffcli.Command{
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
fs.BoolVar(&getArgs.loop, "loop", false, "run get in a loop, receiving files as they come in")
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
fs.Var(&getArgs.conflict, "conflict", `behavior when a conflicting (same-named) file already exists in the target directory.
fs.Var(&getArgs.conflict, "conflict", "`behavior`"+` when a conflicting (same-named) file already exists in the target directory.
skip: skip conflicting files: leave them in the taildrop inbox and print an error. get any non-conflicting files
overwrite: overwrite existing file
rename: write to a new number-suffixed filename`)
@@ -560,7 +554,7 @@ func runFileGetOneBatch(ctx context.Context, dir string) []error {
func runFileGet(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: file get <target-directory>")
return errors.New("usage: tailscale file get <target-directory>")
}
log.SetFlags(0)

View File

@@ -8,14 +8,12 @@ import (
"flag"
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
)
var funnelCmd = func() *ffcli.Command {
@@ -38,9 +36,9 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{
"funnel <serve-port> {on|off}",
"funnel status [--json]",
}, "\n "),
"tailscale funnel <serve-port> {on|off}",
"tailscale funnel status [--json]",
}, "\n"),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
@@ -48,17 +46,16 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
Exec: e.runFunnel,
UsageFunc: usageFunc,
Exec: e.runFunnel,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
Name: "status",
Exec: e.runServeStatus,
ShortUsage: "tailscale funnel status [--json]",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
},
}
@@ -114,15 +111,8 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
sc.SetFunnel(dnsName, port, on)
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
@@ -177,10 +167,10 @@ func printFunnelWarning(sc *ipn.ServeConfig) {
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
warn = true
fmt.Fprintf(os.Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
fmt.Fprintf(Stderr, "\nWarning: funnel=on for %s, but no serve config\n", hp)
}
}
if warn {
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
fmt.Fprintf(Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
}
}

View File

@@ -12,14 +12,14 @@ import (
var idTokenCmd = &ffcli.Command{
Name: "id-token",
ShortUsage: "id-token <aud>",
ShortHelp: "fetch an OIDC id-token for the Tailscale machine",
ShortUsage: "tailscale id-token <aud>",
ShortHelp: "Fetch an OIDC id-token for the Tailscale machine",
Exec: runIDToken,
}
func runIDToken(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: id-token <aud>")
return errors.New("usage: tailscale id-token <aud>")
}
tr, err := localClient.IDToken(ctx, args[0])

View File

@@ -16,7 +16,7 @@ import (
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortUsage: "tailscale ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,

View File

@@ -12,7 +12,7 @@ import (
var licensesCmd = &ffcli.Command{
Name: "licenses",
ShortUsage: "licenses",
ShortUsage: "tailscale licenses",
ShortHelp: "Get open source license information",
LongHelp: "Get open source license information",
Exec: runLicenses,

View File

@@ -14,11 +14,10 @@ var loginArgs upArgsT
var loginCmd = &ffcli.Command{
Name: "login",
ShortUsage: "login [flags]",
ShortUsage: "tailscale login [flags]",
ShortHelp: "Log in to a Tailscale account",
LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
This command is currently in alpha and may change in the future.`,
UsageFunc: usageFunc,
FlagSet: func() *flag.FlagSet {
return newUpFlagSet(effectiveGOOS(), &loginArgs, "login")
}(),

View File

@@ -13,7 +13,7 @@ import (
var logoutCmd = &ffcli.Command{
Name: "logout",
ShortUsage: "logout [flags]",
ShortUsage: "tailscale logout",
ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(`

View File

@@ -16,7 +16,7 @@ import (
var ncCmd = &ffcli.Command{
Name: "nc",
ShortUsage: "nc <hostname-or-IP> <port>",
ShortUsage: "tailscale nc <hostname-or-IP> <port>",
ShortHelp: "Connect to a port on a host, connected to stdin/stdout",
Exec: runNC,
}
@@ -33,7 +33,7 @@ func runNC(ctx context.Context, args []string) error {
}
if len(args) != 2 {
return errors.New("usage: nc <hostname-or-IP> <port>")
return errors.New("usage: tailscale nc <hostname-or-IP> <port>")
}
hostOrIP, portStr := args[0], args[1]

View File

@@ -28,7 +28,7 @@ import (
var netcheckCmd = &ffcli.Command{
Name: "netcheck",
ShortUsage: "netcheck",
ShortUsage: "tailscale netcheck",
ShortHelp: "Print an analysis of local network conditions",
Exec: runNetcheck,
FlagSet: (func() *flag.FlagSet {

View File

@@ -17,8 +17,6 @@ import (
"strings"
"time"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
@@ -28,7 +26,7 @@ import (
var netlockCmd = &ffcli.Command{
Name: "lock",
ShortUsage: "lock <sub-command> <arguments>",
ShortUsage: "tailscale lock <subcommand> [arguments...]",
ShortHelp: "Manage tailnet lock",
LongHelp: "Manage tailnet lock",
Subcommands: []*ffcli.Command{
@@ -51,6 +49,9 @@ func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
if len(args) >= 2 && args[0] == "tskey-wrap" {
return runTskeyWrapCmd(ctx, args[1:])
}
if len(args) > 0 {
return fmt.Errorf("tailscale lock: unknown subcommand: %s", args[0])
}
return runNetworkLockStatus(ctx, args)
}
@@ -63,7 +64,7 @@ var nlInitArgs struct {
var nlInitCmd = &ffcli.Command{
Name: "init",
ShortUsage: "init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortUsage: "tailscale lock init [--gen-disablement-for-support] --gen-disablements N <trusted-key>...",
ShortHelp: "Initialize tailnet lock",
LongHelp: strings.TrimSpace(`
@@ -150,7 +151,7 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
}
fmt.Printf("%d disablement secrets have been generated and are printed below. Take note of them now, they WILL NOT be shown again.\n", nlInitArgs.numDisablements)
for i := 0; i < nlInitArgs.numDisablements; i++ {
for range nlInitArgs.numDisablements {
var secret [32]byte
if _, err := rand.Read(secret[:]); err != nil {
return err
@@ -185,7 +186,7 @@ var nlStatusArgs struct {
var nlStatusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status",
ShortUsage: "tailscale lock status",
ShortHelp: "Outputs the state of tailnet lock",
LongHelp: "Outputs the state of tailnet lock",
Exec: runNetworkLockStatus,
@@ -197,6 +198,10 @@ var nlStatusCmd = &ffcli.Command{
}
func runNetworkLockStatus(ctx context.Context, args []string) error {
if len(args) > 0 {
return fmt.Errorf("tailscale lock status: unexpected argument")
}
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
@@ -282,7 +287,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
var nlAddCmd = &ffcli.Command{
Name: "add",
ShortUsage: "add <public-key>...",
ShortUsage: "tailscale lock add <public-key>...",
ShortHelp: "Adds one or more trusted signing keys to tailnet lock",
LongHelp: "Adds one or more trusted signing keys to tailnet lock",
Exec: func(ctx context.Context, args []string) error {
@@ -296,7 +301,7 @@ var nlRemoveArgs struct {
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove [--re-sign=false] <public-key>...",
ShortUsage: "tailscale lock remove [--re-sign=false] <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: runNetworkLockRemove,
@@ -437,7 +442,7 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortUsage: "tailscale lock sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or
@@ -456,7 +461,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
)
if len(args) == 0 || len(args) > 2 {
return errors.New("usage: lock sign <node-key> [<rotation-key>]")
return errors.New("usage: tailscale lock sign <node-key> [<rotation-key>]")
}
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
return fmt.Errorf("decoding node-key: %w", err)
@@ -471,17 +476,17 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
// Provide a better help message for when someone clicks through the signing flow
// on the wrong device.
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
fmt.Fprintln(Stderr)
fmt.Fprintln(Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
fmt.Fprintln(Stderr)
}
return err
}
var nlDisableCmd = &ffcli.Command{
Name: "disable",
ShortUsage: "disable <disablement-secret>",
ShortUsage: "tailscale lock disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down tailnet lock for the tailnet",
LongHelp: strings.TrimSpace(`
@@ -503,14 +508,14 @@ func runNetworkLockDisable(ctx context.Context, args []string) error {
return err
}
if len(secrets) != 1 {
return errors.New("usage: lock disable <disablement-secret>")
return errors.New("usage: tailscale lock disable <disablement-secret>")
}
return localClient.NetworkLockDisable(ctx, secrets[0])
}
var nlLocalDisableCmd = &ffcli.Command{
Name: "local-disable",
ShortUsage: "local-disable",
ShortUsage: "tailscale lock local-disable",
ShortHelp: "Disables tailnet lock for this node only",
LongHelp: strings.TrimSpace(`
@@ -532,7 +537,7 @@ func runNetworkLockLocalDisable(ctx context.Context, args []string) error {
var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
ShortUsage: "tailscale lock disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret (advanced users only)",
LongHelp: "Computes a disablement value from a disablement secret (advanced users only)",
Exec: runNetworkLockDisablementKDF,
@@ -540,7 +545,7 @@ var nlDisablementKDFCmd = &ffcli.Command{
func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock disablement-kdf <hex-encoded-disablement-secret>")
return errors.New("usage: tailscale lock disablement-kdf <hex-encoded-disablement-secret>")
}
secret, err := hex.DecodeString(args[0])
if err != nil {
@@ -557,7 +562,7 @@ var nlLogArgs struct {
var nlLogCmd = &ffcli.Command{
Name: "log",
ShortUsage: "log [--limit N]",
ShortUsage: "tailscale lock log [--limit N]",
ShortHelp: "List changes applied to tailnet lock",
LongHelp: "List changes applied to tailnet lock",
Exec: runNetworkLockLog,
@@ -643,20 +648,19 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
return fixTailscaledConnectError(err)
}
if nlLogArgs.json {
enc := json.NewEncoder(os.Stdout)
enc := json.NewEncoder(Stdout)
enc.SetIndent("", " ")
return enc.Encode(updates)
}
useColor := isatty.IsTerminal(os.Stdout.Fd())
out, useColor := colorableOutput()
stdOut := colorable.NewColorableStdout()
for _, update := range updates {
stanza, err := nlDescribeUpdate(update, useColor)
if err != nil {
return err
}
fmt.Fprintln(stdOut, stanza)
fmt.Fprintln(out, stanza)
}
return nil
}
@@ -722,7 +726,7 @@ var nlRevokeKeysArgs struct {
var nlRevokeKeysCmd = &ffcli.Command{
Name: "revoke-keys",
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortUsage: "tailscale lock revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortHelp: "Revoke compromised tailnet-lock keys",
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).

View File

@@ -23,7 +23,7 @@ import (
var pingCmd = &ffcli.Command{
Name: "ping",
ShortUsage: "ping <hostname-or-IP>",
ShortUsage: "tailscale ping <hostname-or-IP>",
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
LongHelp: strings.TrimSpace(`
@@ -95,7 +95,7 @@ func runPing(ctx context.Context, args []string) error {
}
if len(args) != 1 || args[0] == "" {
return errors.New("usage: ping <hostname-or-IP>")
return errors.New("usage: tailscale ping <hostname-or-IP>")
}
var ip string

View File

@@ -27,7 +27,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -45,13 +44,13 @@ func newServeLegacyCommand(e *serveEnv) *ffcli.Command {
Name: "serve",
ShortHelp: "Serve content and local servers",
ShortUsage: strings.Join([]string{
"serve http:<port> <mount-point> <source> [off]",
"serve https:<port> <mount-point> <source> [off]",
"serve tcp:<port> tcp://localhost:<local-port> [off]",
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"serve status [--json]",
"serve reset",
}, "\n "),
"tailscale serve http:<port> <mount-point> <source> [off]",
"tailscale serve https:<port> <mount-point> <source> [off]",
"tailscale serve tcp:<port> tcp://localhost:<local-port> [off]",
"tailscale serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"tailscale serve status [--json]",
"tailscale serve reset",
}, "\n"),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
@@ -92,24 +91,21 @@ EXAMPLES
local plaintext server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
`),
Exec: e.runServe,
UsageFunc: usageFunc,
Exec: e.runServe,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
ShortHelp: "Show current serve/funnel status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
Exec: e.runServeReset,
ShortHelp: "reset current serve/funnel config",
ShortHelp: "Reset current serve/funnel config",
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
},
},
}
@@ -198,7 +194,7 @@ func (e *serveEnv) getLocalClientStatusWithoutPeers(ctx context.Context) (*ipnst
}
description, ok := isRunningOrStarting(st)
if !ok {
fmt.Fprintf(os.Stderr, "%s\n", description)
fmt.Fprintf(Stderr, "%s\n", description)
os.Exit(1)
}
if st.Self == nil {
@@ -252,7 +248,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
turnOff := "off" == args[len(args)-1]
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
fmt.Fprintf(Stderr, "error: invalid number of arguments\n\n")
return errHelp
}
@@ -291,8 +287,8 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
}
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
default:
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
fmt.Fprintf(Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
return errHelp
}
}
@@ -328,13 +324,13 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
}
if !filepath.IsAbs(source) {
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
fmt.Fprintf(Stderr, "error: path must be absolute\n\n")
return errHelp
}
source = filepath.Clean(source)
fi, err := os.Stat(source)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
fmt.Fprintf(Stderr, "error: invalid path: %v\n\n", err)
return errHelp
}
if fi.IsDir() && !strings.HasSuffix(mount, "/") {
@@ -357,35 +353,12 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
if err != nil {
return err
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if sc.IsTCPForwardingOnPort(srvPort) {
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
fmt.Fprintf(Stderr, "error: cannot serve web; already serving TCP\n")
return errHelp
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
}
mak.Set(&sc.Web[hp].Handlers, mount, h)
for k, v := range sc.Web[hp].Handlers {
if v == h {
continue
}
// If the new mount point ends in / and another mount point
// shares the same prefix, remove the other handler.
// (e.g. /foo/ overwrites /foo)
// The opposite example is also handled.
m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/")
if m1 == m2 {
delete(sc.Web[hp].Handlers, k)
continue
}
}
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS)
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
@@ -414,7 +387,7 @@ func isProxyTarget(source string) bool {
// allNumeric reports whether s only comprises of digits
// and has at least one digit.
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
for i := range len(s) {
if s[i] < '0' || s[i] > '9' {
return false
}
@@ -444,19 +417,7 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mou
if !sc.WebHandlerExists(hp, mount) {
return errors.New("error: handler does not exist")
}
// delete existing handler, then cascade delete if empty
delete(sc.Web[hp].Handlers, mount)
if len(sc.Web[hp].Handlers) == 0 {
delete(sc.Web, hp)
delete(sc.TCP, srvPort)
}
// clear empty maps mostly for testing
if len(sc.Web) == 0 {
sc.Web = nil
}
if len(sc.TCP) == 0 {
sc.TCP = nil
}
sc.RemoveWebHandler(dnsName, srvPort, []string{mount}, false)
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
@@ -548,18 +509,18 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
case "tls-terminated-tcp":
terminateTLS = true
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
fmt.Fprintf(Stderr, "error: invalid TCP source %q\n\n", dest)
return errHelp
}
dstURL, err := url.Parse(dest)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
fmt.Fprintf(Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return errHelp
}
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
fmt.Fprintf(Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return errHelp
}
@@ -567,13 +528,13 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
case "localhost", "127.0.0.1":
// ok
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
fmt.Fprintf(Stderr, "error: invalid TCP source %q\n", dest)
fmt.Fprint(Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
return errHelp
}
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
fmt.Fprintf(Stderr, "error: invalid port %q\n\n", dstPortStr)
return errHelp
}
@@ -592,15 +553,12 @@ func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort u
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
}
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if terminateTLS {
sc.TCP[srcPort].TerminateTLS = dnsName
}
sc.SetTCPForwarding(srcPort, fwdAddr, terminateTLS, dnsName)
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
@@ -626,11 +584,7 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
}
if ph := sc.GetTCPPortHandler(src); ph != nil {
delete(sc.TCP, src)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
sc.RemoveTCPForwarding(src)
return e.lc.SetServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
@@ -642,6 +596,9 @@ func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
// Examples:
// - tailscale status
// - tailscale status --json
//
// TODO(tyler,marwan,sonia): `status` should also report foreground configs,
// currently only reports background config.
func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
@@ -844,10 +801,10 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
return nil // already enabled
}
if info.Text != "" {
fmt.Fprintln(os.Stdout, "\n"+info.Text)
fmt.Fprintln(Stdout, "\n"+info.Text)
}
if info.URL != "" {
fmt.Fprintln(os.Stdout, "\n "+info.URL+"\n")
fmt.Fprintln(Stdout, "\n "+info.URL+"\n")
}
if !info.ShouldWait {
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_not_awaiting_enablement", feature), 1)
@@ -892,7 +849,7 @@ func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string,
}
if gotAll {
e.lc.IncrementCounter(ctx, fmt.Sprintf("%s_enabled", feature), 1)
fmt.Fprintln(os.Stdout, "Success.")
fmt.Fprintln(Stdout, "Success.")
return nil
}
}

View File

@@ -9,6 +9,7 @@ import (
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
@@ -21,6 +22,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/logger"
)
@@ -53,6 +55,9 @@ func TestCleanMountPoint(t *testing.T) {
}
func TestServeConfigMutations(t *testing.T) {
tstest.Replace(t, &Stderr, io.Discard)
tstest.Replace(t, &Stdout, io.Discard)
// Stateful mutations, starting from an empty config.
type step struct {
command []string // serve args; nil means no command to run (only reset)
@@ -705,6 +710,7 @@ func TestServeConfigMutations(t *testing.T) {
lc: lc,
testFlagOut: &flagOut,
testStdout: &stdout,
testStderr: io.Discard,
}
lastCount := lc.setCount
var cmd *ffcli.Command
@@ -716,6 +722,10 @@ func TestServeConfigMutations(t *testing.T) {
cmd = newServeLegacyCommand(e)
args = st.command
}
if cmd.FlagSet == nil {
cmd.FlagSet = flag.NewFlagSet(cmd.Name, flag.ContinueOnError)
cmd.FlagSet.SetOutput(Stdout)
}
err := cmd.ParseAndRun(context.Background(), args)
if flagOut.Len() > 0 {
t.Logf("flag package output: %q", flagOut.Bytes())
@@ -749,6 +759,9 @@ func TestServeConfigMutations(t *testing.T) {
}
func TestVerifyFunnelEnabled(t *testing.T) {
tstest.Replace(t, &Stderr, io.Discard)
tstest.Replace(t, &Stdout, io.Discard)
lc := &fakeLocalServeClient{}
var stdout bytes.Buffer
var flagOut bytes.Buffer
@@ -756,6 +769,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
lc: lc,
testFlagOut: &flagOut,
testStdout: &stdout,
testStderr: io.Discard,
}
tests := []struct {
@@ -807,9 +821,11 @@ func TestVerifyFunnelEnabled(t *testing.T) {
lc.setQueryFeatureResponse(tt.queryFeatureResponse)
if tt.caps != nil {
oldCaps := fakeStatus.Self.Capabilities
defer func() { fakeStatus.Self.Capabilities = oldCaps }() // reset after test
fakeStatus.Self.Capabilities = tt.caps
cm := make(tailcfg.NodeCapMap)
for _, c := range tt.caps {
cm[c] = nil
}
tstest.Replace(t, &fakeStatus.Self.CapMap, cm)
}
defer func() {
@@ -853,8 +869,11 @@ type fakeLocalServeClient struct {
var fakeStatus = &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []tailcfg.NodeCapability{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
DNSName: "foo.test.ts.net",
CapMap: tailcfg.NodeCapMap{
tailcfg.NodeAttrFunnel: nil,
tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil,
},
},
}

View File

@@ -18,7 +18,6 @@ import (
"os/signal"
"path"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
@@ -111,10 +110,10 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
Name: info.Name,
ShortHelp: info.ShortHelp,
ShortUsage: strings.Join([]string{
fmt.Sprintf("%s <target>", info.Name),
fmt.Sprintf("%s status [--json]", info.Name),
fmt.Sprintf("%s reset", info.Name),
}, "\n "),
fmt.Sprintf("tailscale %s <target>", info.Name),
fmt.Sprintf("tailscale %s status [--json]", info.Name),
fmt.Sprintf("tailscale %s reset", info.Name),
}, "\n"),
LongHelp: info.LongHelp + fmt.Sprintf(strings.TrimSpace(serveHelpCommon), info.Name),
Exec: e.runServeCombined(subcmd),
@@ -132,20 +131,20 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
UsageFunc: usageFuncNoDefaultValues,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "view current proxy configuration",
Name: "status",
ShortUsage: "tailscale " + info.Name + " status [--json]",
Exec: e.runServeStatus,
ShortHelp: "View current " + info.Name + " configuration",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
ShortHelp: "reset current serve/funnel config",
Exec: e.runServeReset,
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
Name: "reset",
ShortUsage: "tailscale " + info.Name + " reset",
ShortHelp: "Reset current " + info.Name + " config",
Exec: e.runServeReset,
FlagSet: e.newFlags("serve-reset", nil),
},
},
}
@@ -334,7 +333,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
const backgroundExistsMsg = "background configuration already exists, use `tailscale %s --%s=%d off` to remove the existing configuration"
func (e *serveEnv) validateConfig(sc *ipn.ServeConfig, port uint16, wantServe serveType) error {
sc, isFg := findConfig(sc, port)
sc, isFg := sc.FindConfig(port)
if sc == nil {
return nil
}
@@ -366,24 +365,6 @@ func serveFromPortHandler(tcp *ipn.TCPPortHandler) serveType {
}
}
// findConfig finds a config that contains the given port, which can be
// the top level background config or an inner foreground one. The second
// result is true if it's foreground
func findConfig(sc *ipn.ServeConfig, port uint16) (*ipn.ServeConfig, bool) {
if sc == nil {
return nil, false
}
if _, ok := sc.TCP[port]; ok {
return sc, false
}
for _, sc := range sc.Foreground {
if _, ok := sc.TCP[port]; ok {
return sc, true
}
}
return nil, false
}
func (e *serveEnv) setServe(sc *ipn.ServeConfig, st *ipnstate.Status, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool) error {
// update serve config based on the type
switch srvType {
@@ -516,9 +497,9 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
}
h.Text = text
case filepath.IsAbs(target):
if version.IsSandboxedMacOS() {
// don't allow path serving for now on macOS (2022-11-15)
return errors.New("path serving is not supported if sandboxed on macOS")
if version.IsMacAppStore() || version.IsMacSys() {
// The Tailscale network extension cannot serve arbitrary paths on macOS due to sandbox restrictions (2024-03-26)
return errors.New("Path serving is not supported on macOS due to sandbox restrictions. To use Tailscale Serve on macOS, switch to the open-source tailscaled distribution. See https://tailscale.com/kb/1065/macos-variants for more information.")
}
target = filepath.Clean(target)
@@ -535,7 +516,7 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
}
h.Path = target
default:
t, err := expandProxyTargetDev(target, []string{"http", "https", "https+insecure"}, "http")
t, err := ipn.ExpandProxyTargetValue(target, []string{"http", "https", "https+insecure"}, "http")
if err != nil {
return err
}
@@ -547,29 +528,7 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
return errors.New("cannot serve web; already serving TCP")
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
}
mak.Set(&sc.Web[hp].Handlers, mount, h)
// TODO: handle multiple web handlers from foreground mode
for k, v := range sc.Web[hp].Handlers {
if v == h {
continue
}
// If the new mount point ends in / and another mount point
// shares the same prefix, remove the other handler.
// (e.g. /foo/ overwrites /foo)
// The opposite example is also handled.
m1 := strings.TrimSuffix(mount, "/")
m2 := strings.TrimSuffix(k, "/")
if m1 == m2 {
delete(sc.Web[hp].Handlers, k)
}
}
sc.SetWebHandler(h, dnsName, srvPort, mount, useTLS)
return nil
}
@@ -585,7 +544,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
return fmt.Errorf("invalid TCP target %q", target)
}
targetURL, err := expandProxyTargetDev(target, []string{"tcp"}, "tcp")
targetURL, err := ipn.ExpandProxyTargetValue(target, []string{"tcp"}, "tcp")
if err != nil {
return fmt.Errorf("unable to expand target: %v", err)
}
@@ -600,11 +559,7 @@ func (e *serveEnv) applyTCPServe(sc *ipn.ServeConfig, dnsName string, srcType se
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
}
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: dstURL.Host})
if terminateTLS {
sc.TCP[srcPort].TerminateTLS = dnsName
}
sc.SetTCPForwarding(srcPort, dstURL.Host, terminateTLS, dnsName)
return nil
}
@@ -618,14 +573,10 @@ func (e *serveEnv) applyFunnel(sc *ipn.ServeConfig, dnsName string, srvPort uint
sc = new(ipn.ServeConfig)
}
// TODO: should ensure there is no other conflicting funnel
// TODO: add error handling for if toggling for existing sc
if allowFunnel {
mak.Set(&sc.AllowFunnel, hp, true)
} else if _, exists := sc.AllowFunnel[hp]; exists {
fmt.Fprintf(e.stderr(), "Removing Funnel for %s\n", hp)
delete(sc.AllowFunnel, hp)
if _, exists := sc.AllowFunnel[hp]; exists && !allowFunnel {
fmt.Fprintf(e.stderr(), "Removing Funnel for %s:%s\n", dnsName, hp)
}
sc.SetFunnel(dnsName, srvPort, allowFunnel)
}
// unsetServe removes the serve config for the given serve port.
@@ -814,34 +765,7 @@ func (e *serveEnv) removeWebServe(sc *ipn.ServeConfig, dnsName string, srvPort u
}
}
// delete existing handler, then cascade delete if empty
for _, m := range mounts {
delete(sc.Web[hp].Handlers, m)
}
if len(sc.Web[hp].Handlers) == 0 {
delete(sc.Web, hp)
delete(sc.AllowFunnel, hp)
delete(sc.TCP, srvPort)
}
// clear empty maps mostly for testing
if len(sc.Web) == 0 {
sc.Web = nil
}
if len(sc.TCP) == 0 {
sc.TCP = nil
}
// disable funnel if no remaining mounts exist for the serve port
if sc.Web == nil && sc.TCP == nil {
delete(sc.AllowFunnel, hp)
}
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
sc.RemoveWebHandler(dnsName, srvPort, mounts, true)
return nil
}
@@ -857,68 +781,10 @@ func (e *serveEnv) removeTCPServe(sc *ipn.ServeConfig, src uint16) error {
if sc.IsServingWeb(src) {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
}
delete(sc.TCP, src)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
sc.RemoveTCPForwarding(src)
return nil
}
// expandProxyTargetDev expands the supported target values to be proxied
// allowing for input values to be a port number, a partial URL, or a full URL
// including a path.
//
// examples:
// - 3000
// - localhost:3000
// - tcp://localhost:3000
// - http://localhost:3000
// - https://localhost:3000
// - https-insecure://localhost:3000
// - https-insecure://localhost:3000/foo
func expandProxyTargetDev(target string, supportedSchemes []string, defaultScheme string) (string, error) {
const host = "127.0.0.1"
// support target being a port number
if port, err := strconv.ParseUint(target, 10, 16); err == nil {
return fmt.Sprintf("%s://%s:%d", defaultScheme, host, port), nil
}
// prepend scheme if not present
if !strings.Contains(target, "://") {
target = defaultScheme + "://" + target
}
// make sure we can parse the target
u, err := url.ParseRequestURI(target)
if err != nil {
return "", fmt.Errorf("invalid URL %w", err)
}
// ensure a supported scheme
if !slices.Contains(supportedSchemes, u.Scheme) {
return "", fmt.Errorf("must be a URL starting with one of the supported schemes: %v", supportedSchemes)
}
// validate the host.
switch u.Hostname() {
case "localhost", "127.0.0.1":
default:
return "", errors.New("only localhost or 127.0.0.1 proxies are currently supported")
}
// validate the port
port, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil || port == 0 {
return "", fmt.Errorf("invalid port %q", u.Port())
}
u.Host = fmt.Sprintf("%s:%d", host, port)
return u.String(), nil
}
// cleanURLPath ensures the path is clean and has a leading "/".
func cleanURLPath(urlPath string) (string, error) {
if urlPath == "" {
@@ -957,12 +823,12 @@ func (e *serveEnv) stdout() io.Writer {
if e.testStdout != nil {
return e.testStdout
}
return os.Stdout
return Stdout
}
func (e *serveEnv) stderr() io.Writer {
if e.testStderr != nil {
return e.testStderr
}
return os.Stderr
return Stderr
}

View File

@@ -1041,63 +1041,6 @@ func TestSrcTypeFromFlags(t *testing.T) {
}
}
func TestExpandProxyTargetDev(t *testing.T) {
tests := []struct {
name string
input string
defaultScheme string
supportedSchemes []string
expected string
wantErr bool
}{
{name: "port-only", input: "8080", expected: "http://127.0.0.1:8080"},
{name: "hostname+port", input: "localhost:8080", expected: "http://127.0.0.1:8080"},
{name: "convert-localhost", input: "http://localhost:8080", expected: "http://127.0.0.1:8080"},
{name: "no-change", input: "http://127.0.0.1:8080", expected: "http://127.0.0.1:8080"},
{name: "include-path", input: "http://127.0.0.1:8080/foo", expected: "http://127.0.0.1:8080/foo"},
{name: "https-scheme", input: "https://localhost:8080", expected: "https://127.0.0.1:8080"},
{name: "https+insecure-scheme", input: "https+insecure://localhost:8080", expected: "https+insecure://127.0.0.1:8080"},
{name: "change-default-scheme", input: "localhost:8080", defaultScheme: "https", expected: "https://127.0.0.1:8080"},
{name: "change-supported-schemes", input: "localhost:8080", defaultScheme: "tcp", supportedSchemes: []string{"tcp"}, expected: "tcp://127.0.0.1:8080"},
// errors
{name: "invalid-port", input: "localhost:9999999", wantErr: true},
{name: "unsupported-scheme", input: "ftp://localhost:8080", expected: "", wantErr: true},
{name: "not-localhost", input: "https://tailscale.com:8080", expected: "", wantErr: true},
{name: "empty-input", input: "", expected: "", wantErr: true},
}
for _, tt := range tests {
defaultScheme := "http"
supportedSchemes := []string{"http", "https", "https+insecure"}
if tt.supportedSchemes != nil {
supportedSchemes = tt.supportedSchemes
}
if tt.defaultScheme != "" {
defaultScheme = tt.defaultScheme
}
t.Run(tt.name, func(t *testing.T) {
actual, err := expandProxyTargetDev(tt.input, supportedSchemes, defaultScheme)
if tt.wantErr == true && err == nil {
t.Errorf("Expected an error but got none")
return
}
if tt.wantErr == false && err != nil {
t.Errorf("Got an error, but didn't expect one: %v", err)
return
}
if actual != tt.expected {
t.Errorf("Got: %q; expected: %q", actual, tt.expected)
}
})
}
}
func TestCleanURLPath(t *testing.T) {
tests := []struct {
input string

View File

@@ -25,7 +25,7 @@ import (
var setCmd = &ffcli.Command{
Name: "set",
ShortUsage: "set [flags]",
ShortUsage: "tailscale set [flags]",
ShortHelp: "Change specified preferences",
LongHelp: `"tailscale set" allows changing specific preferences.
@@ -75,7 +75,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "notify about available Tailscale updates")
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "run a web interface for managing this node, served over Tailscale at port 5252")
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")

View File

@@ -26,7 +26,7 @@ import (
var sshCmd = &ffcli.Command{
Name: "ssh",
ShortUsage: "ssh [user@]<host> [args...]",
ShortUsage: "tailscale ssh [user@]<host> [args...]",
ShortHelp: "SSH to a Tailscale machine",
LongHelp: strings.TrimSpace(`
@@ -48,11 +48,11 @@ The 'tailscale ssh' wrapper adds a few things:
}
func runSSH(ctx context.Context, args []string) error {
if runtime.GOOS == "darwin" && version.IsSandboxedMacOS() && !envknob.UseWIPCode() {
return errors.New("The 'tailscale ssh' subcommand is not available on sandboxed macOS builds.\nUse the regular 'ssh' client instead.")
if runtime.GOOS == "darwin" && version.IsMacAppStore() && !envknob.UseWIPCode() {
return errors.New("The 'tailscale ssh' subcommand is not available on macOS builds distributed through the App Store or TestFlight.\nInstall the Standalone variant of Tailscale (download it from https://pkgs.tailscale.com), or use the regular 'ssh' client instead.")
}
if len(args) == 0 {
return errors.New("usage: ssh [user@]<host>")
return errors.New("usage: tailscale ssh [user@]<host>")
}
arg, argRest := args[0], args[1:]
username, host, ok := strings.Cut(arg, "@")
@@ -106,10 +106,8 @@ func runSSH(ctx context.Context, args []string) error {
"-o", "CanonicalizeHostname no", // https://github.com/tailscale/tailscale/issues/10348
)
// TODO(bradfitz): nc is currently broken on macOS:
// https://github.com/tailscale/tailscale/issues/4529
// So don't use it for now. MagicDNS is usually working on macOS anyway
// and they're not in userspace mode, so 'nc' isn't very useful.
// MagicDNS is usually working on macOS anyway and they're not in userspace
// mode, so 'nc' isn't very useful.
if runtime.GOOS != "darwin" {
socketArg := ""
if rootArgs.socket != "" && rootArgs.socket != paths.DefaultTailscaledSocket() {

View File

@@ -29,7 +29,7 @@ import (
var statusCmd = &ffcli.Command{
Name: "status",
ShortUsage: "status [--active] [--web] [--json]",
ShortUsage: "tailscale status [--active] [--web] [--json]",
ShortHelp: "Show state of tailscaled and its connections",
LongHelp: strings.TrimSpace(`

View File

@@ -17,26 +17,22 @@ import (
)
var switchCmd = &ffcli.Command{
Name: "switch",
ShortHelp: "Switches to a different Tailscale account",
Name: "switch",
ShortUsage: "tailscale switch <id>",
ShortHelp: "Switches to a different Tailscale account",
LongHelp: `"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`,
FlagSet: func() *flag.FlagSet {
fs := flag.NewFlagSet("switch", flag.ExitOnError)
fs.BoolVar(&switchArgs.list, "list", false, "list available accounts")
return fs
}(),
Exec: switchProfile,
UsageFunc: func(*ffcli.Command) string {
return `USAGE
switch <id>
switch --list
"tailscale switch" switches between logged in accounts. You can
use the ID that's returned from 'tailnet switch -list'
to pick which profile you want to switch to. Alternatively, you
can use the Tailnet or the account names to switch as well.
This command is currently in alpha and may change in the future.`
},
}
var switchArgs struct {
@@ -48,7 +44,7 @@ func listProfiles(ctx context.Context) error {
if err != nil {
return err
}
tw := tabwriter.NewWriter(os.Stdout, 2, 2, 2, ' ', 0)
tw := tabwriter.NewWriter(Stdout, 2, 2, 2, ' ', 0)
defer tw.Flush()
printRow := func(vals ...string) {
fmt.Fprintln(tw, strings.Join(vals, "\t"))

View File

@@ -44,7 +44,7 @@ import (
var upCmd = &ffcli.Command{
Name: "up",
ShortUsage: "up [flags]",
ShortUsage: "tailscale up [flags]",
ShortHelp: "Connect to Tailscale, logging in if needed",
LongHelp: strings.TrimSpace(`
@@ -496,11 +496,23 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
running := make(chan bool, 1) // gets value once in state ipn.Running
pumpErr := make(chan error, 1)
var printed bool // whether we've yet printed anything to stdout or stderr
var loginOnce sync.Once
startLoginInteractive := func() { loginOnce.Do(func() { localClient.StartLoginInteractive(ctx) }) }
// localAPIMu should be held while doing mutable LocalAPI calls
// to the backend. In particular, it prevents StartLoginInteractive from
// being called from the watcher goroutine while the Start call from
// the other goroutine is in progress.
// See https://github.com/tailscale/tailscale/issues/7036#issuecomment-2053771466
// TODO(bradfitz): simplify this once #11649 is cleaned up and Start is
// hopefully removed.
var localAPIMu sync.Mutex
startLoginInteractive := sync.OnceFunc(func() {
localAPIMu.Lock()
defer localAPIMu.Unlock()
localClient.StartLoginInteractive(ctx)
})
go func() {
var printed bool // whether we've yet printed anything to stdout or stderr
for {
n, err := watcher.Next()
if err != nil {
@@ -574,12 +586,14 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
// Special case: bare "tailscale up" means to just start
// running, if there's ever been a login.
if simpleUp {
localAPIMu.Lock()
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
},
WantRunningSet: true,
})
localAPIMu.Unlock()
if err != nil {
return err
}
@@ -596,10 +610,13 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if err != nil {
return err
}
if err := localClient.Start(ctx, ipn.Options{
localAPIMu.Lock()
err = localClient.Start(ctx, ipn.Options{
AuthKey: authKey,
UpdatePrefs: prefs,
}); err != nil {
})
localAPIMu.Unlock()
if err != nil {
return err
}
if upArgs.forceReauth {
@@ -652,6 +669,7 @@ func upWorthyWarning(s string) bool {
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
strings.Contains(s, healthmsg.LockedOut) ||
strings.Contains(s, healthmsg.WarnExitNodeUsage) ||
strings.Contains(strings.ToLower(s), "update available: ")
}

View File

@@ -19,8 +19,8 @@ import (
var updateCmd = &ffcli.Command{
Name: "update",
ShortUsage: "update",
ShortHelp: "[BETA] Update Tailscale to the latest/different version",
ShortUsage: "tailscale update",
ShortHelp: "Update Tailscale to the latest/different version",
Exec: runUpdate,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("update")

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"flag"
"fmt"
"os"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/clientupdate"
@@ -18,7 +17,7 @@ import (
var versionCmd = &ffcli.Command{
Name: "version",
ShortUsage: "version [flags]",
ShortUsage: "tailscale version [flags]",
ShortHelp: "Print Tailscale version",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("version")
@@ -70,7 +69,7 @@ func runVersion(ctx context.Context, args []string) error {
Meta: m,
Upstream: upstreamVer,
}
e := json.NewEncoder(os.Stdout)
e := json.NewEncoder(Stdout)
e.SetIndent("", "\t")
return e.Encode(out)
}

View File

@@ -26,7 +26,7 @@ import (
var webCmd = &ffcli.Command{
Name: "web",
ShortUsage: "web [flags]",
ShortUsage: "tailscale web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(`

View File

@@ -9,7 +9,6 @@ import (
"errors"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
@@ -18,13 +17,12 @@ import (
var whoisCmd = &ffcli.Command{
Name: "whois",
ShortUsage: "whois [--json] ip[:port]",
ShortUsage: "tailscale whois [--json] ip[:port]",
ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)",
LongHelp: strings.TrimSpace(`
'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6).
`),
UsageFunc: usageFunc,
Exec: runWhoIs,
Exec: runWhoIs,
FlagSet: func() *flag.FlagSet {
fs := newFlagSet("whois")
fs.BoolVar(&whoIsArgs.json, "json", false, "output in JSON format")
@@ -53,7 +51,7 @@ func runWhoIs(ctx context.Context, args []string) error {
return nil
}
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
w := tabwriter.NewWriter(Stdout, 10, 5, 5, ' ', 0)
fmt.Fprintf(w, "Machine:\n")
fmt.Fprintf(w, " Name:\t%s\n", strings.TrimSuffix(who.Node.Name, "."))
fmt.Fprintf(w, " ID:\t%s\n", who.Node.StableID)

View File

@@ -27,7 +27,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
💣 github.com/mattn/go-isatty from tailscale.com/cmd/tailscale/cli+
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
@@ -84,6 +84,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
tailscale.com/disco from tailscale.com/derp
tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/health from tailscale.com/net/tlsdial
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
@@ -118,7 +119,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailfs from tailscale.com/cmd/tailscale/cli+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/control/controlhttp+

View File

@@ -78,6 +78,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
L github.com/coreos/go-systemd/v22/dbus from tailscale.com/clientupdate
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
@@ -87,8 +88,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
💣 github.com/djherbis/times from tailscale.com/tailfs/tailfsimpl
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/gaissmai/bart from tailscale.com/net/tstun
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from github.com/coreos/go-systemd/v22/dbus+
@@ -100,7 +107,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/clientupdate
github.com/google/uuid from tailscale.com/clientupdate+
github.com/gorilla/csrf from tailscale.com/client/web
github.com/gorilla/securecookie from github.com/gorilla/csrf
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
@@ -109,7 +116,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
github.com/jellydator/ttlcache/v3 from tailscale.com/tailfs/tailfsimpl/compositedav
github.com/jellydator/ttlcache/v3 from tailscale.com/drive/driveimpl/compositedav
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
@@ -119,7 +126,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/huff0+
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
LD github.com/kr/fs from github.com/pkg/sftp
@@ -170,7 +177,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
github.com/tailscale/xnet/webdav from tailscale.com/tailfs/tailfsimpl+
github.com/tailscale/xnet/webdav from tailscale.com/drive/driveimpl+
github.com/tailscale/xnet/webdav/internal/xml from github.com/tailscale/xnet/webdav
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
@@ -249,6 +256,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/drive/driveimpl from tailscale.com/cmd/tailscaled
tailscale.com/drive/driveimpl/compositedav from tailscale.com/drive/driveimpl
tailscale.com/drive/driveimpl/dirfs from tailscale.com/drive/driveimpl+
tailscale.com/drive/driveimpl/shared from tailscale.com/drive/driveimpl+
tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
@@ -285,7 +297,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/flowtrack from tailscale.com/net/packet+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock+
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
tailscale.com/net/netknob from tailscale.com/logpolicy+
@@ -308,24 +320,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun/table from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/control/controlclient+
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
tailscale.com/tailfs from tailscale.com/client/tailscale+
tailscale.com/tailfs/tailfsimpl from tailscale.com/cmd/tailscaled
tailscale.com/tailfs/tailfsimpl/compositedav from tailscale.com/tailfs/tailfsimpl
tailscale.com/tailfs/tailfsimpl/dirfs from tailscale.com/tailfs/tailfsimpl+
tailscale.com/tailfs/tailfsimpl/shared from tailscale.com/tailfs/tailfsimpl+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
tailscale.com/tka from tailscale.com/client/tailscale+
@@ -377,6 +381,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
tailscale.com/util/osuser from tailscale.com/ipn/localapi+
tailscale.com/util/progresstracking from tailscale.com/ipn/localapi
tailscale.com/util/race from tailscale.com/net/dns/resolver
tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
@@ -388,11 +393,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
tailscale.com/util/truncate from tailscale.com/logtail
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
tailscale.com/util/vizerror from tailscale.com/tailcfg+
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/client/web+
tailscale.com/version/distro from tailscale.com/client/web+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
@@ -405,7 +412,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine+
tailscale.com/wgengine/wglog from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/argon2 from tailscale.com/tka
@@ -522,7 +529,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
math/rand from github.com/mdlayher/netlink+
math/rand/v2 from tailscale.com/util/rands
mime from github.com/tailscale/xnet/webdav+
mime/multipart from net/http
mime/multipart from net/http+
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+

View File

@@ -33,6 +33,7 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/drive/driveimpl"
"tailscale.com/envknob"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnlocal"
@@ -52,7 +53,6 @@ import (
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/syncs"
"tailscale.com/tailfs/tailfsimpl"
"tailscale.com/tsd"
"tailscale.com/tsweb/varz"
"tailscale.com/types/flagtype"
@@ -79,7 +79,7 @@ func defaultTunName() string {
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
// as a magic value that uses/creates any free number.
return "utun"
case "plan9":
case "plan9", "aix":
return "userspace-networking"
case "linux":
switch distro.Get() {
@@ -116,7 +116,7 @@ var args struct {
// or comma-separated list thereof.
tunname string
cleanup bool
cleanUp bool
confFile string
debug string
port uint16
@@ -145,7 +145,7 @@ var subCommands = map[string]*func([]string) error{
"uninstall-system-daemon": &uninstallSystemDaemon,
"debug": &debugModeFunc,
"be-child": &beChildFunc,
"serve-tailfs": &serveTailFSFunc,
"serve-taildrive": &serveDriveFunc,
}
var beCLI func() // non-nil if CLI is linked in
@@ -156,7 +156,7 @@ func main() {
printVersion := false
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
flag.BoolVar(&args.cleanUp, "cleanup", false, "clean up system state and exit")
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
@@ -207,7 +207,7 @@ func main() {
os.Exit(0)
}
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanUp {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
}
@@ -387,12 +387,16 @@ func run() (err error) {
}
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
if envknob.Bool("TS_PLEASE_PANIC") {
panic("TS_PLEASE_PANIC asked us to panic")
}
dns.Cleanup(logf, args.tunname)
router.Cleanup(logf, args.tunname)
if envknob.Bool("TS_PLEASE_PANIC") {
panic("TS_PLEASE_PANIC asked us to panic")
}
// Always clean up, even if we're going to run the server. This covers cases
// such as when a system was rebooted without shutting down, or tailscaled
// crashed, and would for example restore system DNS configuration.
dns.CleanUp(logf, args.tunname)
router.CleanUp(logf, args.tunname)
// If the cleanUp flag was passed, then exit.
if args.cleanUp {
return nil
}
@@ -407,7 +411,7 @@ func run() (err error) {
debugMux = newDebugMux()
}
sys.Set(tailfsimpl.NewFileSystemForRemote(logf))
sys.Set(driveimpl.NewFileSystemForRemote(logf))
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
}
@@ -432,13 +436,26 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
if sigPipe != nil {
signal.Ignore(sigPipe)
}
wgEngineCreated := make(chan struct{})
go func() {
select {
case s := <-interrupt:
logf("tailscaled got signal %v; shutting down", s)
cancel()
case <-ctx.Done():
// continue
var wgEngineClosed <-chan struct{}
wgEngineCreated := wgEngineCreated // local shadow
for {
select {
case s := <-interrupt:
logf("tailscaled got signal %v; shutting down", s)
cancel()
return
case <-wgEngineClosed:
logf("wgengine has been closed; shutting down")
cancel()
return
case <-wgEngineCreated:
wgEngineClosed = sys.Engine.Get().Done()
wgEngineCreated = nil
case <-ctx.Done():
return
}
}
}()
@@ -464,6 +481,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
if err == nil {
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
srv.SetLocalBackend(lb)
close(wgEngineCreated)
return
}
lbErr.Store(err) // before the following cancel
@@ -631,12 +649,12 @@ var tstunNew = tstun.New
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
NetMon: sys.NetMon.Get(),
Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(),
TailFSForLocal: tailfsimpl.NewFileSystemForLocal(logf),
ListenPort: args.port,
NetMon: sys.NetMon.Get(),
Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set,
ControlKnobs: sys.ControlKnobs(),
DriveForLocal: driveimpl.NewFileSystemForLocal(logf),
}
onlyNetstack = name == "userspace-networking"
@@ -695,7 +713,6 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
conf.DNS = d
conf.Router = r
if handleSubnetsInNetstack() {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
netstackSubnetRouter = true
}
sys.Set(conf.Router)
@@ -739,7 +756,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
tfs, _ := sys.TailFSForLocal.GetOK()
tfs, _ := sys.DriveForLocal.GetOK()
ret, err := netstack.Create(logf,
sys.Tun.Get(),
sys.Engine.Get(),
@@ -817,25 +834,25 @@ func beChild(args []string) error {
return f(args[1:])
}
var serveTailFSFunc = serveTailFS
var serveDriveFunc = serveDrive
// serveTailFS serves one or more tailfs on localhost using the WebDAV
// protocol. On UNIX and MacOS tailscaled environment, tailfs spawns child
// tailscaled processes in serve-tailfs mode in order to access the fliesystem
// serveDrive serves one or more Taildrives on localhost using the WebDAV
// protocol. On UNIX and MacOS tailscaled environment, Taildrive spawns child
// tailscaled processes in serve-taildrive mode in order to access the fliesystem
// as specific (usually unprivileged) users.
//
// serveTailFS prints the address on which it's listening to stdout so that the
// serveDrive prints the address on which it's listening to stdout so that the
// parent process knows where to connect to.
func serveTailFS(args []string) error {
func serveDrive(args []string) error {
if len(args) == 0 {
return errors.New("missing shares")
}
if len(args)%2 != 0 {
return errors.New("need <sharename> <path> pairs")
}
s, err := tailfsimpl.NewFileServer()
s, err := driveimpl.NewFileServer()
if err != nil {
return fmt.Errorf("unable to start tailfs FileServer: %v", err)
return fmt.Errorf("unable to start Taildrive file server: %v", err)
}
shares := make(map[string]string)
for i := 0; i < len(args); i += 2 {

View File

@@ -6,7 +6,6 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStartPre=/usr/sbin/tailscaled --cleanup
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup

View File

@@ -42,13 +42,13 @@ import (
"golang.org/x/sys/windows/svc/eventlog"
"golang.zx2c4.com/wintun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/drive/driveimpl"
"tailscale.com/envknob"
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/tailfs/tailfsimpl"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
@@ -316,7 +316,7 @@ func beWindowsSubprocess() bool {
}
sys.Set(netMon)
sys.Set(tailfsimpl.NewFileSystemForRemote(log.Printf))
sys.Set(driveimpl.NewFileSystemForRemote(log.Printf))
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, sys)

View File

@@ -29,7 +29,7 @@ func main() {
DERPMap: derpMap,
ExplicitBaseURL: "http://127.0.0.1:9911",
}
for i := 0; i < *flagNFake; i++ {
for range *flagNFake {
control.AddFakeNode()
}
mux := http.NewServeMux()

View File

@@ -90,8 +90,11 @@ func newIPN(jsConfig js.Value) map[string]any {
c := logtail.Config{
Collection: lpc.Collection,
PrivateID: lpc.PrivateID,
// NewZstdEncoder is intentionally not passed in, compressed requests
// set HTTP headers that are not supported by the no-cors fetching mode.
// Compressed requests set HTTP headers that are not supported by the
// no-cors fetching mode:
CompressLogs: false,
HTTPC: &http.Client{Transport: &noCORSTransport{http.DefaultTransport}},
}
logtail := logtail.NewLogger(c, log.Printf)
@@ -319,7 +322,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
}
func (i *jsIPN) login() {
go i.lb.StartLoginInteractive()
go i.lb.StartLoginInteractive(context.Background())
}
func (i *jsIPN) logout() {

View File

@@ -149,7 +149,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
}
}
writeTemplate("common")
for i := 0; i < t.NumFields(); i++ {
for i := range t.NumFields() {
f := t.Field(i)
fname := f.Name()
if !f.Exported() {
@@ -172,6 +172,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
switch elem.String() {
case "byte":
args.FieldType = it.QualifiedName(fieldType)
it.Import("tailscale.com/types/views")
writeTemplate("byteSliceField")
default:
args.FieldType = it.QualifiedName(elem)
@@ -291,7 +292,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
}
writeTemplate("unsupportedField")
}
for i := 0; i < typ.NumMethods(); i++ {
for i := range typ.NumMethods() {
f := typ.Method(i)
if !f.Exported() {
continue

View File

@@ -91,7 +91,7 @@ func TestFastPath(t *testing.T) {
const packets = 10
s := "test"
for i := 0; i < packets; i++ {
for range packets {
// Many separate writes, to force separate Noise frames that
// all get buffered up and then all sent as a single slice to
// the server.
@@ -251,7 +251,7 @@ func TestConnMemoryOverhead(t *testing.T) {
}
defer closeAll()
for i := 0; i < num; i++ {
for range num {
client, server := pair(t)
closers = append(closers, client, server)
go func() {

View File

@@ -64,7 +64,7 @@ func TestNoReuse(t *testing.T) {
serverHandshakes = map[[48]byte]bool{}
packets = map[[32]byte]bool{}
)
for i := 0; i < 10; i++ {
for range 10 {
var (
clientRaw, serverRaw = memnet.NewConn("noise", 128000)
clientBuf, serverBuf bytes.Buffer
@@ -162,7 +162,7 @@ func (r *tamperReader) Read(bs []byte) (int, error) {
func TestTampering(t *testing.T) {
// Tamper with every byte of the client initiation message.
for i := 0; i < 101; i++ {
for i := range 101 {
var (
clientConn, serverRaw = memnet.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, i, 0}}
@@ -190,7 +190,7 @@ func TestTampering(t *testing.T) {
}
// Tamper with every byte of the server response message.
for i := 0; i < 51; i++ {
for i := range 51 {
var (
clientRaw, serverConn = memnet.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, i, 0}}
@@ -215,7 +215,7 @@ func TestTampering(t *testing.T) {
}
// Tamper with every byte of the first server>client transport message.
for i := 0; i < 30; i++ {
for i := range 30 {
var (
clientRaw, serverConn = memnet.NewConn("noise", 128000)
clientConn = &readerConn{clientRaw, &tamperReader{clientRaw, 51 + i, 0}}
@@ -256,7 +256,7 @@ func TestTampering(t *testing.T) {
}
// Tamper with every byte of the first client>server transport message.
for i := 0; i < 30; i++ {
for i := range 30 {
var (
clientConn, serverRaw = memnet.NewConn("noise", 128000)
serverConn = &readerConn{serverRaw, &tamperReader{serverRaw, 101 + i, 0}}

View File

@@ -9,7 +9,7 @@ import (
)
func fieldsOf(t reflect.Type) (fields []string) {
for i := 0; i < t.NumField(); i++ {
for i := range t.NumField() {
if name := t.Field(i).Name; name != "_" {
fields = append(fields, name)
}

Some files were not shown because too many files have changed in this diff Show More