Compare commits

...

141 Commits

Author SHA1 Message Date
Fran Bull
27b69ca97b wip 2025-02-27 13:43:56 -08:00
Fran Bull
ea684a5ed5 update for start signature change 2025-02-27 13:43:56 -08:00
Fran Bull
238fe26165 fix cmd/natc-consensus tests 2025-02-27 13:43:56 -08:00
Fran Bull
06347d7cc7 wip 2025-02-27 13:43:56 -08:00
Fran Bull
cd7d3ae4d2 add clarifying continue 2025-02-27 13:35:07 -08:00
Fran Bull
7dbcb388b8 shorten 2025-02-27 13:30:11 -08:00
Fran Bull
c4110ec886 Export functions in authorization file 2025-02-27 13:23:26 -08:00
Fran Bull
24ce3279f4 close the conn, don't leave it open 2025-02-27 13:01:05 -08:00
Fran Bull
ad7d1ee07a addr -> netip.AddrPortFrom().String() 2025-02-27 11:41:28 -08:00
Fran Bull
4175e2e21d don't try to http.Error after trying to encode to w 2025-02-27 10:18:30 -08:00
Fran Bull
af2fd8bd7e change comment style 2025-02-27 09:38:28 -08:00
Fran Bull
559643b034 command mux robustness 2025-02-27 08:47:24 -08:00
Fran Bull
b789daaf99 badrequest -> methodnotallowed 2025-02-27 08:01:23 -08:00
Fran Bull
ace8630d89 splithostport -> parseaddrport 2025-02-27 07:47:07 -08:00
Fran Bull
e8b2224932 401 -> 403 2025-02-27 07:39:16 -08:00
Fran Bull
69f76641ac auth getstatus max 1/s 2025-02-27 07:31:30 -08:00
Fran Bull
ae30f58b46 centralize cmd http auth 2025-02-27 05:15:49 -08:00
Fran Bull
5afa742b06 unnecessary export 2025-02-26 15:03:37 -08:00
Fran Bull
c35c3d1194 use a views.Slice 2025-02-26 14:45:44 -08:00
Fran Bull
66ecab9540 use a set for authorization, test it 2025-02-26 14:09:24 -08:00
Fran Bull
f63ce0066d comment use of mutex 2025-02-26 09:36:20 -08:00
Fran Bull
9d65e1fc22 use status code constants, and the right ones 2025-02-26 09:34:13 -08:00
Fran Bull
2d2b954006 no need to defer 2025-02-26 09:26:39 -08:00
Fran Bull
5e15c25937 no need for a blank return at the end 2025-02-26 09:25:36 -08:00
Fran Bull
f7ec770f03 use new encoder, not marshalindent 2025-02-26 09:10:10 -08:00
Fran Bull
3a35ac716d only serve debug monitor if asked for 2025-02-26 09:04:51 -08:00
Fran Bull
e13b8c271b log errors don't return to http client 2025-02-26 08:38:08 -08:00
Fran Bull
e0415e0221 use dnsname instead of strings split 2025-02-26 08:29:51 -08:00
Fran Bull
febe30ea68 error auth refresh if server not running 2025-02-26 08:29:13 -08:00
Fran Bull
5fa145674d make tests pass under -race 2025-02-25 14:57:50 -08:00
Fran Bull
8dfb749ea5 missed a direct events access 2025-02-24 12:01:15 -08:00
Fran Bull
773894638c lint says conn can't be nil 2025-02-24 11:55:45 -08:00
Fran Bull
0b971dffd3 configure logging 2025-02-24 11:41:53 -08:00
Fran Bull
f0223a9dba avoid race 2025-02-24 10:38:21 -08:00
Fran Bull
3ed0736ae9 protect nil pointer 2025-02-24 10:29:05 -08:00
Fran Bull
05277e020e move package doc to right place 2025-02-24 08:45:10 -08:00
Fran Bull
e623e1d2d9 go mod tidy 2025-02-24 08:30:45 -08:00
Fran Bull
aee5b38001 use httpm i guess? 2025-02-21 13:38:53 -08:00
Fran Bull
89af057be5 add copyright headers 2025-02-21 13:10:59 -08:00
Fran Bull
d944cd1778 do we need to wait longer? 2025-02-21 13:07:41 -08:00
Fran Bull
0354836398 don't actually need those bits for the interface 2025-02-21 13:06:07 -08:00
Fran Bull
4040e14cb8 use the error value 2025-02-21 13:02:05 -08:00
Fran Bull
82e6b2508a fix test 2025-02-21 12:34:53 -08:00
Fran Bull
a828917152 allow concurrent usage of authorization 2025-02-21 11:24:00 -08:00
Fran Bull
d593a85bae comments, whitespace 2025-02-21 11:23:22 -08:00
Fran Bull
7c539e3d2f to squash: restrict communication to tagged nodes 2025-02-20 15:14:22 -08:00
Fran Bull
6ebb0c749d tsconsensus: add a tsconsensus package
tsconsensus enables tsnet.Server instances to form a consensus.

tsconsensus wraps hashicorp/raft with
 * the ability to do discovery via tailscale tags
 * inter node communication over tailscale
 * routing of commands to the leader

Updates #14667

Signed-off-by: Fran Bull <fran@tailscale.com>
2025-02-20 11:12:17 -08:00
Erisa A
074372d6c5 scripts/installer.sh: add SparkyLinux as a Debian derivative (#15076)
Fixes #15075

Signed-off-by: Erisa A <erisa@tailscale.com>
2025-02-20 18:22:08 +00:00
Andrew Lytvynov
2c3338c46b client/tailscale: fix Client.BuildURL and Client.BuildTailnetURL (#15064)
This method uses `path.Join` to build the URL. Turns out with 1.24 this
started stripping consecutive "/" characters, so "http://..." in baseURL
becomes "http:/...".

Also, `c.Tailnet` is a function that returns `c.tailnet`. Using it as a
path element would encode as a pointer instead of the tailnet name.

Finally, provide a way to prevent escaping of path elements e.g. for `?`
in `acl?details=1`.

Updates #15015

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-19 17:19:54 -08:00
Brad Fitzpatrick
836c01258d go.toolchain.branch: update to Go 1.24 (#15016)
* go.toolchain.branch: update to Go 1.24

Updates #15015

Change-Id: I29c934ec17e60c3ac3264f30fbbe68fc21422f4d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>

* cmd/testwrapper: fix for go1.24

Updates #15015

Signed-off-by: Paul Scott <paul@tailscale.com>

* go.mod,Dockerfile: bump to Go 1.24

Also bump golangci-lint to a version that was built with 1.24

Updates #15015

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>

---------

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Paul Scott <paul@tailscale.com>
Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Co-authored-by: Paul Scott <paul@tailscale.com>
Co-authored-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-19 10:55:49 -08:00
Andrew Lytvynov
cc923713f6 tempfork/acme: pull in latest changes for Go 1.24 (#15062)
9a281fd8fa

Updates #15015

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-19 10:42:06 -08:00
Andrew Lytvynov
323747c3e0 various: disable MPTCP when setting TCP_USER_TIMEOUT sockopt (#15063)
There's nothing about it on
https://github.com/multipath-tcp/mptcp_net-next/issues/ but empirically
MPTCP doesn't support this option on awly's kernel 6.13.2 and in GitHub
actions.

Updates #15015

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-19 10:41:45 -08:00
Nick Khyl
09982e1918 ipn/ipnlocal: reset always-on override and apply policy settings on start
We already reset the always-on override flag when switching profiles and in a few other cases.
In this PR, we update (*LocalBackend).Start() to reset it as well. This is necessary to support
scenarios where Start() is called explicitly, such as when the GUI starts or when tailscale up is used
with additional flags and passes prefs via ipn.Options in a call to Start() rather than via EditPrefs.

Additionally, we update it to apply policy settings to the current prefs, which is necessary
for properly overriding prefs specified in ipn.Options.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-18 16:49:25 -06:00
Percy Wegmann
1f1a26776b client/tailscale,cmd/k8s-operator,internal/client/tailscale: move VIP service client methods into internal control client
Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 16:25:17 -06:00
Percy Wegmann
9c731b848b cmd/gitops-pusher: log error details when unable to fetch ACL ETag
This will help debug unexpected issues encountered by consumers of the gitops-pusher.

Updates tailscale/corp#26664

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 14:29:14 -06:00
Andrew Lytvynov
ec5f04b274 appc: fix a deadlock in route advertisements (#15031)
`routeAdvertiser` is the `iplocal.LocalBackend`. Calls to
`Advertise/UnadvertiseRoute` end up calling `EditPrefs` which in turn
calls `authReconfig` which finally calls `readvertiseAppConnectorRoutes`
which calls `AppConnector.DomainRoutes` and gets stuck on a mutex that
was already held when `routeAdvertiser` was called.

Make all calls to `routeAdvertiser` in `app.AppConnector` go through the
execqueue instead as a short-term fix.

Updates tailscale/corp#25965

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
Co-authored-by: Irbe Krumina <irbe@tailscale.com>
2025-02-18 11:31:14 -08:00
Percy Wegmann
052eefbcce tsnet: require I_Acknowledge_This_API_Is_Experimental to use AuthenticatedAPITransport()
It's not entirely clear whether this capability will be maintained, or in what form,
so this serves as a warning to that effect.

Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 10:23:04 -06:00
Percy Wegmann
9ae9de469a internal/client/tailscale: change Client from alias into wrapper
This will allow Client to be extended with additional functions for internal use.

Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 10:23:04 -06:00
Percy Wegmann
8a792ab540 tsnet: provide AuthenticatedAPITransport for use with tailscale.com/client/tailscale/v2
This allows use of the officially supported control server API,
authenticated with the tsnet node's nodekey.

Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 10:23:04 -06:00
Percy Wegmann
4f0222388a cmd,tsnet,internal/client: create internal shim to deprecated control plane API
Even after we remove the deprecated API, we will want to maintain a minimal
API for internal use, in order to avoid importing the external
tailscale.com/client/tailscale/v2 package. This shim exposes only the necessary
parts of the deprecated API for internal use, which gains us the following:

1. It removes deprecation warnings for internal use of the API.
2. It gives us an inventory of which parts we will want to keep for internal use.

Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 10:23:04 -06:00
Percy Wegmann
d923979e65 client/tailscale: mark control API client deprecated
The official client for 3rd party use is at tailscale.com/client/tailscale/v2.

Updates #22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-18 10:23:04 -06:00
Brad Fitzpatrick
cbf3852b5d cmd/testwrapper: temporarily remove test coverage support
testwrapper doesn't work with Go 1.24 and the coverage support is
making it harder to debug.

Updates #15015
Updates tailscale/corp#26659

Change-Id: I0125e881d08c92f1ecef88b57344f6bbb571b569
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-17 09:51:23 -08:00
Irbe Krumina
b21eec7621 ipn/ipnlocal,tailcfg: don't send WireIngress if IngressEnabled already true (#14960)
Hostinfo.WireIngress is used as a hint that the node intends to use
funnel. We now send another field, IngressEnabled, in cases where
funnel is explicitly enabled, and the logic control-side has
been changed to look at IngressEnabled as well as WireIngress in all
cases where previously the hint was used - so we can now stop sending
WireIngress when IngressEnabled is true to save some bandwidth.

Updates tailscale/tailscale#11572
Updates tailscale/corp#25931

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-02-16 09:38:02 +00:00
James Tucker
606f7ef2c6 net/netcheck: remove unnecessary custom map clone function
Updates #8419
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-14 18:56:10 -08:00
Nick Khyl
6df5c8f32e various: keep tailscale connected when Always On mode is enabled on Windows
In this PR, we enable the registration of LocalBackend extensions to exclude code specific to certain
platforms or environments. We then introduce desktopSessionsExt, which is included only in Windows builds
and only if the ts_omit_desktop_sessions tag is disabled for the build. This extension tracks desktop sessions
and switches to (or remains on) the appropriate profile when a user signs in or out, locks their screen,
or disconnects a remote session.

As desktopSessionsExt requires an ipn/desktop.SessionManager, we register it with tsd.System
for the tailscaled subprocess on Windows.

We also fix a bug in the sessionWatcher implementation where it attempts to close a nil channel on stop.

Updates #14823
Updates tailscale/corp#26247

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-14 16:40:54 -06:00
Irbe Krumina
e11ff28443 cmd/k8s-operator: allow to optionally configure an HTTP endpoint for the HA Ingress (#14986)
Updates tailscale/corp#24795

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-02-14 18:07:17 +00:00
James Sanderson
45f29a208a control/controlclient,tailcfg:types: remove MaxKeyduration from NetMap
This reverts most of 124dc10261 (#10401).

Removing in favour of adding this in CapMaps instead (#14829).

Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2025-02-14 18:06:23 +00:00
James Sanderson
717fa68f3a tailcfg: read max key duration from node cap map [capver 114]
This will be used by clients to make better decisions on when to warn users
about impending key expiry.

Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2025-02-14 18:06:23 +00:00
kari-ts
4c3c04a413 ipn, tailscale/cli: add TaildropTargetStatus and remove race with FileTargets (#15017)
Introduce new TaildropTargetStatus in PeerStatus
Refactor getTargetStableID to solely rely on Status() instead of calling FileTargets(). This removes a possible race condition between the two calls and provides more detailed failure information if a peer can't receive files.

Updates tailscale/tailscale#14393

Signed-off-by: kari-ts <kari@tailscale.com>
2025-02-14 09:56:50 -08:00
James 'zofrex' Sanderson
e142571397 ipn/ipnlocal: add GetFilterForTest (#15025)
Needed to test full packet filter in e2e tests. See tailscale/corp#26596

Updates tailscale/corp#20514

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2025-02-14 15:25:48 +00:00
Joe Tsai
1d035db4df types/bools: fix doc typo (#15021)
The Select function was renamed as IfElse.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2025-02-13 16:12:00 -08:00
Percy Wegmann
db231107a2 ssh/tailssh: accept passwords and public keys
Some clients don't request 'none' authentication. Instead, they immediately supply
a password or public key. This change allows them to do so, but ignores the supplied
credentials and authenticates using Tailscale instead.

Updates #14922

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-13 11:29:45 -06:00
James Tucker
f2f7fd12eb go.mod: bump bart
Bart has had some substantial improvements in internal representation,
update functions, and other optimizations to reduce memory usage and
improve runtime performance.

Updates tailscale/corp#26353

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-12 17:52:33 -08:00
Nick Khyl
7aef4fd44d ipn/ipn{local,server}: extract logic that determines the "best" Tailscale profile to use
In this PR, we further refactor LocalBackend and Unattended Mode to extract the logic that determines
which profile should be used at the time of the check, such as when a LocalAPI client connects or disconnects.
We then update (*LocalBackend).switchProfileLockedOnEntry to to switch to the profile returned by
(*LocalBackend).resolveBestProfileLocked() rather than to the caller-specified specified profile, and rename it
to switchToBestProfileLockedOnEntry.

This is done in preparation for updating (*LocalBackend).getBackgroundProfileIDLocked to support Always-On
mode by determining which profile to use based on which users, if any, are currently logged in and have an active
foreground desktop session.

Updates #14823
Updates tailscale/corp#26247

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-12 19:06:40 -06:00
Brad Fitzpatrick
b7f508fccf Revert "control/controlclient: delete unreferenced mapSession UserProfiles"
This reverts commit 413fb5b933.

See long story in #14992

Updates #14992
Updates tailscale/corp#26058

Change-Id: I3de7d080443efe47cbf281ea20887a3caf202488
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 14:53:04 -08:00
Nick Khyl
01efddea01 docs/windows/policy: update ADMX/ADML policy definitions to include the new Always On setting
This adds a new policy definition for the AlwaysOn.Enabled policy setting
as well as the AlwaysOn.OverrideWithReason sub-option.

Updates #14823
Updates tailscale/corp#26247

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-11 16:17:37 -06:00
License Updater
2994dde535 licenses: update license notices
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2025-02-11 13:58:37 -08:00
Nick Khyl
9b32ba7f54 ipn/ipn{local,server}: move "staying alive in server mode" from ipnserver to LocalBackend
Currently, we disconnect Tailscale and reset LocalBackend on Windows when the last LocalAPI client
disconnects, unless Unattended Mode is enabled for the current profile. And the implementation
is somewhat racy since the current profile could theoretically change after
(*ipnserver.Server).addActiveHTTPRequest checks (*LocalBackend).InServerMode() and before it calls
(*LocalBackend).SetCurrentUser(nil) (or, previously, (*LocalBackend).ResetForClientDisconnect).

Additionally, we might want to keep Tailscale running and connected while a user is logged in
rather than tying it to whether a LocalAPI client is connected (i.e., while the GUI is running),
even when Unattended Mode is disabled for a profile. This includes scenarios where the new
AlwaysOn mode is enabled, as well as when Tailscale is used on headless Windows editions,
such as Windows Server Core, where the GUI is not supported. It may also be desirable to switch
to the "background" profile when a user logs off from their device or implement other similar
features.

To facilitate these improvements, we move the logic from ipnserver.Server to ipnlocal.LocalBackend,
where it determines whether to keep Tailscale running when the current user disconnects.
We also update the logic that determines whether a connection should be allowed to better reflect
the fact that, currently, LocalAPI connections are not allowed unless:
 - the current UID is "", meaning that either we are not on a multi-user system or Tailscale is idle;
 - the LocalAPI client belongs to the current user (their UIDs are the same);
 - the LocalAPI client is Local System (special case; Local System is always allowed).
Whether Unattended Mode is enabled only affects the error message returned to the Local API client
when the connection is denied.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-11 15:58:06 -06:00
Nick Khyl
bc0cd512ee ipn/desktop: add a new package for managing desktop sessions on Windows
This PR adds a new package, ipn/desktop, which provides a platform-agnostic
interface for enumerating desktop sessions and registering session callbacks.
Currently, it is implemented only for Windows.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-11 15:31:42 -06:00
Nick Khyl
5eacf61844 ipn/ipnauth: implement WindowsActor
WindowsActor is an ipnauth.Actor implementation that represents a logged-in
Windows user by wrapping their Windows user token.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-11 15:31:42 -06:00
Nick Khyl
e9e2bc5bd7 ipn/ipn{auth,server}: update ipnauth.Actor to carry a context
The context carries additional information about the actor, such as the
request reason, and is canceled when the actor is done.

Additionally, we implement three new ipn.Actor types that wrap other actors
to modify their behavior:
 - WithRequestReason, which adds a request reason to the actor;
 - WithoutClose, which narrows the actor's interface to prevent it from being
   closed;
 - WithPolicyChecks, which adds policy checks to the actor's CheckProfileAccess
   method.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-11 15:31:42 -06:00
Brad Fitzpatrick
5a082fccec tailcfg: remove ancient UserProfiles.Roles field
And add omitempty to the ProfilePicURL too while here. Plenty
of users (and tagged devices) don't have profile pics.

Updates #14988

Change-Id: I6534bc14edb58fe1034d2d35ae2395f09fd7dd0d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 13:16:18 -08:00
Andrew Dunham
926a43fe51 tailcfg: make NetPortRange.Bits omitempty
This is deprecated anyway, and we don't need to be sending
`"Bits":null` on the wire for the majority of clients.

Updates tailscale/corp#20965
Updates tailscale/corp#26353

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I95a3e3d72619389ae34a6547ebf47043445374e1
2025-02-11 15:42:56 -05:00
Anton
f35c49d211 net/dns: update to illarion/gonotify/v3 to fix a panic
Fixes #14699

Signed-off-by: Anton <anton@tailscale.com>
2025-02-11 18:53:38 +00:00
Anton
c4984632ca net/dns: add a simple test for resolv.conf inotify watcher
Updates #14699

Signed-off-by: Anton <anton@tailscale.com>
2025-02-11 18:53:38 +00:00
Brad Fitzpatrick
b865ceea20 tailcfg: update + clean up machine API docs, remove some dead code
The machine API docs were still often referring to the nacl boxes
which are no longer present in the client. Fix that up, fix the paths,
add the HTTP methods.

And then delete some unused code I found in the process.

Updates #cleanup

Change-Id: I1591274acbb00a08b7ca4879dfebd5e6b8a9fbcd
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 10:40:24 -08:00
Joe Tsai
8b347060f8 types/bool: add Int (#14984)
Add Int which converts a bool into an integer.

Updates tailscale/corp#22024

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2025-02-11 10:23:36 -08:00
Brad Fitzpatrick
27f8e2e31d go.mod: bump x/* deps
Notably, this pulls in https://go.googlesource.com/net/+/2dab271ff1b7396498746703d88fefcddcc5cec7
for golang/go#71557.

Updates #8043

Change-Id: I3637dbf27b90423dd4d54d147f12688b51f3ce36
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 09:18:14 -08:00
Brad Fitzpatrick
2f98197857 tempfork/sshtest/ssh: add fork of golang.org/x/crypto/ssh for testing only
This fork golang.org/x/crypto/ssh (at upstream x/crypto git rev e47973b1c1)
into tailscale.com/tempfork/sshtest/ssh so we can hack up the client in weird
ways to simulate other SSH clients seen in the wild.

Two changes were made to the files when they were copied from x/crypto:

* internal/poly1305 imports were replaced by the non-internal version;
  no code changes otherwise. It didn't need the internal one.
* all decode-with-passphrase funcs were deleted, to avoid
  using the internal package x/crypto/ssh/internal/bcrypt_pbkdf

Then the tests passed.

Updates #14969

Change-Id: Ibf1abebfe608c75fef4da0255314f65e54ce5077
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 07:45:06 -08:00
Brad Fitzpatrick
9706c9f4ff types/netmap,*: pass around UserProfiles as views (pointers) instead
Smaller.

Updates tailscale/corp#26058 (@andrew-d noticed during this)

Change-Id: Id33cddd171aaf8f042073b6d3c183b0a746e9931
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-11 07:12:54 -08:00
Andrew Lytvynov
1047d11102 go.toolchain.rev: bump to Go 1.23.6 (#14976)
Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-10 19:03:07 -08:00
Nick Khyl
48dd4bbe21 ipn/ipn{local,server}: remove ResetForClientDisconnect in favor of SetCurrentUser(nil)
There’s (*LocalBackend).ResetForClientDisconnect, and there’s also (*LocalBackend).resetForProfileChangeLockedOnEntry.
Both methods essentially did the same thing but in slightly different ways. For example, resetForProfileChangeLockedOnEntry didn’t reset the control client until (*LocalBackend).Start() was called at the very end and didn’t reset the keyExpired flag, while ResetForClientDisconnect didn’t reinitialize TKA.

Since SetCurrentUser can be called with a nil argument to reset the currently connected user and internally calls resetForProfileChangeLockedOnEntry, we can remove ResetForClientDisconnect and let SetCurrentUser and resetForProfileChangeLockedOnEntry handle it.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-10 14:54:14 -06:00
dependabot[bot]
11cd98fab0 .github: Bump golangci/golangci-lint-action from 6.2.0 to 6.3.1 (#14963)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6.2.0 to 6.3.1.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](ec5d18412c...2e788936b0)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 10:09:44 -07:00
dependabot[bot]
76fe556fcd .github: Bump github/codeql-action from 3.28.5 to 3.28.9 (#14962)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.5 to 3.28.9.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f6091c0113...9e8d0789d4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-10 09:58:08 -07:00
Nick Khyl
122255765a ipn/ipnlocal: fix (*profileManager).DefaultUserProfileID for users other than current
Currently, profileManager filters profiles based on their creator/owner and the "current user"'s UID.
This causes DefaultUserProfileID(uid) to work incorrectly when the UID doesn't match the current user.

While we plan to remove the concept of the "current user" completely, we're not there yet.

In this PR, we fix DefaultUserProfileID by updating profileManager to allow checking profile access
for a given UID and modifying helper methods to accept UID as a parameter when returning
matching profiles.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-10 10:23:10 -06:00
Erisa A
532e38bdc8 scripts/installer.sh: fix --yes argument for freebsd (#14958)
This argument apparently has to be before the package name
Updates #14745

Signed-off-by: Erisa A <erisa@tailscale.com>
2025-02-08 14:45:41 +00:00
Adrian Dewhurst
7b3e5b5df3 wgengine/netstack: respond to service IPs in Linux tun mode
When in tun mode on Linux, AllowedIPs are not automatically added to
netstack because the kernel is responsible for handling subnet routes.
This ensures that virtual IPs are always added to netstack.

When in tun mode, pings were also not being handled, so this adds
explicit support for ping as well.

Fixes tailscale/corp#26387

Change-Id: I6af02848bf2572701288125f247d1eaa6f661107
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2025-02-06 20:14:11 -05:00
James Tucker
e1523fe686 cmd/natc: remove speculative tuning from natc
These tunings reduced memory usage while the implementation was
struggling with earlier bugs, but will no longer be necessary after
those bugs are addressed.

Depends #14933
Depends #14934
Updates #9707
Updates #10408
Updates tailscale/corp#24483
Updates tailscale/corp#25169

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-06 16:17:44 -08:00
James Tucker
e113b106a6 go.mod,wgengine/netstack: use cubic congestion control, bump gvisor
Cubic performs better than Reno in higher BDP scenarios, and enables the
use of the hystart++ implementation contributed by Coder. This improves
throughput on higher BDP links with a much faster ramp.

gVisor is bumped as well for some fixes related to send queue processing
and RTT tracking.

Updates #9707
Updates #10408
Updates #12393
Updates tailscale/corp#24483
Updates tailscale/corp#25169

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-06 16:17:34 -08:00
James Tucker
4903d6c80b wgengine/netstack: block link writes when full rather than drop
Originally identified by Coder and documented in their blog post, this
implementation differs slightly as our link endpoint was introduced for
a different purpose, but the behavior is the same: apply backpressure
rather than dropping packets. This reduces the negative impact of large
packet count bursts substantially. An alternative would be to swell the
size of the channel buffer substantially, however that's largely just
moving where buffering occurs and may lead to reduced signalling back to
lower layer or upstream congestion controls.

Updates #9707
Updates #10408
Updates #12393
Updates tailscale/corp#24483
Updates tailscale/corp#25169

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-06 16:17:25 -08:00
Erisa A
caafe68eb2 scripts/installer.sh: add BigLinux as a Manjaro derivative (#14936)
Fixes #13343

Signed-off-by: Erisa A <erisa@tailscale.com>
2025-02-06 22:19:16 +00:00
Sandro Jäckel
08a96a86af cmd/tailscale: make ssh command work when tailscaled is built with the ts_include_cli tag
Fixes #12125

Signed-off-by: Sandro Jäckel <sandro.jaeckel@gmail.com>
2025-02-06 12:55:40 -06:00
James Tucker
83808029d8 wgengine/netstack: disable RACK on all platforms
The gVisor RACK implementation appears to perfom badly, particularly in
scenarios with higher BDP. This may have gone poorly noticed as a result
of it being gated on SACK, which is not enabled by default in upstream
gVisor, but itself has a higher positive impact on performance. Both the
RACK and DACK implementations (which are now one) have overlapping
non-completion of tasks in their work streams on the public tracker.

Updates #9707

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-06 10:10:44 -08:00
Erisa A
431216017b scripts/installer.sh: add FreeBSD 14 (#14925)
Fixes #14745

Also adds --yes to pkg to match other package managers

Signed-off-by: Erisa A <erisa@tailscale.com>
2025-02-06 16:32:51 +00:00
Mike O'Driscoll
d08f830d50 cmd/derper: support no mesh key (#14931)
Incorrect disabled support for not having a mesh key in
d5316a4fbb

Allow for no mesh key to be set.

Fixes #14928

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2025-02-06 10:53:08 -05:00
Mike O'Driscoll
9a9ce12a3e cmd/derper: close setec after use (#14929)
Since dynamic reload of setec is not supported
in derper at this time, close the server after
the secret is loaded.

Updates tailscale/corp#25756

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2025-02-06 10:52:42 -05:00
Jonathan Nobels
1bf4c6481a safesocket: add ability for Darwin clients to set explicit credentials (#14702)
updates tailscale/corp#25687

The darwin appstore and standalone clients now support XPC and the keychain for passing user credentials securely between the gui process and an NEVPNExtension hosted tailscaled. Clients that can communicate directly with the network extension, via XPC or the keychain, are now expected to call SetCredentials and supply credentials explicitly, fixing issues with the cli breaking if the current user cannot read the contents of /Library/Tailscale due to group membership restrictions. This matches how those clients source and supply credentials to the localAPI http client.

Non-platform-specific code that has traditionally been in the client is moved to safesocket.

/Libraray/Tailscaled/sameuserproof has its permissions changed to that it's readably only by users in the admin group. This restricts standalone CLI access for and direct use of localAPI to admins.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2025-02-06 09:51:00 -05:00
Brad Fitzpatrick
05ac21ebe4 all: use new LocalAPI client package location
It was moved in f57fa3cbc3.

Updates tailscale/corp#22748

Change-Id: I19f965e6bded1d4c919310aa5b864f2de0cd6220
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-05 14:41:42 -08:00
Percy Wegmann
8ecce0e98d client: add missing localclient aliases (#14921)
localclient_aliases.go was missing some package level functions from client/local.
This adds them.

Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-05 16:06:20 -05:00
Percy Wegmann
f57fa3cbc3 client,localclient: move localclient.go to client/local package
Updates tailscale/corp#22748

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-05 12:39:52 -06:00
Percy Wegmann
3f2bec5f64 ssh: don't use -l option for shells on OpenBSD
Shells on OpenBSD don't support the -l option. This means that when
handling SSH in-process, we can't give the user a login shell, but this
change at least allows connecting at all.

Updates #13338

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-05 12:39:03 -06:00
Nick Khyl
0e6d99cc36 docs/windows/policy: remove an extra closing >
Something I accidentally added in #14217.
It doesn't seem to impact Intune or the Administrative Templates MMC extension,
but it should still be fixed.

Updates #cleanup

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-05 12:14:41 -06:00
Percy Wegmann
8287842269 ssh: refactor OS names into constants
Updates #13338

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-05 11:46:32 -06:00
Percy Wegmann
e4bee94857 ssh: don't use -l option for shells on FreeBSD
Shells on FreeBSD don't support the -l option. This means that when
handling SSH in-process, we can't give the user a login shell, but this
change at least allows connecting at all.

Updates #13338

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-02-05 11:46:32 -06:00
Mike O'Driscoll
e6e00012b2 cmd/derper: remove logging of mesh key (#14915)
A previous PR accidentally logged the key as part
of an error. Remove logging of the key.

Add log print for Setec store steup.

Updates tailscale/corp#25756

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2025-02-05 11:36:05 -05:00
Mike O'Driscoll
d5316a4fbb cmd/derper: add setec secret support (#14890)
Add setec secret support for derper.
Support dev mode via env var, and setec via secrets URL.

For backwards compatibility use setec load from file also.

Updates tailscale/corp#25756

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2025-02-05 10:41:18 -05:00
Andrew Lytvynov
e19c01f5b3 clientupdate: refuse to update in tsnet binaries (#14911)
When running via tsnet, c2n will be hooked up so requests to update can
reach the node. But it will then apply whatever OS-specific update
function, upgrading the local tailscaled instead.

We can't update tsnet automatically, so refuse it.

Fixes #14892

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2025-02-04 15:51:03 -08:00
Nick Khyl
9726e1f208 ipn/{ipnserver,localapi},tsnet: use ipnauth.Self as the actor in tsnet localapi handlers
With #14843 merged, (*localapi.Handler).servePrefs() now requires a non-nil actor,
and other places may soon require it as well.

In this PR, we update localapi.NewHandler with a new required parameter for the actor.
We then update tsnet to use ipnauth.Self.

We also rearrange the code in (*ipnserver.Server).serveHTTP() to pass the actor via Handler's
constructor instead of the field.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-04 16:37:30 -06:00
Joe Tsai
0b7087c401 logpolicy: expose MaxBufferSize and MaxUploadSize options (#14903)
Updates tailscale/corp#26342

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2025-02-04 12:51:27 -08:00
Nick Khyl
00fe8845b1 ipn/{ipnauth,ipnlocal,ipnserver}: move the AlwaysOn policy check from ipnserver to ipnauth
In this PR, we move the code that checks the AlwaysOn policy from ipnserver.actor to ipnauth.
It is intended to be used by ipnauth.Actor implementations, and we temporarily make it exported
while these implementations reside in ipnserver and in corp. We'll unexport it later.

We also update [ipnauth.Actor.CheckProfileAccess] to accept an auditLogger, which is called
to write details about the action to the audit log when required by the policy, and update
LocalBackend.EditPrefsAs to use an auditLogger that writes to the regular backend log.

Updates tailscale/corp#26146

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-04 14:36:01 -06:00
Irbe Krumina
5ef934b62d cmd/k8s-operator: reinstate HA Ingress reconciler (#14887)
This change:

- reinstates the HA Ingress controller that was disabled for 1.80 release

- fixes the API calls to manage VIPServices as the API was changed

- triggers the HA Ingress reconciler on ProxyGroup changes

Updates tailscale/tailscale#24795

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-02-04 13:09:43 +00:00
Jordan Whited
cfe578870d derp: tcp-write-timeout=0 should disable write deadline (#14895)
Updates tailscale/corp#26316

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2025-02-03 15:14:16 -08:00
James Tucker
80a100b3cb net/netmon: add extra panic guard around ParseRIB
We once again have a report of a panic from ParseRIB. This panic guard
should probably remain permanent.

Updates #14201

This reverts commit de9d4b2f88.

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-03 12:35:35 -08:00
Adrian Dewhurst
97c4c0ecf0 ipn/ipnlocal: add VIP service IPs to localnets
Without adding this, the packet filter rejects traffic to VIP service
addresses before checking the filters sent in the netmap.

Fixes tailscale/corp#26241

Change-Id: Idd54448048e9b786cf4873fd33b3b21e03d3ad4c
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2025-02-03 15:34:19 -05:00
Adrian Dewhurst
600f25dac9 tailcfg: add JSON unmarshal helper for view of node/peer capabilities
Many places that need to work with node/peer capabilities end up with a
something-View and need to either reimplement the helper code or make an
expensive copy. We have the machinery to easily handle this now.

Updates #cleanup

Change-Id: Ic3f55be329f0fc6c178de26b34359d0e8c6ca5fc
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2025-02-03 14:49:11 -05:00
Brad Fitzpatrick
95e2353294 wgengine/wgcfg/nmcfg: coalesce, limit some debug logs
Updates #14881

Change-Id: I708d29244fe901ab037203a5d7c2cae3c77e4c78
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-03 10:36:36 -08:00
James Tucker
10fe10ea10 derp/derphttp,ipn/localapi,net/captivedetection: add cache resistance to captive portal detection
Observed on some airlines (British Airways, WestJet), Squid is
configured to cache and transform these results, which is disruptive.
The server and client should both actively request that this is not done
by setting Cache-Control headers.

Send a timestamp parameter to further work against caches that do not
respect the cache-control headers.

Updates #14856

Signed-off-by: James Tucker <james@tailscale.com>
2025-02-03 10:15:26 -08:00
Nick Khyl
17ca2b7721 cmd/tailscale/cli: update tailscale down to accept an optional --reason
If specified, the reason is sent via the LocalAPI for auditing purposes.

Updates tailscale/corp#26146

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-03 11:07:55 -06:00
Brad Fitzpatrick
496347c724 go.mod: bump inetaf/tcpproxy
To fix a logging crash.

Updates tailscale/corp#20503

Change-Id: I1beafe34afeb577aaaf6800a408faf6454b16912
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-02-03 09:06:30 -08:00
Nick Khyl
d832467461 client/tailscale,ipn/ipn{local,server},util/syspolicy: implement the AlwaysOn.OverrideWithReason policy setting
In this PR, we update client/tailscale.LocalClient to allow sending requests with an optional X-Tailscale-Reason
header. We then update ipn/ipnserver.{actor,Server} to retrieve this reason, if specified, and use it to determine
whether ipnauth.Disconnect is allowed when the AlwaysOn.OverrideWithReason policy setting is enabled.
For now, we log the reason, along with the profile and OS username, to the backend log.

Finally, we update LocalBackend to remember when a disconnect was permitted and do not reconnect automatically
unless the policy changes.

Updates tailscale/corp#26146

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-01 13:34:45 -06:00
Nick Khyl
2c02f712d1 util/syspolicy/internal/metrics: replace dots with underscores for metric names
Dots are not allowed in metric names and cause panics. Since we use dots in names like
AlwaysOn.OverrideWithReason, let's replace them with underscores. We don’t want to use
setting.KeyPathSeparator here just yet to make it fully hierarchical, but we will decide as
we progress on the (experimental) AlwaysOn.* policy settings.

tailscale/corp#26146

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-02-01 13:10:42 -06:00
Nick Khyl
a0537dc027 ipn/ipnlocal: fix a panic in setPrefsLockedOnEntry when cc is nil
The AlwaysOn policy can be applied by (*LocalBackend).applySysPolicy, flipping WantRunning from false to true
before (*LocalBackend).Start() has been called for the first time and set a control client in b.cc. This results in a nil
pointer dereference and a panic when setPrefsLockedOnEntry applies the change and calls controlclient.Client.Login().

In this PR, we fix it by only doing a login if b.cc has been set.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31 18:41:02 -06:00
Percy Wegmann
2e95313b8b ssh,tempfork/gliderlabs/ssh: replace github.com/tailscale/golang-x-crypto/ssh with golang.org/x/crypto/ssh
The upstream crypto package now supports sending banners at any time during
authentication, so the Tailscale fork of crypto/ssh is no longer necessary.

github.com/tailscale/golang-x-crypto is still needed for some custom ACME
autocert functionality.

tempfork/gliderlabs is still necessary because of a few other customizations,
mostly related to TTY handling.

Originally implemented in 46fd4e58a2,
which was reverted in b60f6b849a to
keep the change out of v1.80.

Updates #8593

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-01-31 16:36:39 -06:00
Nick Khyl
0a51bbc765 ipn/ipnauth,util/syspolicy: improve comments
Updates #cleanup
Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31 11:33:13 -06:00
Nick Khyl
02ad21717f ipn/ipn{auth,server,local}: initial support for the always-on mode
In this PR, we update LocalBackend to set WantRunning=true when applying policy settings
to the current profile's prefs, if the "always-on" mode is enabled.

We also implement a new (*LocalBackend).EditPrefsAs() method, which is like EditPrefs
but accepts an actor (e.g., a LocalAPI client's identity) that initiated the change.
If WantRunning is being set to false, the new EditPrefsAs method checks whether the actor
has ipnauth.Disconnect access to the profile and propagates an error if they do not.

Finally, we update (*ipnserver.actor).CheckProfileAccess to allow a disconnect
only if the "always-on" mode is not enabled by the AlwaysOn policy setting.

This is not a comprehensive solution to the "always-on" mode across platforms,
as instead of disconnecting a user could achieve the same effect by creating
a new empty profile, initiating a reauth, or by deleting the profile.
These are the things we should address in future PRs.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31 10:22:20 -06:00
Nick Khyl
535a3dbebd ipn/ipnauth: implement an Actor representing tailscaled itself
Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31 10:22:20 -06:00
Nick Khyl
081595de63 ipn/{ipnauth, ipnserver}: extend the ipnauth.Actor interface with a CheckProfileAccess method
The implementations define it to verify whether the actor has the requested access to a login profile.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-31 10:22:20 -06:00
Nick Khyl
4e7f4086b2 ipn: generate LoginProfileView and use it instead of *LoginProfile where appropriate
Conventionally, we use views (e.g., ipn.PrefsView, tailcfg.NodeView, etc.) when
dealing with structs that shouldn't be mutated. However, ipn.LoginProfile has been
an exception so far, with a mix of passing and returning LoginProfile by reference
(allowing accidental mutations) and by value (which is wasteful, given its
current size of 192 bytes).

In this PR, we generate an ipn.LoginProfileView and use it instead of passing/returning
LoginProfiles by mutable reference or copying them when passing/returning by value.
Now, LoginProfiles can only be mutated by (*profileManager).setProfilePrefs.

Updates #14823

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-01-30 18:12:54 -06:00
Brad Fitzpatrick
7d5fe13d27 types/views: make SliceEqualAnyOrder also do short slice optimization
SliceEqualAnyOrderFunc had an optimization missing from SliceEqualAnyOrder.

Now they share the same code and both have the optimization.

Updates #14593

Change-Id: I550726e0964fc4006e77bb44addc67be989c131c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-01-30 22:29:35 +00:00
Andrea Gottardo
8ee72cd33c cli/funnel: fix comment typo (#14840)
Updates #cleanup

Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2025-01-30 14:21:32 -08:00
Andrea Gottardo
08dd4994d0 VERSION.txt: this is v1.81.0 (#14838)
Signed-off-by: Andrea Gottardo <andrea@gottardo.me>
2025-01-30 13:04:29 -08:00
236 changed files with 28429 additions and 2486 deletions

View File

@@ -55,7 +55,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -66,7 +66,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -80,4 +80,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f6091c0113d1dcf9b98e269ee48e8a7e51b7bdd4 # v3.28.5
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9

View File

@@ -31,9 +31,9 @@ jobs:
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6.2.0
uses: golangci/golangci-lint-action@2e788936b09dd82dc280e845628a40d2ba6b204c # v6.3.1
with:
version: v1.60
version: v1.64
# Show only new issues if it's a pull request.
only-new-issues: true

View File

@@ -64,7 +64,6 @@ jobs:
matrix:
include:
- goarch: amd64
coverflags: "-coverprofile=/tmp/coverage.out"
- goarch: amd64
buildflags: "-race"
shard: '1/3'
@@ -119,15 +118,10 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ${{matrix.coverflags}} ./... ${{matrix.buildflags}}
run: NOBASHDEBUG=true PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
env:
GOARCH: ${{ matrix.goarch }}
TS_TEST_SHARD: ${{ matrix.shard }}
- name: Publish to coveralls.io
if: matrix.coverflags != '' # only publish results if we've tracked coverage
uses: shogo82148/actions-goveralls@v1
with:
path-to-profile: /tmp/coverage.out
- name: bench all
run: ./tool/go test ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ $(for x in $(git grep -l "^func Benchmark" | xargs dirname | sort | uniq); do echo "./$x"; done)
env:

View File

@@ -27,7 +27,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.23-alpine AS build-env
FROM golang:1.24-alpine AS build-env
WORKDIR /go/src/tailscale

View File

@@ -1 +1 @@
1.79.0
1.81.0

View File

@@ -289,9 +289,11 @@ func (e *AppConnector) updateDomains(domains []string) {
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
e.queue.Add(func() {
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
})
}
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
@@ -310,11 +312,6 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
return
}
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
return
}
var toRemove []netip.Prefix
// If we're storing routes and know e.controlRoutes is a good
@@ -338,9 +335,14 @@ nextRoute:
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
e.queue.Add(func() {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
})
e.controlRoutes = routes
if err := e.storeRoutesLocked(); err != nil {

View File

@@ -8,6 +8,7 @@ import (
"net/netip"
"reflect"
"slices"
"sync/atomic"
"testing"
"time"
@@ -86,6 +87,7 @@ func TestUpdateRoutes(t *testing.T) {
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
a.updateRoutes(routes)
a.Wait(ctx)
slices.SortFunc(rc.Routes(), prefixCompare)
rc.SetRoutes(slices.Compact(rc.Routes()))
@@ -105,6 +107,7 @@ func TestUpdateRoutes(t *testing.T) {
}
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
ctx := context.Background()
for _, shouldStore := range []bool{false, true} {
rc := &appctest.RouteCollector{}
var a *AppConnector
@@ -117,6 +120,7 @@ func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
a.updateRoutes(routes)
a.Wait(ctx)
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes)
@@ -636,3 +640,57 @@ func TestMetricBucketsAreSorted(t *testing.T) {
t.Errorf("metricStoreRoutesNBuckets must be in order")
}
}
// TestUpdateRoutesDeadlock is a regression test for a deadlock in
// LocalBackend<->AppConnector interaction. When using real LocalBackend as the
// routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
// back into AppConnector via authReconfig. If everything is called
// synchronously, this results in a deadlock on AppConnector.mu.
func TestUpdateRoutesDeadlock(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc, &RouteInfo{}, fakeStoreRoutes)
advertiseCalled := new(atomic.Bool)
unadvertiseCalled := new(atomic.Bool)
rc.AdvertiseCallback = func() {
// Call something that requires a.mu to be held.
a.DomainRoutes()
advertiseCalled.Store(true)
}
rc.UnadvertiseCallback = func() {
// Call something that requires a.mu to be held.
a.DomainRoutes()
unadvertiseCalled.Store(true)
}
a.updateDomains([]string{"example.com"})
a.Wait(ctx)
// Trigger rc.AdveriseRoute.
a.updateRoutes(
[]netip.Prefix{
netip.MustParsePrefix("127.0.0.1/32"),
netip.MustParsePrefix("127.0.0.2/32"),
},
)
a.Wait(ctx)
// Trigger rc.UnadveriseRoute.
a.updateRoutes(
[]netip.Prefix{
netip.MustParsePrefix("127.0.0.1/32"),
},
)
a.Wait(ctx)
if !advertiseCalled.Load() {
t.Error("AdvertiseRoute was not called")
}
if !unadvertiseCalled.Load() {
t.Error("UnadvertiseRoute was not called")
}
if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
t.Fatalf("got %v, want %v", rc.Routes(), want)
}
}

View File

@@ -11,12 +11,22 @@ import (
// RouteCollector is a test helper that collects the list of routes advertised
type RouteCollector struct {
// AdvertiseCallback (optional) is called synchronously from
// AdvertiseRoute.
AdvertiseCallback func()
// UnadvertiseCallback (optional) is called synchronously from
// UnadvertiseRoute.
UnadvertiseCallback func()
routes []netip.Prefix
removedRoutes []netip.Prefix
}
func (rc *RouteCollector) AdvertiseRoute(pfx ...netip.Prefix) error {
rc.routes = append(rc.routes, pfx...)
if rc.AdvertiseCallback != nil {
rc.AdvertiseCallback()
}
return nil
}
@@ -30,6 +40,9 @@ func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error {
rc.removedRoutes = append(rc.removedRoutes, r)
}
}
if rc.UnadvertiseCallback != nil {
rc.UnadvertiseCallback()
}
return nil
}

View File

@@ -3,13 +3,15 @@
//go:build go1.22
package tailscale
// Package local contains a Go client for the Tailscale LocalAPI.
package local
import (
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -43,11 +45,11 @@ import (
"tailscale.com/util/syspolicy/setting"
)
// defaultLocalClient is the default LocalClient when using the legacy
// defaultClient is the default Client when using the legacy
// package-level functions.
var defaultLocalClient LocalClient
var defaultClient Client
// LocalClient is a client to Tailscale's "LocalAPI", communicating with the
// Client is a client to Tailscale's "LocalAPI", communicating with the
// Tailscale daemon on the local machine. Its API is not necessarily stable and
// subject to changes between releases. Some API calls have stricter
// compatibility guarantees, once they've been widely adopted. See method docs
@@ -57,7 +59,7 @@ var defaultLocalClient LocalClient
//
// Any exported fields should be set before using methods on the type
// and not changed thereafter.
type LocalClient struct {
type Client struct {
// Dial optionally specifies an alternate func that connects to the local
// machine's tailscaled or equivalent. If nil, a default is used.
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
@@ -91,21 +93,21 @@ type LocalClient struct {
tsClientOnce sync.Once
}
func (lc *LocalClient) socket() string {
func (lc *Client) socket() string {
if lc.Socket != "" {
return lc.Socket
}
return paths.DefaultTailscaledSocket()
}
func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
func (lc *Client) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
if lc.Dial != nil {
return lc.Dial
}
return lc.defaultDialer
}
func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
func (lc *Client) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
@@ -131,7 +133,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
func (lc *Client) DoLocalRequest(req *http.Request) (*http.Response, error) {
req.Header.Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
lc.tsClientOnce.Do(func() {
lc.tsClient = &http.Client{
@@ -148,7 +150,7 @@ func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error)
return lc.tsClient.Do(req)
}
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
func (lc *Client) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := lc.DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != envknob.IPCVersion() && onVersionMismatch != nil {
@@ -237,12 +239,17 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, nil)
func (lc *Client) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
var headers http.Header
if reason := apitype.RequestReasonKey.Value(ctx); reason != "" {
reasonBase64 := base64.StdEncoding.EncodeToString([]byte(reason))
headers = http.Header{apitype.RequestReasonHeader: {reasonBase64}}
}
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, headers)
return slurp, err
}
func (lc *LocalClient) sendWithHeaders(
func (lc *Client) sendWithHeaders(
ctx context.Context,
method,
path string,
@@ -281,15 +288,15 @@ type httpStatusError struct {
HTTPStatus int
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
func (lc *Client) get200(ctx context.Context, path string) ([]byte, error) {
return lc.send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
//
// Deprecated: use LocalClient.WhoIs.
// Deprecated: use Client.WhoIs.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
return defaultLocalClient.WhoIs(ctx, remoteAddr)
return defaultClient.WhoIs(ctx, remoteAddr)
}
func decodeJSON[T any](b []byte) (ret T, err error) {
@@ -307,7 +314,7 @@ func decodeJSON[T any](b []byte) (ret T, err error) {
// For connections proxied by tailscaled, this looks up the owner of the given
// address as TCP first, falling back to UDP; if you want to only check a
// specific address family, use WhoIsProto.
func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -324,7 +331,7 @@ var ErrPeerNotFound = errors.New("peer not found")
// WhoIsNodeKey returns the owner of the given wireguard public key.
//
// If not found, the error is ErrPeerNotFound.
func (lc *LocalClient) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(key.String()))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -339,7 +346,7 @@ func (lc *LocalClient) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*a
// IP:port, for the given protocol (tcp or udp).
//
// If not found, the error is ErrPeerNotFound.
func (lc *LocalClient) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?proto="+url.QueryEscape(proto)+"&addr="+url.QueryEscape(remoteAddr))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -351,19 +358,19 @@ func (lc *LocalClient) WhoIsProto(ctx context.Context, proto, remoteAddr string)
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func (lc *LocalClient) Goroutines(ctx context.Context) ([]byte, error) {
func (lc *Client) Goroutines(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
func (lc *Client) DaemonMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/metrics")
}
// UserMetrics returns the user metrics in
// the Prometheus text exposition format.
func (lc *LocalClient) UserMetrics(ctx context.Context) ([]byte, error) {
func (lc *Client) UserMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/usermetrics")
}
@@ -372,7 +379,7 @@ func (lc *LocalClient) UserMetrics(ctx context.Context) ([]byte, error) {
// metric is created and initialized to delta.
//
// IncrementCounter does not support gauge metrics or negative delta values.
func (lc *LocalClient) IncrementCounter(ctx context.Context, name string, delta int) error {
func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) error {
type metricUpdate struct {
Name string `json:"name"`
Type string `json:"type"`
@@ -391,7 +398,7 @@ func (lc *LocalClient) IncrementCounter(ctx context.Context, name string, delta
// TailDaemonLogs returns a stream the Tailscale daemon's logs as they arrive.
// Close the context to stop the stream.
func (lc *LocalClient) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/logtap", nil)
if err != nil {
return nil, err
@@ -407,7 +414,7 @@ func (lc *LocalClient) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
}
// Pprof returns a pprof profile of the Tailscale daemon.
func (lc *LocalClient) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
func (lc *Client) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
@@ -440,7 +447,7 @@ type BugReportOpts struct {
//
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
func (lc *Client) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
qparams := make(url.Values)
if opts.Note != "" {
qparams.Set("note", opts.Note)
@@ -485,13 +492,13 @@ func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts
//
// This is the same as calling BugReportWithOpts and only specifying the Note
// field.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
func (lc *Client) BugReport(ctx context.Context, note string) (string, error) {
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
func (lc *Client) DebugAction(ctx context.Context, action string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
@@ -502,7 +509,7 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
// DebugActionBody invokes a debug action with a body parameter, such as
// "debug-force-prefer-derp".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
func (lc *Client) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, rbody)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
@@ -512,7 +519,7 @@ func (lc *LocalClient) DebugActionBody(ctx context.Context, action string, rbody
// DebugResultJSON invokes a debug action and returns its result as something JSON-able.
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) {
func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
@@ -555,7 +562,7 @@ type DebugPortmapOpts struct {
// process.
//
// opts can be nil; if so, default values will be used.
func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
vals := make(url.Values)
if opts == nil {
opts = &DebugPortmapOpts{}
@@ -590,7 +597,7 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts)
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/dev-set-state-store?"+(url.Values{
"key": {key},
"value": {value},
@@ -604,7 +611,7 @@ func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value strin
// SetComponentDebugLogging sets component's debug logging enabled for
// the provided duration. If the duration is in the past, the debug logging
// is disabled.
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
@@ -625,25 +632,25 @@ func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component s
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
return defaultClient.Status(ctx)
}
// Status returns the Tailscale daemon's status.
func (lc *LocalClient) Status(ctx context.Context) (*ipnstate.Status, error) {
func (lc *Client) Status(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.StatusWithoutPeers(ctx)
return defaultClient.StatusWithoutPeers(ctx)
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func (lc *LocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
func (lc *Client) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "?peers=false")
}
func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
func (lc *Client) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := lc.get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
@@ -654,7 +661,7 @@ func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstat
// IDToken is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
func (lc *Client) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
if err != nil {
return nil, err
@@ -666,14 +673,14 @@ func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenR
// received by the Tailscale daemon in its staging/cache directory but not yet
// transferred by the user's CLI or GUI client and written to a user's home
// directory somewhere.
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
func (lc *Client) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
return lc.AwaitWaitingFiles(ctx, 0)
}
// AwaitWaitingFiles is like WaitingFiles but takes a duration to await for an answer.
// If the duration is 0, it will return immediately. The duration is respected at second
// granularity only. If no files are available, it returns (nil, nil).
func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
func (lc *Client) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
path := "/localapi/v0/files/?waitsec=" + fmt.Sprint(int(d.Seconds()))
body, err := lc.get200(ctx, path)
if err != nil {
@@ -682,12 +689,12 @@ func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) (
return decodeJSON[[]apitype.WaitingFile](body)
}
func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
func (lc *Client) DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
func (lc *Client) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
@@ -708,7 +715,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return res.Body, res.ContentLength, nil
}
func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
func (lc *Client) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := lc.get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
@@ -720,7 +727,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
@@ -743,7 +750,7 @@ func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID
// CheckIPForwarding asks the local Tailscale daemon whether it looks like the
// machine is properly configured to forward IP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
func (lc *Client) CheckIPForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
@@ -763,7 +770,7 @@ func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
// CheckUDPGROForwarding asks the local Tailscale daemon whether it looks like
// the machine is optimally configured to forward UDP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-udp-gro-forwarding")
if err != nil {
return err
@@ -784,7 +791,7 @@ func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
// node. This can be done to improve performance of tailnet nodes acting as exit
// nodes or subnet routers.
// See https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
func (lc *LocalClient) SetUDPGROForwarding(ctx context.Context) error {
func (lc *Client) SetUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/set-udp-gro-forwarding")
if err != nil {
return err
@@ -807,12 +814,12 @@ func (lc *LocalClient) SetUDPGROForwarding(ctx context.Context) error {
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary.
func (lc *LocalClient) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
func (lc *Client) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, jsonBody(p))
return err
}
func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
func (lc *Client) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := lc.get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
@@ -824,7 +831,12 @@ func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return &p, nil
}
func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
// EditPrefs updates the [ipn.Prefs] of the current Tailscale profile, applying the changes in mp.
// It returns an error if the changes cannot be applied, such as due to the caller's access rights
// or a policy restriction. An optional reason or justification for the request can be
// provided as a context value using [apitype.RequestReasonKey]. If permitted by policy,
// access may be granted, and the reason will be logged for auditing purposes.
func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, jsonBody(mp))
if err != nil {
return nil, err
@@ -833,7 +845,7 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
}
// GetEffectivePolicy returns the effective policy for the specified scope.
func (lc *LocalClient) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
@@ -847,7 +859,7 @@ func (lc *LocalClient) GetEffectivePolicy(ctx context.Context, scope setting.Pol
// ReloadEffectivePolicy reloads the effective policy for the specified scope
// by reading and merging policy settings from all applicable policy sources.
func (lc *LocalClient) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
@@ -861,7 +873,7 @@ func (lc *LocalClient) ReloadEffectivePolicy(ctx context.Context, scope setting.
// GetDNSOSConfig returns the system DNS configuration for the current device.
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
if err != nil {
return nil, err
@@ -876,7 +888,7 @@ func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig
// QueryDNS executes a DNS query for a name (`google.com.`) and query type (`CNAME`).
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
// (often just one, but can be more if we raced multiple resolvers).
func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
if err != nil {
return nil, nil, err
@@ -889,20 +901,20 @@ func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType stri
}
// StartLoginInteractive starts an interactive login.
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
func (lc *Client) StartLoginInteractive(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
return err
}
// Start applies the configuration specified in opts, and starts the
// state machine.
func (lc *LocalClient) Start(ctx context.Context, opts ipn.Options) error {
func (lc *Client) Start(ctx context.Context, opts ipn.Options) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/start", http.StatusNoContent, jsonBody(opts))
return err
}
// Logout logs out the current node.
func (lc *LocalClient) Logout(ctx context.Context) error {
func (lc *Client) Logout(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
@@ -921,7 +933,7 @@ func (lc *LocalClient) Logout(ctx context.Context) error {
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
@@ -935,7 +947,7 @@ func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
// tailscaled), a FQDN, or an IP address.
//
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
return lc.UserDial(ctx, "tcp", host, port)
}
@@ -946,7 +958,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
//
// The ctx is only used for the duration of the call, not the lifetime of the
// net.Conn.
func (lc *LocalClient) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
func (lc *Client) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
@@ -997,7 +1009,7 @@ func (lc *LocalClient) UserDial(ctx context.Context, network, host string, port
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
@@ -1013,9 +1025,9 @@ func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, er
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use LocalClient.CertPair.
// Deprecated: use Client.CertPair.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return defaultLocalClient.CertPair(ctx, domain)
return defaultClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
@@ -1023,7 +1035,7 @@ func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err e
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return lc.CertPairWithValidity(ctx, domain, 0)
}
@@ -1036,7 +1048,7 @@ func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, ke
// valid, but for less than minValidity, it will be synchronously renewed.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
if err != nil {
return nil, nil, err
@@ -1062,9 +1074,9 @@ func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string,
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// Deprecated: use LocalClient.GetCertificate.
// Deprecated: use Client.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultLocalClient.GetCertificate(hi)
return defaultClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
@@ -1075,7 +1087,7 @@ func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
// tls.Config.GetCertificate.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
@@ -1101,13 +1113,13 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
// Deprecated: use Client.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
return defaultClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", false
@@ -1135,7 +1147,7 @@ type PingOpts struct {
// Ping sends a ping of the provided type to the provided IP and waits
// for its response. The opts type specifies additional options.
func (lc *LocalClient) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType, opts PingOpts) (*ipnstate.PingResult, error) {
func (lc *Client) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType, opts PingOpts) (*ipnstate.PingResult, error) {
v := url.Values{}
v.Set("ip", ip.String())
v.Set("size", strconv.Itoa(opts.Size))
@@ -1149,12 +1161,12 @@ func (lc *LocalClient) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype
// Ping sends a ping of the provided type to the provided IP and waits
// for its response.
func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
}
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
@@ -1165,7 +1177,7 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
// NetworkLockInit initializes the tailnet key authority.
//
// TODO(tom): Plumb through disablement secrets.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type initRequest struct {
Keys []tka.Key
@@ -1186,7 +1198,7 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
// enable unattended bringup in the locked tailnet.
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
@@ -1209,7 +1221,7 @@ func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
@@ -1228,7 +1240,7 @@ func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKey
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
@@ -1246,7 +1258,7 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
if err != nil {
return nil, fmt.Errorf("error: %w", err)
@@ -1255,7 +1267,7 @@ func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatyp
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
v.Set("limit", fmt.Sprint(maxEntries))
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
@@ -1266,7 +1278,7 @@ func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ip
}
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
// This endpoint expects an empty JSON stanza as the payload.
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
@@ -1281,7 +1293,7 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
@@ -1295,7 +1307,7 @@ func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url
}
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
@@ -1310,7 +1322,7 @@ func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys
}
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
r := bytes.NewReader(aum.Serialize())
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
if err != nil {
@@ -1321,7 +1333,7 @@ func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
r := bytes.NewReader(aum.Serialize())
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
if err != nil {
@@ -1332,7 +1344,7 @@ func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
@@ -1347,7 +1359,7 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
// to another replica whilst there is still some grace period for the existing connections to terminate.
func (lc *LocalClient) DisconnectControl(ctx context.Context) error {
func (lc *Client) DisconnectControl(ctx context.Context) error {
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
if err != nil {
return fmt.Errorf("error disconnecting control: %w", err)
@@ -1356,7 +1368,7 @@ func (lc *LocalClient) DisconnectControl(ctx context.Context) error {
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
@@ -1366,7 +1378,7 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
@@ -1441,7 +1453,7 @@ func (r jsonReader) Read(p []byte) (n int, err error) {
}
// ProfileStatus returns the current profile and the list of all profiles.
func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProfile, all []ipn.LoginProfile, err error) {
func (lc *Client) ProfileStatus(ctx context.Context) (current ipn.LoginProfile, all []ipn.LoginProfile, err error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/profiles/current", 200, nil)
if err != nil {
return
@@ -1459,7 +1471,7 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
}
// ReloadConfig reloads the config file, if possible.
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
func (lc *Client) ReloadConfig(ctx context.Context) (ok bool, err error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
if err != nil {
return
@@ -1477,13 +1489,13 @@ func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
// profile is not assigned an ID until it is persisted after a successful login.
// In order to login to the new profile, the user must call LoginInteractive.
func (lc *LocalClient) SwitchToEmptyProfile(ctx context.Context) error {
func (lc *Client) SwitchToEmptyProfile(ctx context.Context) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/profiles/", http.StatusCreated, nil)
return err
}
// SwitchProfile switches to the given profile.
func (lc *LocalClient) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
func (lc *Client) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/profiles/"+url.PathEscape(string(profile)), 204, nil)
return err
}
@@ -1491,7 +1503,7 @@ func (lc *LocalClient) SwitchProfile(ctx context.Context, profile ipn.ProfileID)
// DeleteProfile removes the profile with the given ID.
// If the profile is the current profile, an empty profile
// will be selected as if SwitchToEmptyProfile was called.
func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
func (lc *Client) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
return err
}
@@ -1508,7 +1520,7 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
// to block until the feature has been enabled.
//
// 2023-08-09: Valid feature values are "serve" and "funnel".
func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
func (lc *Client) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
v := url.Values{"feature": {feature}}
body, err := lc.send(ctx, "POST", "/localapi/v0/query-feature?"+v.Encode(), 200, nil)
if err != nil {
@@ -1517,7 +1529,7 @@ func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailc
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
}
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
func (lc *Client) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
v := url.Values{"region": {regionIDOrCode}}
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
if err != nil {
@@ -1527,7 +1539,7 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin
}
// DebugPacketFilterRules returns the packet filter rules for the current device.
func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
func (lc *Client) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
@@ -1538,7 +1550,7 @@ func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.Fi
// DebugSetExpireIn marks the current node key to expire in d.
//
// This is meant primarily for debug and testing.
func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
v := url.Values{"expiry": {fmt.Sprint(time.Now().Add(d).Unix())}}
_, err := lc.send(ctx, "POST", "/localapi/v0/set-expiry-sooner?"+v.Encode(), 200, nil)
return err
@@ -1548,7 +1560,7 @@ func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) er
//
// The provided context does not determine the lifetime of the
// returned io.ReadCloser.
func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-capture", nil)
if err != nil {
return nil, err
@@ -1574,7 +1586,7 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
// resources.
//
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
nil)
@@ -1600,7 +1612,7 @@ func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt)
// CheckUpdate returns a tailcfg.ClientVersion indicating whether or not an update is available
// to be installed via the LocalAPI. In case the LocalAPI can't install updates, it returns a
// ClientVersion that says that we are up to date.
func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
func (lc *Client) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
body, err := lc.get200(ctx, "/localapi/v0/update/check")
if err != nil {
return nil, err
@@ -1616,7 +1628,7 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
// 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 {
func (lc *Client) 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
}
@@ -1624,7 +1636,7 @@ func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// Taildrive know to use the file server running in the GUI app.
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
func (lc *Client) DriveSetServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err
}
@@ -1632,14 +1644,14 @@ func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) erro
// 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 {
func (lc *Client) DriveShareSet(ctx context.Context, share *drive.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
return err
}
// 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 {
func (lc *Client) DriveShareRemove(ctx context.Context, name string) error {
_, err := lc.send(
ctx,
"DELETE",
@@ -1650,7 +1662,7 @@ func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error
}
// DriveShareRename renames the share from old to new name.
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
func (lc *Client) DriveShareRename(ctx context.Context, oldName, newName string) error {
_, err := lc.send(
ctx,
"POST",
@@ -1662,7 +1674,7 @@ func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName st
// DriveShareList returns the list of shares that drive is currently serving
// to remote nodes.
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
func (lc *Client) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
if err != nil {
return nil, err
@@ -1673,7 +1685,7 @@ func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, erro
}
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
// It's returned by LocalClient.WatchIPNBus.
// It's returned by Client.WatchIPNBus.
//
// It must be closed when done.
type IPNBusWatcher struct {
@@ -1697,7 +1709,7 @@ func (w *IPNBusWatcher) Close() error {
}
// Next returns the next ipn.Notify from the stream.
// If the context from LocalClient.WatchIPNBus is done, that error is returned.
// If the context from Client.WatchIPNBus is done, that error is returned.
func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
var n ipn.Notify
if err := w.dec.Decode(&n); err != nil {
@@ -1710,7 +1722,7 @@ func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
}
// SuggestExitNode requests an exit node suggestion and returns the exit node's details.
func (lc *LocalClient) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
func (lc *Client) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/suggest-exit-node")
if err != nil {
return apitype.ExitNodeSuggestionResponse{}, err

View File

@@ -3,7 +3,7 @@
//go:build go1.19
package tailscale
package local
import (
"context"
@@ -41,7 +41,7 @@ func TestWhoIsPeerNotFound(t *testing.T) {
}))
defer ts.Close()
lc := &LocalClient{
lc := &Client{
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
var std net.Dialer
return std.DialContext(ctx, network, ts.Listener.Addr().(*net.TCPAddr).String())

View File

@@ -26,7 +26,7 @@ import (
"github.com/atotto/clipboard"
dbus "github.com/godbus/dbus/v5"
"github.com/toqueteos/webbrowser"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
@@ -67,7 +67,7 @@ func (menu *Menu) Run() {
type Menu struct {
mu sync.Mutex // protects the entire Menu
lc tailscale.LocalClient
lc local.Client
status *ipnstate.Status
curProfile ipn.LoginProfile
allProfiles []ipn.LoginProfile

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"net/netip"
"net/url"
)
// ACLRow defines a rule that grants access by a set of users or groups to a set
@@ -83,7 +84,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -97,7 +98,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
// Otherwise, try to decode the response.
@@ -126,7 +127,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", url.Values{"details": {"1"}})
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -138,7 +139,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
data := struct {
@@ -146,7 +147,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
Warnings []string `json:"warnings"`
}{}
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
return nil, fmt.Errorf("json.Unmarshal %q: %w", b, err)
}
acl = &ACLHuJSON{
@@ -184,7 +185,7 @@ func (e ACLTestError) Error() string {
}
func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, "", err
@@ -328,7 +329,7 @@ type ACLPreview struct {
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", "preview")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, err
@@ -350,7 +351,7 @@ func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, preview
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
if err = json.Unmarshal(b, &res); err != nil {
return nil, err
@@ -488,7 +489,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
return nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", "validate")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData))
if err != nil {
return nil, err

View File

@@ -7,11 +7,29 @@ package apitype
import (
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/util/ctxkey"
)
// LocalAPIHost is the Host header value used by the LocalAPI.
const LocalAPIHost = "local-tailscaled.sock"
// RequestReasonHeader is the header used to pass justification for a LocalAPI request,
// such as when a user wants to perform an action they don't have permission for,
// and a policy allows it with justification. As of 2025-01-29, it is only used to
// allow a user to disconnect Tailscale when the "always-on" mode is enabled.
//
// The header value is base64-encoded using the standard encoding defined in RFC 4648.
//
// See tailscale/corp#26146.
const RequestReasonHeader = "X-Tailscale-Reason"
// RequestReasonKey is the context key used to pass the request reason
// when making a LocalAPI request via [local.Client].
// It's value is a raw string. An empty string means no reason was provided.
//
// See tailscale/corp#26146.
var RequestReasonKey = ctxkey.New(RequestReasonHeader, "")
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
// In successful whois responses, Node and UserProfile are never nil.
type WhoIsResponse struct {

View File

@@ -131,7 +131,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("devices")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -149,7 +149,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var devices GetDevicesResponse
@@ -188,7 +188,7 @@ func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFiel
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
err = json.Unmarshal(b, &device)
@@ -221,7 +221,7 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error)
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
}
@@ -253,7 +253,7 @@ func (c *Client) SetAuthorized(ctx context.Context, deviceID string, authorized
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
@@ -281,7 +281,7 @@ func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil

View File

@@ -44,7 +44,7 @@ type DNSPreferences struct {
}
func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
path := c.BuildTailnetURL("dns", endpoint)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -57,14 +57,14 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
path := c.BuildTailnetURL("dns", endpoint)
data, err := json.Marshal(&postData)
if err != nil {
return nil, err
@@ -84,7 +84,7 @@ func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData a
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
return b, nil

View File

@@ -11,13 +11,14 @@ import (
"log"
"net/http"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
)
func main() {
var lc local.Client
s := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: tailscale.GetCertificate,
GetCertificate: lc.GetCertificate,
},
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<h1>Hello from Tailscale!</h1> It works.")

View File

@@ -40,7 +40,7 @@ type KeyDeviceCreateCapabilities struct {
// Keys returns the list of keys for the current user.
func (c *Client) Keys(ctx context.Context) ([]string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("keys")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -51,7 +51,7 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var keys struct {
@@ -99,7 +99,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("keys")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
if err != nil {
return "", nil, err
@@ -110,7 +110,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
if resp.StatusCode != http.StatusOK {
return "", nil, handleErrorResponse(b, resp)
return "", nil, HandleErrorResponse(b, resp)
}
var key struct {
@@ -126,7 +126,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
// Key returns the metadata for the given key ID. Currently, capabilities are
// only returned for auth keys, API keys only return general metadata.
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
path := c.BuildTailnetURL("keys", id)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -137,7 +137,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var key Key
@@ -149,7 +149,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
// DeleteKey deletes the key with the given ID.
func (c *Client) DeleteKey(ctx context.Context, id string) error {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
path := c.BuildTailnetURL("keys", id)
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
if err != nil {
return err
@@ -160,7 +160,7 @@ func (c *Client) DeleteKey(ctx context.Context, id string) error {
return err
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"context"
"crypto/tls"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
)
// ErrPeerNotFound is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
var ErrPeerNotFound = local.ErrPeerNotFound
// LocalClient is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type LocalClient = local.Client
// IPNBusWatcher is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type IPNBusWatcher = local.IPNBusWatcher
// BugReportOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type BugReportOpts = local.BugReportOpts
// DebugPortMapOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type DebugPortmapOpts = local.DebugPortmapOpts
// PingOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type PingOpts = local.PingOpts
// GetCertificate is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return local.GetCertificate(hi)
}
// SetVersionMismatchHandler is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
local.SetVersionMismatchHandler(f)
}
// IsAccessDeniedError is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func IsAccessDeniedError(err error) bool {
return local.IsAccessDeniedError(err)
}
// IsPreconditionsFailedError is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func IsPreconditionsFailedError(err error) bool {
return local.IsPreconditionsFailedError(err)
}
// WhoIs is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
return local.WhoIs(ctx, remoteAddr)
}
// Status is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return local.Status(ctx)
}
// StatusWithoutPeers is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return local.StatusWithoutPeers(ctx)
}
// CertPair is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return local.CertPair(ctx, domain)
}
// ExpandSNIName is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return local.ExpandSNIName(ctx, name)
}

View File

@@ -44,7 +44,7 @@ func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, e
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var sr Routes
@@ -84,7 +84,7 @@ func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netip
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var srr *Routes

View File

@@ -9,7 +9,6 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"tailscale.com/util/httpm"
)
@@ -22,7 +21,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.baseURL(), url.PathEscape(string(tailnetID)))
path := c.BuildTailnetURL("tailnet")
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return err
@@ -35,7 +34,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil

View File

@@ -3,11 +3,12 @@
//go:build go1.19
// Package tailscale contains Go clients for the Tailscale LocalAPI and
// Tailscale control plane API.
// Package tailscale contains a Go client for the Tailscale control plane API.
//
// Warning: this package is in development and makes no API compatibility
// promises as of 2022-04-29. It is subject to change at any time.
// This package is only intended for internal and transitional use.
//
// Deprecated: the official control plane client is available at
// tailscale.com/client/tailscale/v2.
package tailscale
import (
@@ -16,13 +17,12 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
)
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
// for now. It was added 2022-04-29 when it was moved to this git repo
// and will be removed when the public API has settled.
//
// TODO(bradfitz): remove this after the we're happy with the public API.
// for now. This package is being replaced by tailscale.com/client/tailscale/v2.
var I_Acknowledge_This_API_Is_Unstable = false
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
@@ -36,6 +36,8 @@ const maxReadSize = 10 << 20
//
// Use NewClient to instantiate one. Exported fields should be set before
// the client is used and not changed thereafter.
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
type Client struct {
// tailnet is the globally unique identifier for a Tailscale network, such
// as "example.com" or "user@gmail.com".
@@ -63,6 +65,46 @@ func (c *Client) httpClient() *http.Client {
return http.DefaultClient
}
// BuildURL builds a url to http(s)://<apiserver>/api/v2/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildURL(devices, 5) with the default server URL would result in
// https://api.tailscale.com/api/v2/devices/5.
func (c *Client) BuildURL(pathElements ...any) string {
elem := make([]string, 1, len(pathElements)+1)
elem[0] = "/api/v2"
var query string
for i, pathElement := range pathElements {
if uv, ok := pathElement.(url.Values); ok && i == len(pathElements)-1 {
query = uv.Encode()
} else {
elem = append(elem, url.PathEscape(fmt.Sprint(pathElement)))
}
}
url := c.baseURL() + path.Join(elem...)
if query != "" {
url += "?" + query
}
return url
}
// BuildTailnetURL builds a url to http(s)://<apiserver>/api/v2/tailnet/<tailnet>/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildTailnetURL(policy, validate) with the default server URL and a tailnet of "example.com"
// would result in https://api.tailscale.com/api/v2/tailnet/example.com/policy/validate.
func (c *Client) BuildTailnetURL(pathElements ...any) string {
allElements := make([]any, 2, len(pathElements)+2)
allElements[0] = "tailnet"
allElements[1] = c.tailnet
allElements = append(allElements, pathElements...)
return c.BuildURL(allElements...)
}
func (c *Client) baseURL() string {
if c.BaseURL != "" {
return c.BaseURL
@@ -98,6 +140,8 @@ func (c *Client) setAuth(r *http.Request) {
// If httpClient is nil, then http.DefaultClient is used.
// "api.tailscale.com" is set as the BaseURL for the returned client
// and can be changed manually by the user.
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
func NewClient(tailnet string, auth AuthMethod) *Client {
return &Client{
tailnet: tailnet,
@@ -148,12 +192,14 @@ func (e ErrResponse) Error() string {
return fmt.Sprintf("Status: %d, Message: %q", e.Status, e.Message)
}
// handleErrorResponse decodes the error message from the server and returns
// HandleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
func handleErrorResponse(b []byte, resp *http.Response) error {
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
func HandleErrorResponse(b []byte, resp *http.Response) error {
var errResp ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return err
return fmt.Errorf("json.Unmarshal %q: %w", b, err)
}
errResp.Status = resp.StatusCode
return errResp

View File

@@ -0,0 +1,86 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"net/url"
"testing"
)
func TestClientBuildURL(t *testing.T) {
c := Client{BaseURL: "http://127.0.0.1:1234"}
for _, tt := range []struct {
desc string
elements []any
want string
}{
{
desc: "single-element",
elements: []any{"devices"},
want: "http://127.0.0.1:1234/api/v2/devices",
},
{
desc: "multiple-elements",
elements: []any{"tailnet", "example.com"},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com",
},
{
desc: "escape-element",
elements: []any{"tailnet", "example dot com?foo=bar"},
want: `http://127.0.0.1:1234/api/v2/tailnet/example%20dot%20com%3Ffoo=bar`,
},
{
desc: "url.Values",
elements: []any{"tailnet", "example.com", "acl", url.Values{"details": {"1"}}},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/acl?details=1`,
},
} {
t.Run(tt.desc, func(t *testing.T) {
got := c.BuildURL(tt.elements...)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}
func TestClientBuildTailnetURL(t *testing.T) {
c := Client{
BaseURL: "http://127.0.0.1:1234",
tailnet: "example.com",
}
for _, tt := range []struct {
desc string
elements []any
want string
}{
{
desc: "single-element",
elements: []any{"devices"},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com/devices",
},
{
desc: "multiple-elements",
elements: []any{"devices", 123},
want: "http://127.0.0.1:1234/api/v2/tailnet/example.com/devices/123",
},
{
desc: "escape-element",
elements: []any{"foo bar?baz=qux"},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/foo%20bar%3Fbaz=qux`,
},
{
desc: "url.Values",
elements: []any{"acl", url.Values{"details": {"1"}}},
want: `http://127.0.0.1:1234/api/v2/tailnet/example.com/acl?details=1`,
},
} {
t.Run(tt.desc, func(t *testing.T) {
got := c.BuildTailnetURL(tt.elements...)
if got != tt.want {
t.Errorf("got %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -22,7 +22,7 @@ import (
"time"
"github.com/gorilla/csrf"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
@@ -50,7 +50,7 @@ type Server struct {
mode ServerMode
logf logger.Logf
lc *tailscale.LocalClient
lc *local.Client
timeNow func() time.Time
// devMode indicates that the server run with frontend assets
@@ -125,9 +125,9 @@ type ServerOpts struct {
// PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
PathPrefix string
// LocalClient is the tailscale.LocalClient to use for this web server.
// LocalClient is the local.Client to use for this web server.
// If nil, a new one will be created.
LocalClient *tailscale.LocalClient
LocalClient *local.Client
// TimeNow optionally provides a time function.
// time.Now is used as default.
@@ -166,7 +166,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
return nil, fmt.Errorf("invalid Mode provided")
}
if opts.LocalClient == nil {
opts.LocalClient = &tailscale.LocalClient{}
opts.LocalClient = &local.Client{}
}
s = &Server{
mode: opts.Mode,

View File

@@ -20,7 +20,7 @@ import (
"time"
"github.com/google/go-cmp/cmp"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -120,7 +120,7 @@ func TestServeAPI(t *testing.T) {
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
timeNow: time.Now,
}
@@ -288,7 +288,7 @@ func TestGetTailscaleBrowserSession(t *testing.T) {
s := &Server{
timeNow: time.Now,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
}
// Add some browser sessions to cache state.
@@ -457,7 +457,7 @@ func TestAuthorizeRequest(t *testing.T) {
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
timeNow: time.Now,
}
validCookie := "ts-cookie"
@@ -572,7 +572,7 @@ func TestServeAuth(t *testing.T) {
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
timeNow: func() time.Time { return timeNow },
newAuthURL: mockNewAuthURL,
waitAuthURL: mockWaitAuthURL,
@@ -914,7 +914,7 @@ func TestServeAPIAuthMetricLogging(t *testing.T) {
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
timeNow: func() time.Time { return timeNow },
newAuthURL: mockNewAuthURL,
waitAuthURL: mockWaitAuthURL,
@@ -1126,7 +1126,7 @@ func TestRequireTailscaleIP(t *testing.T) {
s := &Server{
mode: ManageServerMode,
lc: &tailscale.LocalClient{Dial: lal.Dial},
lc: &local.Client{Dial: lal.Dial},
timeNow: time.Now,
logf: t.Logf,
}

View File

@@ -27,6 +27,7 @@ import (
"strconv"
"strings"
"tailscale.com/hostinfo"
"tailscale.com/types/logger"
"tailscale.com/util/cmpver"
"tailscale.com/version"
@@ -169,6 +170,12 @@ func NewUpdater(args Arguments) (*Updater, error) {
type updateFunction func() error
func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
hi := hostinfo.New()
// We don't know how to update custom tsnet binaries, it's up to the user.
if hi.Package == "tsnet" {
return nil, false
}
switch runtime.GOOS {
case "windows":
return up.updateWindows, true

View File

@@ -10,7 +10,7 @@ import (
"io"
"net/http"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
)
@@ -18,7 +18,7 @@ import (
// the tailscaled's LocalAPI usermetrics endpoint at /localapi/v0/usermetrics.
type metrics struct {
debugEndpoint string
lc *tailscale.LocalClient
lc *local.Client
}
func proxy(w http.ResponseWriter, r *http.Request, url string, do func(*http.Request) (*http.Response, error)) {
@@ -68,7 +68,7 @@ func (m *metrics) handleDebug(w http.ResponseWriter, r *http.Request) {
// In 1.78.x and 1.80.x, it also proxies debug paths to tailscaled's debug
// endpoint if configured to ease migration for a breaking change serving user
// metrics instead of debug metrics on the "metrics" port.
func metricsHandlers(mux *http.ServeMux, lc *tailscale.LocalClient, debugAddrPort string) {
func metricsHandlers(mux *http.ServeMux, lc *local.Client, debugAddrPort string) {
m := &metrics{
lc: lc,
debugEndpoint: debugAddrPort,

View File

@@ -17,7 +17,7 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/ipn"
"tailscale.com/kube/kubetypes"
"tailscale.com/types/netmap"
@@ -28,7 +28,7 @@ import (
// applies it to lc. It exits when ctx is canceled. cdChanged is a channel that
// is written to when the certDomain changes, causing the serve config to be
// re-read and applied.
func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *tailscale.LocalClient, kc *kubeClient) {
func watchServeConfigChanges(ctx context.Context, path string, cdChanged <-chan bool, certDomainAtomic *atomic.Pointer[string], lc *local.Client, kc *kubeClient) {
if certDomainAtomic == nil {
panic("certDomainAtomic must not be nil")
}
@@ -91,7 +91,7 @@ func certDomainFromNetmap(nm *netmap.NetworkMap) string {
return nm.DNS.CertDomains[0]
}
// localClient is a subset of tailscale.LocalClient that can be mocked for testing.
// localClient is a subset of [local.Client] that can be mocked for testing.
type localClient interface {
SetServeConfig(context.Context, *ipn.ServeConfig) error
}

View File

@@ -12,7 +12,7 @@ import (
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/ipn"
"tailscale.com/kube/kubetypes"
)
@@ -197,7 +197,7 @@ func TestReadServeConfig(t *testing.T) {
}
type fakeLocalClient struct {
*tailscale.LocalClient
*local.Client
setServeCalled bool
}

View File

@@ -21,7 +21,7 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/ipn"
"tailscale.com/kube/egressservices"
"tailscale.com/kube/kubeclient"
@@ -50,7 +50,7 @@ type egressProxy struct {
kc kubeclient.Client // never nil
stateSecret string // name of the kube state Secret
tsClient *tailscale.LocalClient // never nil
tsClient *local.Client // never nil
netmapChan chan ipn.Notify // chan to receive netmap updates on
@@ -131,7 +131,7 @@ type egressProxyRunOpts struct {
cfgPath string
nfr linuxfw.NetfilterRunner
kc kubeclient.Client
tsClient *tailscale.LocalClient
tsClient *local.Client
stateSecret string
netmapChan chan ipn.Notify
podIPv4 string

View File

@@ -20,10 +20,10 @@ import (
"time"
"github.com/fsnotify/fsnotify"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
)
func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, *os.Process, error) {
func startTailscaled(ctx context.Context, cfg *settings) (*local.Client, *os.Process, error) {
args := tailscaledArgs(cfg)
// tailscaled runs without context, since it needs to persist
// beyond the startup timeout in ctx.
@@ -54,7 +54,7 @@ func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient
break
}
tsClient := &tailscale.LocalClient{
tsClient := &local.Client{
Socket: cfg.Socket,
UseSocketOnly: true,
}
@@ -170,7 +170,7 @@ func tailscaleSet(ctx context.Context, cfg *settings) error {
return nil
}
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *tailscale.LocalClient, errCh chan<- error) {
func watchTailscaledConfigChanges(ctx context.Context, path string, lc *local.Client, errCh chan<- error) {
var (
tickChan <-chan time.Time
tailscaledCfgDir = filepath.Dir(path)

View File

@@ -51,9 +51,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
L 💣 github.com/tailscale/netlink/nl from github.com/tailscale/netlink
github.com/tailscale/setec/client/setec from tailscale.com/cmd/derper
github.com/tailscale/setec/types/api from github.com/tailscale/setec/client/setec
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
💣 go4.org/mem from tailscale.com/client/local+
go4.org/netipx from tailscale.com/net/tsaddr
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
@@ -86,17 +88,18 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
tailscale.com from tailscale.com/version
💣 tailscale.com/atomicfile from tailscale.com/cmd/derper+
tailscale.com/client/local from tailscale.com/client/tailscale+
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
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/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/hostinfo from tailscale.com/net/netmon+
tailscale.com/ipn from tailscale.com/client/tailscale
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn from tailscale.com/client/local
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/kube/kubetypes from tailscale.com/envknob
tailscale.com/metrics from tailscale.com/cmd/derper+
tailscale.com/net/bakedroots from tailscale.com/net/tlsdial
@@ -106,7 +109,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/netknob from tailscale.com/net/netns
💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/netutil from tailscale.com/client/local
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/net/stunserver
tailscale.com/net/stunserver from tailscale.com/cmd/derper
@@ -116,11 +119,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/net/wsconn from tailscale.com/cmd/derper
tailscale.com/paths from tailscale.com/client/tailscale
💣 tailscale.com/safesocket from tailscale.com/client/tailscale
tailscale.com/paths from tailscale.com/client/local
💣 tailscale.com/safesocket from tailscale.com/client/local
tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale+
tailscale.com/tailcfg from tailscale.com/client/local+
tailscale.com/tka from tailscale.com/client/local+
W tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tstime from tailscale.com/derp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
@@ -131,7 +134,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/dnstype from tailscale.com/tailcfg+
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/tailcfg+
tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/version+
tailscale.com/types/logger from tailscale.com/cmd/derper+
tailscale.com/types/netmap from tailscale.com/ipn
@@ -141,7 +144,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/types/ptr from tailscale.com/hostinfo+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/tkatype from tailscale.com/client/local+
tailscale.com/types/views from tailscale.com/ipn+
tailscale.com/util/cibuild from tailscale.com/health
tailscale.com/util/clientmetric from tailscale.com/net/netmon+
@@ -188,13 +191,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
W golang.org/x/exp/constraints from tailscale.com/util/winutil
golang.org/x/exp/maps from tailscale.com/util/syspolicy/setting+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
@@ -207,6 +208,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
golang.org/x/sync/singleflight from github.com/tailscale/setec/client/setec
golang.org/x/sys/cpu from golang.org/x/crypto/argon2+
LD golang.org/x/sys/unix from github.com/google/nftables+
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
@@ -226,7 +228,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/aes from crypto/internal/hpke+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -235,31 +237,58 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/hpke from crypto/tls
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/subtle from crypto/cipher+
crypto/tls from golang.org/x/crypto/acme+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
embed from crypto/internal/nistec+
embed from google.golang.org/protobuf/internal/editiondefaults+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -280,23 +309,22 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
html from net/http/pprof+
html/template from tailscale.com/cmd/derper
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall
internal/asan from syscall+
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/aes+
internal/byteorder from crypto/cipher+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/aes+
internal/cpu from crypto/internal/fips140deps/cpu+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/aes+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/godebug from crypto/tls+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime
internal/goexperiment from runtime+
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall
internal/msan from syscall+
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -306,17 +334,20 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from bufio+
io/fs from crypto/x509+
L io/ioutil from github.com/mitchellh/go-ps+
@@ -328,7 +359,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 internal/concurrent+
math/rand/v2 from crypto/ecdsa+
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -341,7 +372,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os from crypto/internal/sysrand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/derper
W os/user from tailscale.com/util/winutil+
@@ -350,10 +381,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
runtime from crypto/internal/nistec+
runtime from crypto/internal/fips140+
runtime/debug from github.com/prometheus/client_golang/prometheus+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
@@ -363,7 +392,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
syscall from crypto/internal/sysrand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
@@ -373,3 +402,4 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -27,6 +27,7 @@ import (
"net/http"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"runtime"
@@ -36,6 +37,7 @@ import (
"syscall"
"time"
"github.com/tailscale/setec/client/setec"
"golang.org/x/time/rate"
"tailscale.com/atomicfile"
"tailscale.com/derp"
@@ -64,6 +66,9 @@ var (
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. If an entry contains a slash, the second part names a hostname to be used when dialing the target.")
secretsURL = flag.String("secrets-url", "", "SETEC server URL for secrets retrieval of mesh key")
secretPrefix = flag.String("secrets-path-prefix", "prod/derp", "setec path prefix for \""+setecMeshKeyName+"\" secret for DERP mesh key")
secretsCacheDir = flag.String("secrets-cache-dir", defaultSetecCacheDir(), "directory to cache setec secrets in (required if --secrets-url is set)")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list. If an entry contains a slash, the second part names a DNS record to poll for its TXT record with a `0` to `100` value for rollout percentage.")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
@@ -84,8 +89,14 @@ var (
var (
tlsRequestVersion = &metrics.LabelMap{Label: "version"}
tlsActiveVersion = &metrics.LabelMap{Label: "version"}
// Exactly 64 hexadecimal lowercase digits.
validMeshKey = regexp.MustCompile(`^[0-9a-f]{64}$`)
)
const setecMeshKeyName = "meshkey"
const meshKeyEnvVar = "TAILSCALE_DERPER_MESH_KEY"
func init() {
expvar.Publish("derper_tls_request_version", tlsRequestVersion)
expvar.Publish("gauge_derper_tls_active_version", tlsActiveVersion)
@@ -141,6 +152,14 @@ func writeNewConfig() config {
return cfg
}
func checkMeshKey(key string) (string, error) {
key = strings.TrimSpace(key)
if !validMeshKey.MatchString(key) {
return "", errors.New("key must contain exactly 64 hex digits")
}
return key, nil
}
func main() {
flag.Parse()
if *versionFlag {
@@ -177,18 +196,55 @@ func main() {
s.SetVerifyClientURLFailOpen(*verifyFailOpen)
s.SetTCPWriteTimeout(*tcpWriteTimeout)
if *meshPSKFile != "" {
b, err := os.ReadFile(*meshPSKFile)
var meshKey string
if *dev {
meshKey = os.Getenv(meshKeyEnvVar)
if meshKey == "" {
log.Printf("No mesh key specified for dev via %s\n", meshKeyEnvVar)
} else {
log.Printf("Set mesh key from %s\n", meshKeyEnvVar)
}
} else if *secretsURL != "" {
meshKeySecret := path.Join(*secretPrefix, setecMeshKeyName)
fc, err := setec.NewFileCache(*secretsCacheDir)
if err != nil {
log.Fatal(err)
log.Fatalf("NewFileCache: %v", err)
}
key := strings.TrimSpace(string(b))
if matched, _ := regexp.MatchString(`(?i)^[0-9a-f]{64,}$`, key); !matched {
log.Fatalf("key in %s must contain 64+ hex digits", *meshPSKFile)
log.Printf("Setting up setec store from %q", *secretsURL)
st, err := setec.NewStore(ctx,
setec.StoreConfig{
Client: setec.Client{Server: *secretsURL},
Secrets: []string{
meshKeySecret,
},
Cache: fc,
})
if err != nil {
log.Fatalf("NewStore: %v", err)
}
s.SetMeshKey(key)
log.Printf("DERP mesh key configured")
meshKey = st.Secret(meshKeySecret).GetString()
log.Println("Got mesh key from setec store")
st.Close()
} else if *meshPSKFile != "" {
b, err := setec.StaticFile(*meshPSKFile)
if err != nil {
log.Fatalf("StaticFile failed to get key: %v", err)
}
log.Println("Got mesh key from static file")
meshKey = b.GetString()
}
if meshKey == "" && *dev {
log.Printf("No mesh key configured for --dev mode")
} else if meshKey == "" {
log.Printf("No mesh key configured")
} else if key, err := checkMeshKey(meshKey); err != nil {
log.Fatalf("invalid mesh key: %v", err)
} else {
s.SetMeshKey(key)
log.Println("DERP mesh key configured")
}
if err := startMesh(s); err != nil {
log.Fatalf("startMesh: %v", err)
}
@@ -268,6 +324,9 @@ func main() {
Control: ktimeout.UserTimeout(*tcpUserTimeout),
KeepAlive: *tcpKeepAlive,
}
// As of 2025-02-19, MPTCP does not support TCP_USER_TIMEOUT socket option
// set in ktimeout.UserTimeout above.
lc.SetMultipathTCP(false)
quietLogger := log.New(logger.HTTPServerLogFilter{Inner: log.Printf}, "", 0)
httpsrv := &http.Server{
@@ -382,6 +441,10 @@ func prodAutocertHostPolicy(_ context.Context, host string) error {
return errors.New("invalid hostname")
}
func defaultSetecCacheDir() string {
return filepath.Join(os.Getenv("HOME"), ".cache", "derper-secrets")
}
func defaultMeshPSKFile() string {
try := []string{
"/home/derp/keys/derp-mesh.key",

View File

@@ -138,3 +138,46 @@ func TestTemplate(t *testing.T) {
t.Error("Output is missing debug info")
}
}
func TestCheckMeshKey(t *testing.T) {
testCases := []struct {
name string
input string
want string
wantErr bool
}{
{
name: "KeyOkay",
input: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
want: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
wantErr: false,
},
{
name: "TrimKeyOkay",
input: " f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6 ",
want: "f1ffafffffffffffffffffffffffffffffffffffffffffffffffff2ffffcfff6",
wantErr: false,
},
{
name: "NotAKey",
input: "zzthisisnotakey",
want: "",
wantErr: true,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
k, err := checkMeshKey(tt.input)
if err != nil && !tt.wantErr {
t.Errorf("unexpected error: %v", err)
}
if k != tt.want && err == nil {
t.Errorf("want: %s doesn't match expected: %s", tt.want, k)
}
})
}
}

View File

@@ -16,14 +16,10 @@ import (
"strings"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/internal/client/tailscale"
)
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.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
reusable := flag.Bool("reusable", false, "allocate a reusable authkey")
ephemeral := flag.Bool("ephemeral", false, "allocate an ephemeral authkey")
preauth := flag.Bool("preauth", true, "set the authkey as pre-authorized")

View File

@@ -13,6 +13,7 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
@@ -405,7 +406,8 @@ func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string
got := resp.StatusCode
want := http.StatusOK
if got != want {
return "", fmt.Errorf("wanted HTTP status code %d but got %d", want, got)
errorDetails, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("wanted HTTP status code %d but got %d: %#q", want, got, string(errorDetails))
}
return Shuck(resp.Header.Get("ETag")), nil

View File

@@ -18,7 +18,7 @@ import (
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
)
@@ -31,7 +31,7 @@ var (
//go:embed hello.tmpl.html
var embeddedTemplate string
var localClient tailscale.LocalClient
var localClient local.Client
func main() {
flag.Parse()

View File

@@ -9,7 +9,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
@@ -31,10 +30,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
L github.com/aws/aws-sdk-go-v2/internal/auth from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
L github.com/aws/aws-sdk-go-v2/internal/auth/smithy from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/context from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/middleware from github.com/aws/aws-sdk-go-v2/service/sso+
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
@@ -69,16 +70,17 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/private/requestcompression from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/tracing from github.com/aws/aws-sdk-go-v2/aws/middleware+
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/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
@@ -96,6 +98,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 github.com/fsnotify/fsnotify from sigs.k8s.io/controller-runtime/pkg/certwatcher
github.com/fxamacker/cbor/v2 from tailscale.com/tka+
github.com/gaissmai/bart from tailscale.com/net/ipset+
github.com/gaissmai/bart/internal/bitset from github.com/gaissmai/bart+
github.com/gaissmai/bart/internal/sparse from github.com/gaissmai/bart
github.com/go-json-experiment/json from tailscale.com/types/opt+
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+
@@ -139,7 +143,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
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+
L 💣 github.com/illarion/gonotify/v2 from tailscale.com/net/dns
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/feature/tap
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
@@ -197,9 +202,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
LD github.com/tailscale/golang-x-crypto/internal/poly1305 from github.com/tailscale/golang-x-crypto/ssh
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@@ -235,7 +237,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
go.uber.org/zap/internal/pool from go.uber.org/zap+
go.uber.org/zap/internal/stacktrace from go.uber.org/zap
go.uber.org/zap/zapcore from github.com/go-logr/zapr+
💣 go4.org/mem from tailscale.com/client/tailscale+
💣 go4.org/mem from tailscale.com/client/local+
go4.org/netipx from tailscale.com/ipn/ipnlocal+
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
@@ -296,7 +298,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -780,7 +782,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com from tailscale.com/version
tailscale.com/appc from tailscale.com/ipn/ipnlocal
💣 tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/client/web+
tailscale.com/client/local from tailscale.com/client/tailscale+
tailscale.com/client/tailscale from tailscale.com/cmd/k8s-operator+
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
tailscale.com/clientupdate from tailscale.com/client/web+
@@ -797,8 +800,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
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/envknob from tailscale.com/client/tailscale+
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan+
tailscale.com/feature/capture from tailscale.com/feature/condregister
@@ -808,12 +811,14 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/tailscale+
tailscale.com/ipn from tailscale.com/client/local+
tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/tsnet+
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
@@ -860,7 +865,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/packet from tailscale.com/net/connstats+
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
tailscale.com/net/ping from tailscale.com/net/netcheck+
@@ -878,19 +883,19 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/tsd+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
tailscale.com/paths from tailscale.com/client/local+
💣 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/safesocket from tailscale.com/client/local+
tailscale.com/sessionrecording from tailscale.com/k8s-operator/sessionrecording+
tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailcfg from tailscale.com/client/local+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
tailscale.com/tempfork/httprec from tailscale.com/control/controlclient
tailscale.com/tka from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/local+
tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tsd from tailscale.com/ipn/ipnlocal+
tailscale.com/tsnet from tailscale.com/cmd/k8s-operator+
@@ -902,7 +907,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/ipn+
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
tailscale.com/types/logger from tailscale.com/appc+
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
@@ -915,7 +920,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
tailscale.com/types/result from tailscale.com/util/lineiter
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
tailscale.com/types/tkatype from tailscale.com/client/local+
tailscale.com/types/views from tailscale.com/appc+
tailscale.com/util/cibuild from tailscale.com/health
tailscale.com/util/clientmetric from tailscale.com/cmd/k8s-operator+
@@ -986,20 +991,21 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/ssh+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
LD golang.org/x/crypto/ssh from tailscale.com/ipn/ipnlocal
LD golang.org/x/crypto/ssh/internal/bcrypt_pbkdf from golang.org/x/crypto/ssh
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from sigs.k8s.io/controller-runtime/pkg/cache+
golang.org/x/exp/slices from tailscale.com/cmd/k8s-operator+
@@ -1012,6 +1018,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
golang.org/x/net/icmp from github.com/prometheus-community/pro-bing+
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
golang.org/x/net/internal/socks from golang.org/x/net/proxy
@@ -1047,7 +1054,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/aes from crypto/internal/hpke+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
@@ -1056,27 +1063,54 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/hpke from crypto/tls
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/subtle from crypto/cipher+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
@@ -1084,7 +1118,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
database/sql/driver from database/sql+
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from crypto/internal/nistec+
embed from github.com/tailscale/web-client-prebuilt+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -1104,7 +1138,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
go/build/constraint from go/parser
go/doc from k8s.io/apimachinery/pkg/runtime
go/doc/comment from go/doc
go/internal/typeparams from go/parser
go/parser from k8s.io/apimachinery/pkg/runtime
go/scanner from go/ast+
go/token from go/ast+
@@ -1116,24 +1149,23 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
html from html/template+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall
internal/asan from syscall+
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/aes+
internal/byteorder from crypto/cipher+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/aes+
internal/cpu from crypto/internal/fips140deps/cpu+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/aes+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime
internal/goexperiment from runtime+
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/lazyregexp from go/doc
internal/msan from syscall
internal/msan from syscall+
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -1143,18 +1175,21 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
@@ -1183,7 +1218,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
net/netip from github.com/gaissmai/bart+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/url from crypto/x509+
os from crypto/rand+
os from crypto/internal/sysrand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/signal from sigs.k8s.io/controller-runtime/pkg/manager/signals
os/user from archive/tar+
@@ -1194,8 +1229,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
@@ -1215,3 +1248,4 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -103,7 +103,7 @@ spec:
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
type:
description: |-
Type of the ProxyGroup proxies. Currently the only supported type is egress.
Type of the ProxyGroup proxies. Supported types are egress and ingress.
Type is immutable once a ProxyGroup is created.
type: string
enum:

View File

@@ -2860,7 +2860,7 @@ spec:
type: array
type:
description: |-
Type of the ProxyGroup proxies. Currently the only supported type is egress.
Type of the ProxyGroup proxies. Supported types are egress and ingress.
Type is immutable once a ProxyGroup is created.
enum:
- egress

View File

@@ -21,7 +21,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
kzap "sigs.k8s.io/controller-runtime/pkg/log/zap"
"tailscale.com/client/tailscale"
"tailscale.com/internal/client/tailscale"
)
const (
@@ -64,7 +64,6 @@ func TestMain(m *testing.M) {
func runTests(m *testing.M) (int, error) {
zlog := kzap.NewRaw([]kzap.Opts{kzap.UseDevMode(true), kzap.Level(zapcore.DebugLevel)}...).Sugar()
logf.SetLogger(zapr.NewLogger(zlog.Desugar()))
tailscale.I_Acknowledge_This_API_Is_Unstable = true
if clientID := os.Getenv("TS_API_CLIENT_ID"); clientID != "" {
cleanup, err := setupClientAndACLs()

View File

@@ -26,7 +26,7 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
tsoperator "tailscale.com/k8s-operator"
@@ -44,6 +44,11 @@ const (
VIPSvcOwnerRef = "tailscale.com/k8s-operator:owned-by:%s"
// FinalizerNamePG is the finalizer used by the IngressPGReconciler
FinalizerNamePG = "tailscale.com/ingress-pg-finalizer"
indexIngressProxyGroup = ".metadata.annotations.ingress-proxy-group"
// annotationHTTPEndpoint can be used to configure the Ingress to expose an HTTP endpoint to tailnet (as
// well as the default HTTPS endpoint).
annotationHTTPEndpoint = "tailscale.com/http-endpoint"
)
var gaugePGIngressResources = clientmetric.NewGauge(kubetypes.MetricIngressPGResourceCount)
@@ -180,7 +185,8 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
return fmt.Errorf("error determining DNS name base: %w", err)
}
dnsName := hostname + "." + tcd
existingVIPSvc, err := a.tsClient.getVIPServiceByName(ctx, hostname)
serviceName := tailcfg.ServiceName("svc:" + hostname)
existingVIPSvc, err := a.tsClient.GetVIPService(ctx, serviceName)
// TODO(irbekrm): here and when creating the VIPService, verify if the error is not terminal (and therefore
// should not be reconciled). For example, if the hostname is already a hostname of a Tailscale node, the GET
// here will fail.
@@ -199,16 +205,16 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
// 3. Ensure that the serve config for the ProxyGroup contains the VIPService
cm, cfg, err := a.proxyGroupServeConfig(ctx, pgName)
if err != nil {
return fmt.Errorf("error getting ingress serve config: %w", err)
return fmt.Errorf("error getting Ingress serve config: %w", err)
}
if cm == nil {
logger.Infof("no ingress serve config ConfigMap found, unable to update serve config. Ensure that ProxyGroup is healthy.")
logger.Infof("no Ingress serve config ConfigMap found, unable to update serve config. Ensure that ProxyGroup is healthy.")
return nil
}
ep := ipn.HostPort(fmt.Sprintf("%s:443", dnsName))
handlers, err := handlersForIngress(ctx, ing, a.Client, a.recorder, dnsName, logger)
if err != nil {
return fmt.Errorf("failed to get handlers for ingress: %w", err)
return fmt.Errorf("failed to get handlers for Ingress: %w", err)
}
ingCfg := &ipn.ServiceConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
@@ -222,7 +228,19 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
},
},
}
serviceName := tailcfg.ServiceName("svc:" + hostname)
// Add HTTP endpoint if configured.
if isHTTPEndpointEnabled(ing) {
logger.Infof("exposing Ingress over HTTP")
epHTTP := ipn.HostPort(fmt.Sprintf("%s:80", dnsName))
ingCfg.TCP[80] = &ipn.TCPPortHandler{
HTTP: true,
}
ingCfg.Web[epHTTP] = &ipn.WebServerConfig{
Handlers: handlers,
}
}
var gotCfg *ipn.ServiceConfig
if cfg != nil && cfg.Services != nil {
gotCfg = cfg.Services[serviceName]
@@ -246,18 +264,25 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
tags = strings.Split(tstr, ",")
}
vipSvc := &VIPService{
Name: hostname,
vipPorts := []string{"443"} // always 443 for Ingress
if isHTTPEndpointEnabled(ing) {
vipPorts = append(vipPorts, "80")
}
vipSvc := &tailscale.VIPService{
Name: serviceName,
Tags: tags,
Ports: []string{"443"}, // always 443 for Ingress
Ports: vipPorts,
Comment: fmt.Sprintf(VIPSvcOwnerRef, ing.UID),
}
if existingVIPSvc != nil {
vipSvc.Addrs = existingVIPSvc.Addrs
}
if existingVIPSvc == nil || !reflect.DeepEqual(vipSvc.Tags, existingVIPSvc.Tags) {
if existingVIPSvc == nil ||
!reflect.DeepEqual(vipSvc.Tags, existingVIPSvc.Tags) ||
!reflect.DeepEqual(vipSvc.Ports, existingVIPSvc.Ports) {
logger.Infof("Ensuring VIPService %q exists and is up to date", hostname)
if err := a.tsClient.createOrUpdateVIPServiceByName(ctx, vipSvc); err != nil {
if err := a.tsClient.CreateOrUpdateVIPService(ctx, vipSvc); err != nil {
logger.Infof("error creating VIPService: %v", err)
return fmt.Errorf("error creating VIPService: %w", err)
}
@@ -265,16 +290,22 @@ func (a *IngressPGReconciler) maybeProvision(ctx context.Context, hostname strin
// 5. Update Ingress status
oldStatus := ing.Status.DeepCopy()
// TODO(irbekrm): once we have ingress ProxyGroup, we can determine if instances are ready to route traffic to the VIPService
ports := []networkingv1.IngressPortStatus{
{
Protocol: "TCP",
Port: 443,
},
}
if isHTTPEndpointEnabled(ing) {
ports = append(ports, networkingv1.IngressPortStatus{
Protocol: "TCP",
Port: 80,
})
}
ing.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{
{
Hostname: dnsName,
Ports: []networkingv1.IngressPortStatus{
{
Protocol: "TCP",
Port: 443,
},
},
Ports: ports,
},
}
if apiequality.Semantic.DeepEqual(oldStatus, ing.Status) {
@@ -305,39 +336,39 @@ func (a *IngressPGReconciler) maybeCleanupProxyGroup(ctx context.Context, proxyG
}
serveConfigChanged := false
// For each VIPService in serve config...
for vipHostname := range cfg.Services {
for vipServiceName := range cfg.Services {
// ...check if there is currently an Ingress with this hostname
found := false
for _, i := range ingList.Items {
ingressHostname := hostnameForIngress(&i)
if ingressHostname == vipHostname.WithoutPrefix() {
if ingressHostname == vipServiceName.WithoutPrefix() {
found = true
break
}
}
if !found {
logger.Infof("VIPService %q is not owned by any Ingress, cleaning up", vipHostname)
svc, err := a.getVIPService(ctx, vipHostname.WithoutPrefix(), logger)
logger.Infof("VIPService %q is not owned by any Ingress, cleaning up", vipServiceName)
svc, err := a.getVIPService(ctx, vipServiceName, logger)
if err != nil {
errResp := &tailscale.ErrResponse{}
if errors.As(err, &errResp) && errResp.Status == http.StatusNotFound {
delete(cfg.Services, vipHostname)
delete(cfg.Services, vipServiceName)
serveConfigChanged = true
continue
}
return err
}
if isVIPServiceForAnyIngress(svc) {
logger.Infof("cleaning up orphaned VIPService %q", vipHostname)
if err := a.tsClient.deleteVIPServiceByName(ctx, vipHostname.WithoutPrefix()); err != nil {
logger.Infof("cleaning up orphaned VIPService %q", vipServiceName)
if err := a.tsClient.DeleteVIPService(ctx, vipServiceName); err != nil {
errResp := &tailscale.ErrResponse{}
if !errors.As(err, &errResp) || errResp.Status != http.StatusNotFound {
return fmt.Errorf("deleting VIPService %q: %w", vipHostname, err)
return fmt.Errorf("deleting VIPService %q: %w", vipServiceName, err)
}
}
}
delete(cfg.Services, vipHostname)
delete(cfg.Services, vipServiceName)
serveConfigChanged = true
}
}
@@ -386,7 +417,7 @@ func (a *IngressPGReconciler) maybeCleanup(ctx context.Context, hostname string,
logger.Infof("Ensuring that VIPService %q configuration is cleaned up", hostname)
// 2. Delete the VIPService.
if err := a.deleteVIPServiceIfExists(ctx, hostname, ing, logger); err != nil {
if err := a.deleteVIPServiceIfExists(ctx, serviceName, ing, logger); err != nil {
return fmt.Errorf("error deleting VIPService: %w", err)
}
@@ -478,26 +509,26 @@ func (a *IngressPGReconciler) shouldExpose(ing *networkingv1.Ingress) bool {
return isTSIngress && pgAnnot != ""
}
func (a *IngressPGReconciler) getVIPService(ctx context.Context, hostname string, logger *zap.SugaredLogger) (*VIPService, error) {
svc, err := a.tsClient.getVIPServiceByName(ctx, hostname)
func (a *IngressPGReconciler) getVIPService(ctx context.Context, name tailcfg.ServiceName, logger *zap.SugaredLogger) (*tailscale.VIPService, error) {
svc, err := a.tsClient.GetVIPService(ctx, name)
if err != nil {
errResp := &tailscale.ErrResponse{}
if ok := errors.As(err, errResp); ok && errResp.Status != http.StatusNotFound {
logger.Infof("error getting VIPService %q: %v", hostname, err)
return nil, fmt.Errorf("error getting VIPService %q: %w", hostname, err)
logger.Infof("error getting VIPService %q: %v", name, err)
return nil, fmt.Errorf("error getting VIPService %q: %w", name, err)
}
}
return svc, nil
}
func isVIPServiceForIngress(svc *VIPService, ing *networkingv1.Ingress) bool {
func isVIPServiceForIngress(svc *tailscale.VIPService, ing *networkingv1.Ingress) bool {
if svc == nil || ing == nil {
return false
}
return strings.EqualFold(svc.Comment, fmt.Sprintf(VIPSvcOwnerRef, ing.UID))
}
func isVIPServiceForAnyIngress(svc *VIPService) bool {
func isVIPServiceForAnyIngress(svc *tailscale.VIPService) bool {
if svc == nil {
return false
}
@@ -550,7 +581,7 @@ func (a *IngressPGReconciler) validateIngress(ing *networkingv1.Ingress, pg *tsa
}
// deleteVIPServiceIfExists attempts to delete the VIPService if it exists and is owned by the given Ingress.
func (a *IngressPGReconciler) deleteVIPServiceIfExists(ctx context.Context, name string, ing *networkingv1.Ingress, logger *zap.SugaredLogger) error {
func (a *IngressPGReconciler) deleteVIPServiceIfExists(ctx context.Context, name tailcfg.ServiceName, ing *networkingv1.Ingress, logger *zap.SugaredLogger) error {
svc, err := a.getVIPService(ctx, name, logger)
if err != nil {
return fmt.Errorf("error getting VIPService: %w", err)
@@ -562,8 +593,16 @@ func (a *IngressPGReconciler) deleteVIPServiceIfExists(ctx context.Context, name
}
logger.Infof("Deleting VIPService %q", name)
if err = a.tsClient.deleteVIPServiceByName(ctx, name); err != nil {
if err = a.tsClient.DeleteVIPService(ctx, name); err != nil {
return fmt.Errorf("error deleting VIPService: %w", err)
}
return nil
}
// isHTTPEndpointEnabled returns true if the Ingress has been configured to expose an HTTP endpoint to tailnet.
func isHTTPEndpointEnabled(ing *networkingv1.Ingress) bool {
if ing == nil {
return false
}
return ing.Annotations[annotationHTTPEndpoint] == "enabled"
}

View File

@@ -8,6 +8,8 @@ package main
import (
"context"
"encoding/json"
"maps"
"reflect"
"testing"
"slices"
@@ -18,81 +20,18 @@ import (
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/client/fake"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
func TestIngressPGReconciler(t *testing.T) {
tsIngressClass := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
}
ingPGR, fc, ft := setupIngressTest(t)
// Pre-create the ProxyGroup
pg := &tsapi.ProxyGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pg",
Generation: 1,
},
Spec: tsapi.ProxyGroupSpec{
Type: tsapi.ProxyGroupTypeIngress,
},
}
// Pre-create the ConfigMap for the ProxyGroup
pgConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pg-ingress-config",
Namespace: "operator-ns",
},
BinaryData: map[string][]byte{
"serve-config.json": []byte(`{"Services":{}}`),
},
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(pg, pgConfigMap, tsIngressClass).
WithStatusSubresource(pg).
Build()
mustUpdateStatus(t, fc, "", pg.Name, func(pg *tsapi.ProxyGroup) {
pg.Status.Conditions = []metav1.Condition{
{
Type: string(tsapi.ProxyGroupReady),
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
}
})
ft := &fakeTSClient{}
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
lc := &fakeLocalClient{
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{
MagicDNSSuffix: "ts.net",
},
},
}
ingPGR := &IngressPGReconciler{
Client: fc,
tsClient: ft,
tsnetServer: fakeTsnetServer,
defaultTags: []string{"tag:k8s"},
tsNamespace: "operator-ns",
logger: zl.Sugar(),
recorder: record.NewFakeRecorder(10),
lc: lc,
}
// Test 1: Default tags
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
@@ -122,8 +61,74 @@ func TestIngressPGReconciler(t *testing.T) {
// Verify initial reconciliation
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyServeConfig(t, fc, "svc:my-svc", false)
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
// Get and verify the ConfigMap was updated
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
})
expectReconciled(t, ingPGR, "default", "test-ingress")
// Verify VIPService uses custom tags
vipSvc, err := ft.GetVIPService(context.Background(), "svc:my-svc")
if err != nil {
t.Fatalf("getting VIPService: %v", err)
}
if vipSvc == nil {
t.Fatal("VIPService not created")
}
wantTags := []string{"tag:custom", "tag:test"} // custom tags only
gotTags := slices.Clone(vipSvc.Tags)
slices.Sort(gotTags)
slices.Sort(wantTags)
if !slices.Equal(gotTags, wantTags) {
t.Errorf("incorrect VIPService tags: got %v, want %v", gotTags, wantTags)
}
// Create second Ingress
ing2 := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "my-other-ingress",
Namespace: "default",
UID: types.UID("5678-UID"),
Annotations: map[string]string{
"tailscale.com/proxy-group": "test-pg",
},
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-other-svc.tailnetxyz.ts.net"}},
},
},
}
mustCreate(t, fc, ing2)
// Verify second Ingress reconciliation
expectReconciled(t, ingPGR, "default", "my-other-ingress")
verifyServeConfig(t, fc, "svc:my-other-svc", false)
verifyVIPService(t, ft, "svc:my-other-svc", []string{"443"})
// Verify first Ingress is still working
verifyServeConfig(t, fc, "svc:my-svc", false)
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
// Delete second Ingress
if err := fc.Delete(context.Background(), ing2); err != nil {
t.Fatalf("deleting second Ingress: %v", err)
}
expectReconciled(t, ingPGR, "default", "my-other-ingress")
// Verify second Ingress cleanup
cm := &corev1.ConfigMap{}
if err := fc.Get(context.Background(), types.NamespacedName{
Name: "test-pg-ingress-config",
@@ -137,46 +142,16 @@ func TestIngressPGReconciler(t *testing.T) {
t.Fatalf("unmarshaling serve config: %v", err)
}
// Verify first Ingress is still configured
if cfg.Services["svc:my-svc"] == nil {
t.Error("expected serve config to contain VIPService configuration")
t.Error("first Ingress service config was incorrectly removed")
}
// Verify second Ingress was cleaned up
if cfg.Services["svc:my-other-svc"] != nil {
t.Error("second Ingress service config was not cleaned up")
}
// Verify VIPService uses default tags
vipSvc, err := ft.getVIPServiceByName(context.Background(), "my-svc")
if err != nil {
t.Fatalf("getting VIPService: %v", err)
}
if vipSvc == nil {
t.Fatal("VIPService not created")
}
wantTags := []string{"tag:k8s"} // default tags
if !slices.Equal(vipSvc.Tags, wantTags) {
t.Errorf("incorrect VIPService tags: got %v, want %v", vipSvc.Tags, wantTags)
}
// Test 2: Custom tags
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
ing.Annotations["tailscale.com/tags"] = "tag:custom,tag:test"
})
expectReconciled(t, ingPGR, "default", "test-ingress")
// Verify VIPService uses custom tags
vipSvc, err = ft.getVIPServiceByName(context.Background(), "my-svc")
if err != nil {
t.Fatalf("getting VIPService: %v", err)
}
if vipSvc == nil {
t.Fatal("VIPService not created")
}
wantTags = []string{"tag:custom", "tag:test"} // custom tags only
gotTags := slices.Clone(vipSvc.Tags)
slices.Sort(gotTags)
slices.Sort(wantTags)
if !slices.Equal(gotTags, wantTags) {
t.Errorf("incorrect VIPService tags: got %v, want %v", gotTags, wantTags)
}
// Delete the Ingress and verify cleanup
// Delete the first Ingress and verify cleanup
if err := fc.Delete(context.Background(), ing); err != nil {
t.Fatalf("deleting Ingress: %v", err)
}
@@ -335,3 +310,233 @@ func TestValidateIngress(t *testing.T) {
})
}
}
func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
ingPGR, fc, ft := setupIngressTest(t)
// Create test Ingress with HTTP endpoint enabled
ing := &networkingv1.Ingress{
TypeMeta: metav1.TypeMeta{Kind: "Ingress", APIVersion: "networking.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/proxy-group": "test-pg",
"tailscale.com/http-endpoint": "enabled",
},
},
Spec: networkingv1.IngressSpec{
IngressClassName: ptr.To("tailscale"),
DefaultBackend: &networkingv1.IngressBackend{
Service: &networkingv1.IngressServiceBackend{
Name: "test",
Port: networkingv1.ServiceBackendPort{
Number: 8080,
},
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-svc"}},
},
},
}
if err := fc.Create(context.Background(), ing); err != nil {
t.Fatal(err)
}
// Verify initial reconciliation with HTTP enabled
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyVIPService(t, ft, "svc:my-svc", []string{"80", "443"})
verifyServeConfig(t, fc, "svc:my-svc", true)
// Verify Ingress status
ing = &networkingv1.Ingress{}
if err := fc.Get(context.Background(), types.NamespacedName{
Name: "test-ingress",
Namespace: "default",
}, ing); err != nil {
t.Fatal(err)
}
wantStatus := []networkingv1.IngressPortStatus{
{Port: 443, Protocol: "TCP"},
{Port: 80, Protocol: "TCP"},
}
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
t.Errorf("incorrect status ports: got %v, want %v",
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
}
// Remove HTTP endpoint annotation
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
delete(ing.Annotations, "tailscale.com/http-endpoint")
})
// Verify reconciliation after removing HTTP
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
verifyServeConfig(t, fc, "svc:my-svc", false)
// Verify Ingress status
ing = &networkingv1.Ingress{}
if err := fc.Get(context.Background(), types.NamespacedName{
Name: "test-ingress",
Namespace: "default",
}, ing); err != nil {
t.Fatal(err)
}
wantStatus = []networkingv1.IngressPortStatus{
{Port: 443, Protocol: "TCP"},
}
if !reflect.DeepEqual(ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus) {
t.Errorf("incorrect status ports: got %v, want %v",
ing.Status.LoadBalancer.Ingress[0].Ports, wantStatus)
}
}
func verifyVIPService(t *testing.T, ft *fakeTSClient, serviceName string, wantPorts []string) {
t.Helper()
vipSvc, err := ft.GetVIPService(context.Background(), tailcfg.ServiceName(serviceName))
if err != nil {
t.Fatalf("getting VIPService %q: %v", serviceName, err)
}
if vipSvc == nil {
t.Fatalf("VIPService %q not created", serviceName)
}
gotPorts := slices.Clone(vipSvc.Ports)
slices.Sort(gotPorts)
slices.Sort(wantPorts)
if !slices.Equal(gotPorts, wantPorts) {
t.Errorf("incorrect ports for VIPService %q: got %v, want %v", serviceName, gotPorts, wantPorts)
}
}
func verifyServeConfig(t *testing.T, fc client.Client, serviceName string, wantHTTP bool) {
t.Helper()
cm := &corev1.ConfigMap{}
if err := fc.Get(context.Background(), types.NamespacedName{
Name: "test-pg-ingress-config",
Namespace: "operator-ns",
}, cm); err != nil {
t.Fatalf("getting ConfigMap: %v", err)
}
cfg := &ipn.ServeConfig{}
if err := json.Unmarshal(cm.BinaryData["serve-config.json"], cfg); err != nil {
t.Fatalf("unmarshaling serve config: %v", err)
}
t.Logf("Looking for service %q in config: %+v", serviceName, cfg)
svc := cfg.Services[tailcfg.ServiceName(serviceName)]
if svc == nil {
t.Fatalf("service %q not found in serve config, services: %+v", serviceName, maps.Keys(cfg.Services))
}
wantHandlers := 1
if wantHTTP {
wantHandlers = 2
}
// Check TCP handlers
if len(svc.TCP) != wantHandlers {
t.Errorf("incorrect number of TCP handlers for service %q: got %d, want %d", serviceName, len(svc.TCP), wantHandlers)
}
if wantHTTP {
if h, ok := svc.TCP[uint16(80)]; !ok {
t.Errorf("HTTP (port 80) handler not found for service %q", serviceName)
} else if !h.HTTP {
t.Errorf("HTTP not enabled for port 80 handler for service %q", serviceName)
}
}
if h, ok := svc.TCP[uint16(443)]; !ok {
t.Errorf("HTTPS (port 443) handler not found for service %q", serviceName)
} else if !h.HTTPS {
t.Errorf("HTTPS not enabled for port 443 handler for service %q", serviceName)
}
// Check Web handlers
if len(svc.Web) != wantHandlers {
t.Errorf("incorrect number of Web handlers for service %q: got %d, want %d", serviceName, len(svc.Web), wantHandlers)
}
}
func setupIngressTest(t *testing.T) (*IngressPGReconciler, client.Client, *fakeTSClient) {
t.Helper()
tsIngressClass := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{Name: "tailscale"},
Spec: networkingv1.IngressClassSpec{Controller: "tailscale.com/ts-ingress"},
}
// Pre-create the ProxyGroup
pg := &tsapi.ProxyGroup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pg",
Generation: 1,
},
Spec: tsapi.ProxyGroupSpec{
Type: tsapi.ProxyGroupTypeIngress,
},
}
// Pre-create the ConfigMap for the ProxyGroup
pgConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pg-ingress-config",
Namespace: "operator-ns",
},
BinaryData: map[string][]byte{
"serve-config.json": []byte(`{"Services":{}}`),
},
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(pg, pgConfigMap, tsIngressClass).
WithStatusSubresource(pg).
Build()
// Set ProxyGroup status to ready
pg.Status.Conditions = []metav1.Condition{
{
Type: string(tsapi.ProxyGroupReady),
Status: metav1.ConditionTrue,
ObservedGeneration: 1,
},
}
if err := fc.Status().Update(context.Background(), pg); err != nil {
t.Fatal(err)
}
ft := &fakeTSClient{}
fakeTsnetServer := &fakeTSNetServer{certDomains: []string{"foo.com"}}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
lc := &fakeLocalClient{
status: &ipnstate.Status{
CurrentTailnet: &ipnstate.TailnetStatus{
MagicDNSSuffix: "ts.net",
},
},
}
ingPGR := &IngressPGReconciler{
Client: fc,
tsClient: ft,
tsnetServer: fakeTsnetServer,
defaultTags: []string{"tag:k8s"},
tsNamespace: "operator-ns",
logger: zl.Sugar(),
recorder: record.NewFakeRecorder(10),
lc: lc,
}
return ingPGR, fc, ft
}

View File

@@ -331,6 +331,33 @@ func runReconcilers(opts reconcilerOpts) {
if err != nil {
startlog.Fatalf("could not create ingress reconciler: %v", err)
}
lc, err := opts.tsServer.LocalClient()
if err != nil {
startlog.Fatalf("could not get local client: %v", err)
}
ingressProxyGroupFilter := handler.EnqueueRequestsFromMapFunc(ingressesFromIngressProxyGroup(mgr.GetClient(), opts.log))
err = builder.
ControllerManagedBy(mgr).
For(&networkingv1.Ingress{}).
Named("ingress-pg-reconciler").
Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(serviceHandlerForIngressPG(mgr.GetClient(), startlog))).
Watches(&tsapi.ProxyGroup{}, ingressProxyGroupFilter).
Complete(&IngressPGReconciler{
recorder: eventRecorder,
tsClient: opts.tsClient,
tsnetServer: opts.tsServer,
defaultTags: strings.Split(opts.proxyTags, ","),
Client: mgr.GetClient(),
logger: opts.log.Named("ingress-pg-reconciler"),
lc: lc,
tsNamespace: opts.tailscaleNamespace,
})
if err != nil {
startlog.Fatalf("could not create ingress-pg-reconciler: %v", err)
}
if err := mgr.GetFieldIndexer().IndexField(context.Background(), new(networkingv1.Ingress), indexIngressProxyGroup, indexPGIngresses); err != nil {
startlog.Fatalf("failed setting up indexer for HA Ingresses: %v", err)
}
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("connector"))
// If a ProxyClassChanges, enqueue all Connectors that have
@@ -1036,6 +1063,36 @@ func egressSvcsFromEgressProxyGroup(cl client.Client, logger *zap.SugaredLogger)
}
}
// ingressesFromIngressProxyGroup is an event handler for ingress ProxyGroups. It returns reconcile requests for all
// user-created Ingresses that should be exposed on this ProxyGroup.
func ingressesFromIngressProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
pg, ok := o.(*tsapi.ProxyGroup)
if !ok {
logger.Infof("[unexpected] ProxyGroup handler triggered for an object that is not a ProxyGroup")
return nil
}
if pg.Spec.Type != tsapi.ProxyGroupTypeIngress {
return nil
}
ingList := &networkingv1.IngressList{}
if err := cl.List(ctx, ingList, client.MatchingFields{indexIngressProxyGroup: pg.Name}); err != nil {
logger.Infof("error listing Ingresses: %v, skipping a reconcile for event on ProxyGroup %s", err, pg.Name)
return nil
}
reqs := make([]reconcile.Request, 0)
for _, svc := range ingList.Items {
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: svc.Namespace,
Name: svc.Name,
},
})
}
return reqs
}
}
// epsFromExternalNameService is an event handler for ExternalName Services that define a Tailscale egress service that
// should be exposed on a ProxyGroup. It returns reconcile requests for EndpointSlices created for this Service.
func epsFromExternalNameService(cl client.Client, logger *zap.SugaredLogger, ns string) handler.MapFunc {
@@ -1156,6 +1213,51 @@ func indexEgressServices(o client.Object) []string {
return []string{o.GetAnnotations()[AnnotationProxyGroup]}
}
// indexPGIngresses adds a local index to a cached Tailscale Ingresses meant to be exposed on a ProxyGroup. The index is
// used a list filter.
func indexPGIngresses(o client.Object) []string {
if !hasProxyGroupAnnotation(o) {
return nil
}
return []string{o.GetAnnotations()[AnnotationProxyGroup]}
}
// serviceHandlerForIngressPG returns a handler for Service events that ensures that if the Service
// associated with an event is a backend Service for a tailscale Ingress with ProxyGroup annotation,
// the associated Ingress gets reconciled.
func serviceHandlerForIngressPG(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
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 {
continue
}
if !hasProxyGroupAnnotation(&ing) {
continue
}
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 hasProxyGroupAnnotation(obj client.Object) bool {
ing := obj.(*networkingv1.Ingress)
return ing.Annotations[AnnotationProxyGroup] != ""

View File

@@ -20,7 +20,7 @@ import (
"go.uber.org/zap"
"k8s.io/client-go/rest"
"k8s.io/client-go/transport"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
ksr "tailscale.com/k8s-operator/sessionrecording"
"tailscale.com/kube/kubetypes"
@@ -189,7 +189,7 @@ func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredL
// LocalAPI and then proxies them to the Kubernetes API.
type apiserverProxy struct {
log *zap.SugaredLogger
lc *tailscale.LocalClient
lc *local.Client
rp *httputil.ReverseProxy
mode apiServerProxyMode

View File

@@ -28,10 +28,11 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
@@ -767,7 +768,7 @@ type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
deleted []string
vipServices map[string]*VIPService
vipServices map[tailcfg.ServiceName]*tailscale.VIPService
}
type fakeTSNetServer struct {
certDomains []string
@@ -874,7 +875,7 @@ func removeAuthKeyIfExistsModifier(t *testing.T) func(s *corev1.Secret) {
}
}
func (c *fakeTSClient) getVIPServiceByName(ctx context.Context, name string) (*VIPService, error) {
func (c *fakeTSClient) GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*tailscale.VIPService, error) {
c.Lock()
defer c.Unlock()
if c.vipServices == nil {
@@ -887,17 +888,17 @@ func (c *fakeTSClient) getVIPServiceByName(ctx context.Context, name string) (*V
return svc, nil
}
func (c *fakeTSClient) createOrUpdateVIPServiceByName(ctx context.Context, svc *VIPService) error {
func (c *fakeTSClient) CreateOrUpdateVIPService(ctx context.Context, svc *tailscale.VIPService) error {
c.Lock()
defer c.Unlock()
if c.vipServices == nil {
c.vipServices = make(map[string]*VIPService)
c.vipServices = make(map[tailcfg.ServiceName]*tailscale.VIPService)
}
c.vipServices[svc.Name] = svc
return nil
}
func (c *fakeTSClient) deleteVIPServiceByName(ctx context.Context, name string) error {
func (c *fakeTSClient) DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
c.Lock()
defer c.Unlock()
if c.vipServices != nil {

View File

@@ -6,18 +6,13 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/util/httpm"
"tailscale.com/internal/client/tailscale"
"tailscale.com/tailcfg"
)
// defaultTailnet is a value that can be used in Tailscale API calls instead of tailnet name to indicate that the API
@@ -44,142 +39,14 @@ func newTSClient(ctx context.Context, clientIDPath, clientSecretPath string) (ts
c := tailscale.NewClient(defaultTailnet, nil)
c.UserAgent = "tailscale-k8s-operator"
c.HTTPClient = credentials.Client(ctx)
tsc := &tsClientImpl{
Client: c,
baseURL: defaultBaseURL,
tailnet: defaultTailnet,
}
return tsc, nil
return c, nil
}
type tsClient interface {
CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error)
Device(ctx context.Context, deviceID string, fields *tailscale.DeviceFieldsOpts) (*tailscale.Device, error)
DeleteDevice(ctx context.Context, nodeStableID string) error
getVIPServiceByName(ctx context.Context, name string) (*VIPService, error)
createOrUpdateVIPServiceByName(ctx context.Context, svc *VIPService) error
deleteVIPServiceByName(ctx context.Context, name string) error
}
type tsClientImpl struct {
*tailscale.Client
baseURL string
tailnet string
}
// VIPService is a Tailscale VIPService with Tailscale API JSON representation.
type VIPService struct {
// Name is the leftmost label of the DNS name of the VIP service.
// Name is required.
Name string `json:"name,omitempty"`
// Addrs are the IP addresses of the VIP Service. There are two addresses:
// the first is IPv4 and the second is IPv6.
// When creating a new VIP Service, the IP addresses are optional: if no
// addresses are specified then they will be selected. If an IPv4 address is
// specified at index 0, then that address will attempt to be used. An IPv6
// address can not be specified upon creation.
Addrs []string `json:"addrs,omitempty"`
// Comment is an optional text string for display in the admin panel.
Comment string `json:"comment,omitempty"`
// Ports are the ports of a VIPService that will be configured via Tailscale serve config.
// If set, any node wishing to advertise this VIPService must have this port configured via Tailscale serve.
Ports []string `json:"ports,omitempty"`
// Tags are optional ACL tags that will be applied to the VIPService.
Tags []string `json:"tags,omitempty"`
}
// GetVIPServiceByName retrieves a VIPService by its name. It returns 404 if the VIPService is not found.
func (c *tsClientImpl) getVIPServiceByName(ctx context.Context, name string) (*VIPService, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/by-name/%s", c.baseURL, c.tailnet, url.PathEscape(name))
req, err := http.NewRequestWithContext(ctx, httpm.GET, path, nil)
if err != nil {
return nil, fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return nil, fmt.Errorf("error making Tailsale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
}
svc := &VIPService{}
if err := json.Unmarshal(b, svc); err != nil {
return nil, err
}
return svc, nil
}
// CreateOrUpdateVIPServiceByName creates or updates a VIPService by its name. Caller must ensure that, if the
// VIPService already exists, the VIPService is fetched first to ensure that any auto-allocated IP addresses are not
// lost during the update. If the VIPService was created without any IP addresses explicitly set (so that they were
// auto-allocated by Tailscale) any subsequent request to this function that does not set any IP addresses will error.
func (c *tsClientImpl) createOrUpdateVIPServiceByName(ctx context.Context, svc *VIPService) error {
data, err := json.Marshal(svc)
if err != nil {
return err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/by-name/%s", c.baseURL, c.tailnet, url.PathEscape(svc.Name))
req, err := http.NewRequestWithContext(ctx, httpm.PUT, path, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// DeleteVIPServiceByName deletes a VIPService by its name. It returns an error if the VIPService
// does not exist or if the deletion fails.
func (c *tsClientImpl) deleteVIPServiceByName(ctx context.Context, name string) error {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/vip-services/by-name/%s", c.baseURL, c.tailnet, url.PathEscape(name))
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := c.sendRequest(req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
}
return nil
}
// sendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *tsClientImpl) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
resp, err := c.Do(req)
if err != nil {
return nil, resp, fmt.Errorf("error actually doing request: %w", err)
}
defer resp.Body.Close()
// Read response
b, err := io.ReadAll(resp.Body)
if err != nil {
err = fmt.Errorf("error reading response body: %v", err)
}
return b, resp, err
}
// handleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
func handleErrorResponse(b []byte, resp *http.Response) error {
var errResp tailscale.ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return err
}
errResp.Status = resp.StatusCode
return errResp
GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*tailscale.VIPService, error)
CreateOrUpdateVIPService(ctx context.Context, svc *tailscale.VIPService) error
DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error
}

View File

@@ -0,0 +1,48 @@
package main
import (
"encoding/json"
"fmt"
"io"
"github.com/hashicorp/raft"
"tailscale.com/tsconsensus"
)
// fulfil the raft lib functional state machine interface
type fsm ipPool
type fsmSnapshot struct{}
func (f *fsm) Apply(l *raft.Log) interface{} {
var c tsconsensus.Command
if err := json.Unmarshal(l.Data, &c); err != nil {
panic(fmt.Sprintf("failed to unmarshal command: %s", err.Error()))
}
switch c.Name {
case "checkoutAddr":
return f.executeCheckoutAddr(c.Args)
case "markLastUsed":
return f.executeMarkLastUsed(c.Args)
default:
panic(fmt.Sprintf("unrecognized command: %s", c.Name))
}
}
func (f *fsm) Snapshot() (raft.FSMSnapshot, error) {
panic("Snapshot unexpectedly used")
return nil, nil
}
func (f *fsm) Restore(rc io.ReadCloser) error {
panic("Restore unexpectedly used")
return nil
}
func (f *fsmSnapshot) Persist(sink raft.SnapshotSink) error {
panic("Persist unexpectedly used")
return nil
}
func (f *fsmSnapshot) Release() {
panic("Release unexpectedly used")
}

View File

@@ -0,0 +1,278 @@
package main
import (
"context"
"encoding/json"
"errors"
"log"
"net/netip"
"sync"
"time"
"github.com/gaissmai/bart"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tsconsensus"
"tailscale.com/tsnet"
"tailscale.com/util/mak"
)
/*
An ipPool is a group of one or more IPV4 ranges from which individual IPV4 addresses can be
checked out.
natc-consensus provides per domain router functionality for a tailnet.
- when a node does a dns lookup for a domain the natc-consensus handles, natc-consensus asks ipPool for an IP address
for that node and domain. When ipPool
- when a node sends traffic to the IP address it has for a domain, natc-consensus asks ipPool which domain that traffic
is for.
- when an IP address hasn't been used for a while ipPool forgets about that node-ip-domain mapping and may provide
that IP address to that node in response to a subsequent DNS request.
The pool is distributed across servers in a cluster, to provide high availability.
Each tailcfg.NodeID has the full range available. The same IPV4 address will be provided to different nodes.
ipPool will maintain the node-ip-domain mapping until it expires, and won't hand out the IP address to that node
again while it maintains the mapping.
Reading from the pool is fast, writing to the pool is slow. Because reads can be done in memory on the server that got
the traffic, but writes must be sent to the consensus peers.
To handle expiry we write on reads, to update the last-used-date, but we do that after we've returned a response.
ipPool.DomainForIP gets the domain associated with a previous IP checkout for a node
ipPool.IPForDomain gets an IP address for the node+domain. It will return an IP address from any existing mapping,
or it may create a mapping with a new unused IP address.
*/
type ipPool struct {
perPeerMap syncs.Map[tailcfg.NodeID, *perPeerState]
v4Ranges []netip.Prefix
dnsAddr netip.Addr
consensus *tsconsensus.Consensus
}
func (ipp *ipPool) DomainForIP(from tailcfg.NodeID, addr netip.Addr, updatedAt time.Time) string {
// TODO lock
pm, ok := ipp.perPeerMap.Load(from)
if !ok {
log.Printf("DomainForIP: peer state absent for: %d", from)
return ""
}
ww, ok := pm.AddrToDomain.Lookup(addr)
if !ok {
log.Printf("DomainForIP: peer state doesn't recognize domain")
return ""
}
go func() {
err := ipp.markLastUsed(from, addr, ww.Domain, updatedAt)
if err != nil {
panic(err)
}
}()
return ww.Domain
}
type markLastUsedArgs struct {
NodeID tailcfg.NodeID
Addr netip.Addr
Domain string
UpdatedAt time.Time
}
// called by raft
func (cd *fsm) executeMarkLastUsed(bs []byte) tsconsensus.CommandResult {
var args markLastUsedArgs
err := json.Unmarshal(bs, &args)
if err != nil {
return tsconsensus.CommandResult{Err: err}
}
err = cd.applyMarkLastUsed(args.NodeID, args.Addr, args.Domain, args.UpdatedAt)
if err != nil {
return tsconsensus.CommandResult{Err: err}
}
return tsconsensus.CommandResult{}
}
func (ipp *fsm) applyMarkLastUsed(from tailcfg.NodeID, addr netip.Addr, domain string, updatedAt time.Time) error {
// TODO lock
ps, ok := ipp.perPeerMap.Load(from)
if !ok {
// There's nothing to mark. But this is unexpected, because we mark last used after we do things with peer state.
log.Printf("applyMarkLastUsed: could not find peer state, nodeID: %s", from)
return nil
}
ww, ok := ps.AddrToDomain.Lookup(addr)
if !ok {
// The peer state didn't have an entry for the IP address (possibly it expired), so there's nothing to mark.
return nil
}
if ww.Domain != domain {
// The IP address expired and was reused for a new domain. Don't mark.
return nil
}
if ww.LastUsed.After(updatedAt) {
// This has been marked more recently. Don't mark.
return nil
}
ww.LastUsed = updatedAt
ps.AddrToDomain.Insert(netip.PrefixFrom(addr, addr.BitLen()), ww)
return nil
}
func (ipp *ipPool) StartConsensus(ctx context.Context, ts *tsnet.Server, clusterTag string) error {
cns, err := tsconsensus.Start(ctx, ts, (*fsm)(ipp), clusterTag, tsconsensus.DefaultConfig(), true)
if err != nil {
return err
}
ipp.consensus = cns
return nil
}
type whereWhen struct {
Domain string
LastUsed time.Time
}
type perPeerState struct {
DomainToAddr map[string]netip.Addr
AddrToDomain *bart.Table[whereWhen]
mu sync.Mutex // not jsonified
}
func (ps *perPeerState) unusedIPV4(ranges []netip.Prefix, exclude netip.Addr, reuseDeadline time.Time) (netip.Addr, bool, string, error) {
// TODO here we iterate through each ip within the ranges until we find one that's unused
// could be done more efficiently either by:
// 1) storing an index into ranges and an ip we had last used from that range in perPeerState
// (how would this work with checking ips back into the pool though?)
// 2) using a random approach like the natc does now, except the raft state machine needs to
// be deterministic so it can replay logs, so I think we would do something like generate a
// random ip each time, and then have a call into the state machine that says "give me whatever
// ip you have, and if you don't have one use this one". I think that would work.
for _, r := range ranges {
ip := r.Addr()
for r.Contains(ip) {
if ip != exclude {
ww, ok := ps.AddrToDomain.Lookup(ip)
if !ok {
return ip, false, "", nil
}
if ww.LastUsed.Before(reuseDeadline) {
return ip, true, ww.Domain, nil
}
}
ip = ip.Next()
}
}
return netip.Addr{}, false, "", errors.New("ip pool exhausted")
}
func (cd *ipPool) IpForDomain(nid tailcfg.NodeID, domain string) (netip.Addr, error) {
now := time.Now()
args := checkoutAddrArgs{
NodeID: nid,
Domain: domain,
ReuseDeadline: now.Add(-10 * time.Second), // TODO what time period? 48 hours?
UpdatedAt: now,
}
bs, err := json.Marshal(args)
if err != nil {
return netip.Addr{}, err
}
c := tsconsensus.Command{
Name: "checkoutAddr",
Args: bs,
}
result, err := cd.consensus.ExecuteCommand(c)
if err != nil {
log.Printf("IpForDomain: raft error executing command: %v", err)
return netip.Addr{}, err
}
if result.Err != nil {
log.Printf("IpForDomain: error returned from state machine: %v", err)
return netip.Addr{}, result.Err
}
var addr netip.Addr
err = json.Unmarshal(result.Result, &addr)
return addr, err
}
func (cd *ipPool) markLastUsed(nid tailcfg.NodeID, addr netip.Addr, domain string, lastUsed time.Time) error {
args := markLastUsedArgs{
NodeID: nid,
Addr: addr,
Domain: domain,
UpdatedAt: lastUsed,
}
bs, err := json.Marshal(args)
if err != nil {
return err
}
//c := command{
c := tsconsensus.Command{
Name: "markLastUsed",
Args: bs,
}
result, err := cd.consensus.ExecuteCommand(c)
if err != nil {
log.Printf("markLastUsed: raft error executing command: %v", err)
return err
}
if result.Err != nil {
log.Printf("markLastUsed: error returned from state machine: %v", err)
return result.Err
}
return nil
}
type checkoutAddrArgs struct {
NodeID tailcfg.NodeID
Domain string
ReuseDeadline time.Time
UpdatedAt time.Time
}
// called by raft
func (cd *fsm) executeCheckoutAddr(bs []byte) tsconsensus.CommandResult {
var args checkoutAddrArgs
err := json.Unmarshal(bs, &args)
if err != nil {
return tsconsensus.CommandResult{Err: err}
}
addr, err := cd.applyCheckoutAddr(args.NodeID, args.Domain, args.ReuseDeadline, args.UpdatedAt)
if err != nil {
return tsconsensus.CommandResult{Err: err}
}
resultBs, err := json.Marshal(addr)
if err != nil {
return tsconsensus.CommandResult{Err: err}
}
return tsconsensus.CommandResult{Result: resultBs}
}
func (cd *fsm) applyCheckoutAddr(nid tailcfg.NodeID, domain string, reuseDeadline, updatedAt time.Time) (netip.Addr, error) {
// TODO lock and unlock
pm, _ := cd.perPeerMap.LoadOrStore(nid, &perPeerState{
AddrToDomain: &bart.Table[whereWhen]{},
})
if existing, ok := pm.DomainToAddr[domain]; ok {
// TODO handle error case where this doesn't exist
ww, _ := pm.AddrToDomain.Lookup(existing)
ww.LastUsed = updatedAt
pm.AddrToDomain.Insert(netip.PrefixFrom(existing, existing.BitLen()), ww)
return existing, nil
}
addr, wasInUse, previousDomain, err := pm.unusedIPV4(cd.v4Ranges, cd.dnsAddr, reuseDeadline)
if err != nil {
return netip.Addr{}, err
}
mak.Set(&pm.DomainToAddr, domain, addr)
if wasInUse {
// remove it from domaintoaddr
delete(pm.DomainToAddr, previousDomain)
// don't need to remove it from addrtodomain, insert will do that
}
pm.AddrToDomain.Insert(netip.PrefixFrom(addr, addr.BitLen()), whereWhen{Domain: domain, LastUsed: updatedAt})
return addr, nil
}

View File

@@ -0,0 +1,100 @@
package main
import (
"net/netip"
"testing"
"time"
"tailscale.com/tailcfg"
)
func TestV6V4(t *testing.T) {
c := connector{
v6ULA: ula(uint16(1)),
}
tests := [][]string{
[]string{"100.64.0.0", "fd7a:115c:a1e0:a99c:1:0:6440:0"},
[]string{"0.0.0.0", "fd7a:115c:a1e0:a99c:1::"},
[]string{"255.255.255.255", "fd7a:115c:a1e0:a99c:1:0:ffff:ffff"},
}
for i, test := range tests {
// to v6
v6 := c.v6ForV4(netip.MustParseAddr(test[0]))
want := netip.MustParseAddr(test[1])
if v6 != want {
t.Fatalf("test %d: want: %v, got: %v", i, want, v6)
}
// to v4
v4 := v4ForV6(netip.MustParseAddr(test[1]))
want = netip.MustParseAddr(test[0])
if v4 != want {
t.Fatalf("test %d: want: %v, got: %v", i, want, v4)
}
}
}
func TestIPForDomain(t *testing.T) {
pfx := netip.MustParsePrefix("100.64.0.0/16")
ipp := fsm{
v4Ranges: []netip.Prefix{pfx},
dnsAddr: netip.MustParseAddr("100.64.0.0"),
}
now := time.Now()
deadline := now.Add(-2 * time.Hour)
a, err := ipp.applyCheckoutAddr(tailcfg.NodeID(1), "example.com", deadline, now)
if err != nil {
t.Fatal(err)
}
if !pfx.Contains(a) {
t.Fatalf("expected %v to be in the prefix %v", a, pfx)
}
b, err := ipp.applyCheckoutAddr(tailcfg.NodeID(1), "a.example.com", deadline, now)
if err != nil {
t.Fatal(err)
}
if !pfx.Contains(b) {
t.Fatalf("expected %v to be in the prefix %v", b, pfx)
}
if b == a {
t.Fatalf("same address issued twice %v, %v", a, b)
}
c, err := ipp.applyCheckoutAddr(tailcfg.NodeID(1), "example.com", deadline, now)
if err != nil {
t.Fatal(err)
}
if c != a {
t.Fatalf("expected %v to be remembered as the addr for example.com, but got %v", a, c)
}
}
func TestDomainForIP(t *testing.T) {
pfx := netip.MustParsePrefix("100.64.0.0/16")
sm := fsm{
v4Ranges: []netip.Prefix{pfx},
dnsAddr: netip.MustParseAddr("100.64.0.0"),
}
ipp := (*ipPool)(&sm)
nid := tailcfg.NodeID(1)
domain := "example.com"
now := time.Now()
deadline := now.Add(-2 * time.Hour)
d := ipp.DomainForIP(nid, netip.MustParseAddr("100.64.0.1"), now)
if d != "" {
t.Fatalf("expected an empty string if the addr is not found but got %s", d)
}
a, err := sm.applyCheckoutAddr(nid, domain, deadline, now)
if err != nil {
t.Fatal(err)
}
d2 := ipp.DomainForIP(nid, a, now)
if d2 != domain {
t.Fatalf("expected %s but got %s", domain, d2)
}
}

504
cmd/natc-consensus/natc.go Normal file
View File

@@ -0,0 +1,504 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The natc command is a work-in-progress implementation of a NAT based
// connector for Tailscale. It is intended to be used to route traffic to a
// specific domain through a specific node.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/netip"
"os"
"strings"
"time"
"github.com/gaissmai/bart"
"github.com/inetaf/tcpproxy"
"github.com/peterbourgon/ff/v3"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
)
func main() {
hostinfo.SetApp("natc")
if !envknob.UseWIPCode() {
log.Fatal("cmd/natc-consensus is a work in progress and has not been security reviewed;\nits use requires TAILSCALE_USE_WIP_CODE=1 be set in the environment for now.")
}
// Parse flags
fs := flag.NewFlagSet("natc", flag.ExitOnError)
var (
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
hostname = fs.String("hostname", "", "Hostname to register the service under")
siteID = fs.Uint("site-id", 1, "an integer site ID to use for the ULA prefix which allows for multiple proxies to act in a HA configuration")
v4PfxStr = fs.String("v4-pfx", "100.64.1.0/24", "comma-separated list of IPv4 prefixes to advertise")
verboseTSNet = fs.Bool("verbose-tsnet", false, "enable verbose logging in tsnet")
printULA = fs.Bool("print-ula", false, "print the ULA prefix and exit")
ignoreDstPfxStr = fs.String("ignore-destinations", "", "comma-separated list of prefixes to ignore")
wgPort = fs.Uint("wg-port", 0, "udp port for wireguard and peer to peer traffic")
clusterTag = fs.String("cluster-tag", "", "TODO")
)
ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_NATC"))
if *printULA {
fmt.Println(ula(uint16(*siteID)))
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if *siteID == 0 {
log.Fatalf("site-id must be set")
} else if *siteID > 0xffff {
log.Fatalf("site-id must be in the range [0, 65535]")
}
var ignoreDstTable *bart.Table[bool]
for _, s := range strings.Split(*ignoreDstPfxStr, ",") {
s := strings.TrimSpace(s)
if s == "" {
continue
}
if ignoreDstTable == nil {
ignoreDstTable = &bart.Table[bool]{}
}
pfx, err := netip.ParsePrefix(s)
if err != nil {
log.Fatalf("unable to parse prefix: %v", err)
}
if pfx.Masked() != pfx {
log.Fatalf("prefix %v is not normalized (bits are set outside the mask)", pfx)
}
ignoreDstTable.Insert(pfx, true)
}
var v4Prefixes []netip.Prefix
for _, s := range strings.Split(*v4PfxStr, ",") {
p := netip.MustParsePrefix(strings.TrimSpace(s))
if p.Masked() != p {
log.Fatalf("v4 prefix %v is not a masked prefix", p)
}
v4Prefixes = append(v4Prefixes, p)
}
if len(v4Prefixes) == 0 {
log.Fatalf("no v4 prefixes specified")
}
dnsAddr := v4Prefixes[0].Addr()
ts := &tsnet.Server{
Hostname: *hostname,
}
ts.ControlURL = "http://host.docker.internal:31544" // TODO
if *wgPort != 0 {
if *wgPort >= 1<<16 {
log.Fatalf("wg-port must be in the range [0, 65535]")
}
ts.Port = uint16(*wgPort)
}
defer ts.Close()
if *verboseTSNet {
ts.Logf = log.Printf
}
// Start special-purpose listeners: dns, http promotion, debug server
if *debugPort != 0 {
mux := http.NewServeMux()
tsweb.Debugger(mux)
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
if err != nil {
log.Fatalf("failed listening on debug port: %v", err)
}
defer dln.Close()
go func() {
log.Fatalf("debug serve: %v", http.Serve(dln, mux))
}()
}
lc, err := ts.LocalClient()
if err != nil {
log.Fatalf("LocalClient() failed: %v", err)
}
if _, err := ts.Up(ctx); err != nil {
log.Fatalf("ts.Up: %v", err)
}
ipp := ipPool{
v4Ranges: v4Prefixes,
dnsAddr: dnsAddr,
}
err = ipp.StartConsensus(ctx, ts, *clusterTag)
if err != nil {
log.Fatalf("StartConsensus: %v", err)
}
defer ipp.consensus.Stop(ctx)
c := &connector{
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
v6ULA: ula(uint16(*siteID)),
ignoreDsts: ignoreDstTable,
ipAddrs: &ipp,
}
c.run(ctx)
}
type connector struct {
// ts is the tsnet.Server used to host the connector.
ts *tsnet.Server
// lc is the LocalClient used to interact with the tsnet.Server hosting this
// connector.
lc *tailscale.LocalClient
// dnsAddr is the IPv4 address to listen on for DNS requests. It is used to
// prevent the app connector from assigning it to a domain.
dnsAddr netip.Addr
// v4Ranges is the list of IPv4 ranges to advertise and assign addresses from.
// These are masked prefixes.
v4Ranges []netip.Prefix
// v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
v6ULA netip.Prefix
ipAddrs *ipPool
// ignoreDsts is initialized at start up with the contents of --ignore-destinations (if none it is nil)
// It is never mutated, only used for lookups.
// Users who want to natc a DNS wildcard but not every address record in that domain can supply the
// exceptions in --ignore-destinations. When we receive a dns request we will look up the fqdn
// and if any of the ip addresses in response to the lookup match any 'ignore destinations' prefix we will
// return a dns response that contains the ip addresses we discovered with the lookup (ie not the
// natc behavior, which would return a dummy ip address pointing at natc).
ignoreDsts *bart.Table[bool]
}
// v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
// The 8th and 9th bytes are used to encode the site ID which allows for
// multiple proxies to act in a HA configuration.
// mnemonic: a99c = appc
var v6ULA = netip.MustParsePrefix("fd7a:115c:a1e0:a99c::/64")
func ula(siteID uint16) netip.Prefix {
as16 := v6ULA.Addr().As16()
as16[8] = byte(siteID >> 8)
as16[9] = byte(siteID)
return netip.PrefixFrom(netip.AddrFrom16(as16), 64+16)
}
// run runs the connector.
//
// The passed in context is only used for the initial setup. The connector runs
// forever.
func (c *connector) run(ctx context.Context) {
if _, err := c.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
AdvertiseRoutesSet: true,
Prefs: ipn.Prefs{
AdvertiseRoutes: append(c.v4Ranges, c.v6ULA),
},
}); err != nil {
log.Fatalf("failed to advertise routes: %v", err)
}
c.ts.RegisterFallbackTCPHandler(c.handleTCPFlow)
c.serveDNS()
}
func (c *connector) serveDNS() {
pc, err := c.ts.ListenPacket("udp", net.JoinHostPort(c.dnsAddr.String(), "53"))
if err != nil {
log.Fatalf("failed listening on port 53: %v", err)
}
defer pc.Close()
log.Printf("Listening for DNS on %s", pc.LocalAddr().String())
for {
buf := make([]byte, 1500)
n, addr, err := pc.ReadFrom(buf)
if err != nil {
if errors.Is(err, net.ErrClosed) {
return
}
log.Printf("serveDNS.ReadFrom failed: %v", err)
continue
}
go c.handleDNS(pc, buf[:n], addr.(*net.UDPAddr))
}
}
func lookupDestinationIP(domain string) ([]netip.Addr, error) {
netIPs, err := net.LookupIP(domain)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
return nil, nil
} else {
return nil, err
}
}
var addrs []netip.Addr
for _, ip := range netIPs {
a, ok := netip.AddrFromSlice(ip)
if ok {
addrs = append(addrs, a)
}
}
return addrs, nil
}
// handleDNS handles a DNS request to the app connector.
// It generates a response based on the request and the node that sent it.
//
// Each node is assigned a unique pair of IP addresses for each domain it
// queries. This assignment is done lazily and is not persisted across restarts.
// A per-peer assignment allows the connector to reuse a limited number of IP
// addresses across multiple nodes and domains. It also allows for clear
// failover behavior when an app connector is restarted.
//
// This assignment later allows the connector to determine where to forward
// traffic based on the destination IP address.
func (c *connector) handleDNS(pc net.PacketConn, buf []byte, remoteAddr *net.UDPAddr) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
who, err := c.lc.WhoIs(ctx, remoteAddr.String())
if err != nil {
log.Printf("HandleDNS: WhoIs failed: %v\n", err)
return
}
var msg dnsmessage.Message
err = msg.Unpack(buf)
if err != nil {
log.Printf("HandleDNS: dnsmessage unpack failed: %v\n ", err)
return
}
// If there are destination ips that we don't want to route, we
// have to do a dns lookup here to find the destination ip.
if c.ignoreDsts != nil {
if len(msg.Questions) > 0 {
q := msg.Questions[0]
switch q.Type {
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
dstAddrs, err := lookupDestinationIP(q.Name.String())
if err != nil {
log.Printf("HandleDNS: lookup destination failed: %v\n ", err)
return
}
if c.ignoreDestination(dstAddrs) {
bs, err := dnsResponse(&msg, dstAddrs)
// TODO (fran): treat as SERVFAIL
if err != nil {
log.Printf("HandleDNS: generate ignore response failed: %v\n", err)
return
}
_, err = pc.WriteTo(bs, remoteAddr)
if err != nil {
log.Printf("HandleDNS: write failed: %v\n", err)
}
return
}
}
}
}
// None of the destination IP addresses match an ignore destination prefix, do
// the natc thing.
resp, err := c.generateDNSResponse(&msg, who.Node.ID)
// TODO (fran): treat as SERVFAIL
if err != nil {
log.Printf("HandleDNS: connector handling failed: %v\n", err)
return
}
// TODO (fran): treat as NXDOMAIN
if len(resp) == 0 {
return
}
// This connector handled the DNS request
_, err = pc.WriteTo(resp, remoteAddr)
if err != nil {
log.Printf("HandleDNS: write failed: %v\n", err)
}
}
// tsMBox is the mailbox used in SOA records.
// The convention is to replace the @ symbol with a dot.
// So in this case, the mailbox is support.tailscale.com. with the trailing dot
// to indicate that it is a fully qualified domain name.
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
// generateDNSResponse generates a DNS response for the given request. The from
// argument is the NodeID of the node that sent the request.
func (c *connector) generateDNSResponse(req *dnsmessage.Message, from tailcfg.NodeID) ([]byte, error) {
var addrs []netip.Addr
if len(req.Questions) > 0 {
switch req.Questions[0].Type {
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
v4, err := c.ipAddrs.IpForDomain(from, req.Questions[0].Name.String())
if err != nil {
return nil, err
}
addrs = []netip.Addr{v4, c.v6ForV4(v4)}
}
}
return dnsResponse(req, addrs)
}
// dnsResponse makes a DNS response for the natc. If the dnsmessage is requesting TypeAAAA
// or TypeA the provided addrs of the requested type will be used.
func dnsResponse(req *dnsmessage.Message, addrs []netip.Addr) ([]byte, error) {
b := dnsmessage.NewBuilder(nil,
dnsmessage.Header{
ID: req.Header.ID,
Response: true,
Authoritative: true,
})
b.EnableCompression()
if len(req.Questions) == 0 {
return b.Finish()
}
q := req.Questions[0]
if err := b.StartQuestions(); err != nil {
return nil, err
}
if err := b.Question(q); err != nil {
return nil, err
}
if err := b.StartAnswers(); err != nil {
return nil, err
}
switch q.Type {
case dnsmessage.TypeAAAA, dnsmessage.TypeA:
want6 := q.Type == dnsmessage.TypeAAAA
for _, ip := range addrs {
if want6 != ip.Is6() {
continue
}
if want6 {
if err := b.AAAAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AAAAResource{AAAA: ip.As16()},
); err != nil {
return nil, err
}
} else {
if err := b.AResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 5},
dnsmessage.AResource{A: ip.As4()},
); err != nil {
return nil, err
}
}
}
case dnsmessage.TypeSOA:
if err := b.SOAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
); err != nil {
return nil, err
}
case dnsmessage.TypeNS:
if err := b.NSResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.NSResource{NS: tsMBox},
); err != nil {
return nil, err
}
}
return b.Finish()
}
// handleTCPFlow handles a TCP flow from the given source to the given
// destination. It uses the source address to determine the node that sent the
// request and the destination address to determine the domain that the request
// is for based on the IP address assigned to the destination in the DNS
// response.
func (c *connector) handleTCPFlow(src, dst netip.AddrPort) (handler func(net.Conn), intercept bool) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
who, err := c.lc.WhoIs(ctx, src.Addr().String())
cancel()
if err != nil {
log.Printf("HandleTCPFlow: WhoIs failed: %v\n", err)
return nil, false
}
from := who.Node.ID
dstAddr := dst.Addr()
if dstAddr.Is6() {
dstAddr = v4ForV6(dstAddr)
}
domain := c.ipAddrs.DomainForIP(from, dstAddr, time.Now())
if domain == "" {
log.Print("handleTCPFlow: found no domain")
return nil, false
}
return func(conn net.Conn) {
proxyTCPConn(conn, domain)
}, true
}
// ignoreDestination reports whether any of the provided dstAddrs match the prefixes configured
// in --ignore-destinations
func (c *connector) ignoreDestination(dstAddrs []netip.Addr) bool {
for _, a := range dstAddrs {
if _, ok := c.ignoreDsts.Lookup(a); ok {
return true
}
}
return false
}
func proxyTCPConn(c net.Conn, dest string) {
if c.RemoteAddr() == nil {
log.Printf("proxyTCPConn: nil RemoteAddr")
c.Close()
return
}
addrPortStr := c.LocalAddr().String()
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("tcpRoundRobinHandler.Handle: bogus addrPort %q", addrPortStr)
c.Close()
return
}
p := &tcpproxy.Proxy{
ListenFunc: func(net, laddr string) (net.Listener, error) {
return netutil.NewOneConnListener(c, nil), nil
},
}
p.AddRoute(addrPortStr, &tcpproxy.DialProxy{
Addr: fmt.Sprintf("%s:%s", dest, port),
})
p.Start()
}
func (c *connector) v6ForV4(v4 netip.Addr) netip.Addr {
as16 := c.v6ULA.Addr().As16()
as4 := v4.As4()
copy(as16[12:], as4[:])
v6 := netip.AddrFrom16(as16)
return v6
}
func v4ForV6(v6 netip.Addr) netip.Addr {
as16 := v6.As16()
var as4 [4]byte
copy(as4[:], as16[12:])
v4 := netip.AddrFrom4(as4)
return v4
}

View File

@@ -27,9 +27,7 @@ import (
"github.com/inetaf/tcpproxy"
"github.com/peterbourgon/ff/v3"
"golang.org/x/net/dns/dnsmessage"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
@@ -140,26 +138,6 @@ func main() {
}
// TODO(raggi): this is not a public interface or guarantee.
ns := ts.Sys().Netstack.Get().(*netstack.Impl)
tcpRXBufOpt := tcpip.TCPReceiveBufferSizeRangeOption{
Min: tcp.MinBufferSize,
Default: tcp.DefaultReceiveBufferSize,
Max: tcp.MaxBufferSize,
}
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpRXBufOpt); err != nil {
log.Fatalf("could not set TCP RX buf size: %v", err)
}
tcpTXBufOpt := tcpip.TCPSendBufferSizeRangeOption{
Min: tcp.MinBufferSize,
Default: tcp.DefaultSendBufferSize,
Max: tcp.MaxBufferSize,
}
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &tcpTXBufOpt); err != nil {
log.Fatalf("could not set TCP TX buf size: %v", err)
}
mslOpt := tcpip.TCPTimeWaitTimeoutOption(5 * time.Second)
if err := ns.SetTransportProtocolOption(tcp.ProtocolNumber, &mslOpt); err != nil {
log.Fatalf("could not set TCP MSL: %v", err)
}
if *debugPort != 0 {
expvar.Publish("netstack", ns.ExpVar())
}
@@ -186,9 +164,9 @@ func main() {
type connector struct {
// ts is the tsnet.Server used to host the connector.
ts *tsnet.Server
// lc is the LocalClient used to interact with the tsnet.Server hosting this
// lc is the local.Client used to interact with the tsnet.Server hosting this
// connector.
lc *tailscale.LocalClient
lc *local.Client
// dnsAddr is the IPv4 address to listen on for DNS requests. It is used to
// prevent the app connector from assigning it to a domain.

View File

@@ -24,7 +24,7 @@ import (
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/metrics"
"tailscale.com/tsnet"
"tailscale.com/tsweb"
@@ -105,7 +105,7 @@ type proxy struct {
upstreamHost string // "my.database.com"
upstreamCertPool *x509.CertPool
downstreamCert []tls.Certificate
client *tailscale.LocalClient
client *local.Client
activeSessions expvar.Int
startedSessions expvar.Int
@@ -115,7 +115,7 @@ type proxy struct {
// newProxy returns a proxy that forwards connections to
// upstreamAddr. The upstream's TLS session is verified using the CA
// cert(s) in upstreamCAPath.
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
func newProxy(upstreamAddr, upstreamCAPath string, client *local.Client) (*proxy, error) {
bs, err := os.ReadFile(upstreamCAPath)
if err != nil {
return nil, err

View File

@@ -36,7 +36,7 @@ import (
"strings"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
)
@@ -127,7 +127,7 @@ func main() {
log.Fatal(http.Serve(ln, proxy))
}
func modifyRequest(req *http.Request, localClient *tailscale.LocalClient) {
func modifyRequest(req *http.Request, localClient *local.Client) {
// with enable_login_token set to true, we get a cookie that handles
// auth for paths that are not /login
if req.URL.Path != "/login" {
@@ -144,7 +144,7 @@ func modifyRequest(req *http.Request, localClient *tailscale.LocalClient) {
req.Header.Set("X-Webauth-Name", user.DisplayName)
}
func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, ipPort string) (*tailcfg.UserProfile, error) {
func getTailscaleUser(ctx context.Context, localClient *local.Client, ipPort string) (*tailcfg.UserProfile, error) {
whois, err := localClient.WhoIs(ctx, ipPort)
if err != nil {
return nil, fmt.Errorf("failed to identify remote host: %w", err)

View File

@@ -22,7 +22,7 @@ import (
"strings"
"github.com/peterbourgon/ff/v3"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
@@ -157,10 +157,8 @@ func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, pro
// NetMap contains app-connector configuration
if nm := msg.NetMap; nm != nil && nm.SelfNode.Valid() {
sn := nm.SelfNode.AsStruct()
var c appctype.AppConnectorConfig
nmConf, err := tailcfg.UnmarshalNodeCapJSON[appctype.AppConnectorConfig](sn.CapMap, configCapKey)
nmConf, err := tailcfg.UnmarshalNodeCapViewJSON[appctype.AppConnectorConfig](nm.SelfNode.CapMap(), configCapKey)
if err != nil {
log.Printf("failed to read app connector configuration from coordination server: %v", err)
} else if len(nmConf) > 0 {
@@ -185,7 +183,7 @@ func run(ctx context.Context, ts *tsnet.Server, wgPort int, hostname string, pro
type sniproxy struct {
srv Server
ts *tsnet.Server
lc *tailscale.LocalClient
lc *local.Client
}
func (s *sniproxy) advertiseRoutesFromConfig(ctx context.Context, c *appctype.AppConnectorConfig) error {

View File

@@ -6,6 +6,9 @@
// highlight the unique parts of the Tailscale SSH server so SSH
// client authors can hit it easily and fix their SSH clients without
// needing to set up Tailscale and Tailscale SSH.
//
// Connections are allowed using any username except for "denyme". Connecting as
// "denyme" will result in an authentication failure with error message.
package main
import (
@@ -16,6 +19,7 @@ import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"flag"
"fmt"
"io"
@@ -24,7 +28,7 @@ import (
"path/filepath"
"time"
gossh "github.com/tailscale/golang-x-crypto/ssh"
gossh "golang.org/x/crypto/ssh"
"tailscale.com/tempfork/gliderlabs/ssh"
)
@@ -62,13 +66,21 @@ func main() {
Handler: handleSessionPostSSHAuth,
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
start := time.Now()
var spac gossh.ServerPreAuthConn
return &gossh.ServerConfig{
NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
return []string{"tailscale"}
PreAuthConnCallback: func(conn gossh.ServerPreAuthConn) {
spac = conn
},
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
if cm.User() == "denyme" {
return nil, &gossh.BannerError{
Err: errors.New("denying access"),
Message: "denyme is not allowed to access this machine\n",
}
}
totalBanners := 2
if cm.User() == "banners" {
@@ -77,9 +89,9 @@ func main() {
for banner := 2; banner <= totalBanners; banner++ {
time.Sleep(time.Second)
if banner == totalBanners {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
} else {
cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
spac.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
}
}
return nil, nil

View File

@@ -88,13 +88,11 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
@@ -116,7 +114,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/aes from crypto/internal/hpke+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -124,32 +122,59 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/hmac from crypto/tls
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/hpke from crypto/tls
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/subtle from crypto/cipher+
crypto/tls from net/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509
embed from crypto/internal/nistec+
embed from google.golang.org/protobuf/internal/editiondefaults+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/go-json-experiment/json
@@ -169,23 +194,22 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
hash/maphash from go4.org/mem
html from net/http/pprof+
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall
internal/asan from syscall+
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/aes+
internal/byteorder from crypto/cipher+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/aes+
internal/cpu from crypto/internal/fips140deps/cpu+
internal/filepathlite from os+
internal/fmtsort from fmt
internal/goarch from crypto/aes+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/godebug from crypto/tls+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime
internal/goexperiment from runtime+
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall
internal/msan from syscall+
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -195,17 +219,20 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from bufio+
io/fs from crypto/x509+
iter from maps+
@@ -216,7 +243,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 internal/concurrent+
math/rand/v2 from crypto/ecdsa+
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -229,17 +256,15 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os from crypto/internal/sysrand+
os/signal from tailscale.com/cmd/stund
path from github.com/prometheus/client_golang/prometheus/internal+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/prometheus/client_golang/prometheus/internal+
regexp/syntax from regexp
runtime from crypto/internal/nistec+
runtime from crypto/internal/fips140+
runtime/debug from github.com/prometheus/client_golang/prometheus+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
@@ -249,7 +274,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
syscall from crypto/internal/sysrand+
text/tabwriter from runtime/pprof
time from compress/gzip+
unicode from bytes+
@@ -257,3 +282,4 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -21,6 +21,7 @@ import (
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/cmd/tailscale/cli/ffcomplete"
"tailscale.com/envknob"
@@ -79,7 +80,7 @@ func CleanUpArgs(args []string) []string {
return out
}
var localClient = tailscale.LocalClient{
var localClient = local.Client{
Socket: paths.DefaultTailscaledSocket(),
}

View File

@@ -9,6 +9,7 @@ import (
"fmt"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
)
@@ -23,10 +24,12 @@ var downCmd = &ffcli.Command{
var downArgs struct {
acceptedRisks string
reason string
}
func newDownFlagSet() *flag.FlagSet {
downf := newFlagSet("down")
downf.StringVar(&downArgs.reason, "reason", "", "reason for the disconnect, if required by a policy")
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
return downf
}
@@ -50,6 +53,7 @@ func runDown(ctx context.Context, args []string) error {
fmt.Fprintf(Stderr, "Tailscale was already stopped.\n")
return nil
}
ctx = apitype.RequestReasonKey.WithValue(ctx, downArgs.reason)
_, err = localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,

View File

@@ -28,6 +28,7 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/cmd/tailscale/cli/ffcomplete"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/tsaddr"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -268,46 +269,77 @@ func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNode
if err != nil {
return "", false, err
}
fts, err := localClient.FileTargets(ctx)
if err != nil {
return "", false, err
}
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.Addr() != ip {
continue
}
isOffline = n.Online != nil && !*n.Online
return n.StableID, isOffline, nil
}
}
return "", false, fileTargetErrorDetail(ctx, ip)
}
// fileTargetErrorDetail returns a non-nil error saying why ip is an
// invalid file sharing target.
func fileTargetErrorDetail(ctx context.Context, ip netip.Addr) error {
found := false
if st, err := localClient.Status(ctx); err == nil && st.Self != nil {
for _, peer := range st.Peer {
for _, pip := range peer.TailscaleIPs {
if pip == ip {
found = true
if peer.UserID != st.Self.UserID {
return errors.New("owned by different user; can only send files to your own devices")
}
}
st, err := localClient.Status(ctx)
if err != nil {
// This likely means tailscaled is unreachable or returned an error on /localapi/v0/status.
return "", false, fmt.Errorf("failed to get local status: %w", err)
}
if st == nil {
// Handle the case if the daemon returns nil with no error.
return "", false, errors.New("no status available")
}
if st.Self == nil {
// We have a status structure, but it doesnt include Self info. Probably not connected.
return "", false, errors.New("local node is not configured or missing Self information")
}
// Find the PeerStatus that corresponds to ip.
var foundPeer *ipnstate.PeerStatus
peerLoop:
for _, ps := range st.Peer {
for _, pip := range ps.TailscaleIPs {
if pip == ip {
foundPeer = ps
break peerLoop
}
}
}
if found {
return errors.New("target seems to be running an old Tailscale version")
// If we didnt find a matching peer at all:
if foundPeer == nil {
if !tsaddr.IsTailscaleIP(ip) {
return "", false, fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
}
return "", false, errors.New("unknown target; not in your Tailnet")
}
if !tsaddr.IsTailscaleIP(ip) {
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
// We found a peer. Decide whether we can send files to it:
isOffline = !foundPeer.Online
switch foundPeer.TaildropTarget {
case ipnstate.TaildropTargetAvailable:
return foundPeer.ID, isOffline, nil
case ipnstate.TaildropTargetNoNetmapAvailable:
return "", isOffline, errors.New("cannot send files: no netmap available on this node")
case ipnstate.TaildropTargetIpnStateNotRunning:
return "", isOffline, errors.New("cannot send files: local Tailscale is not connected to the tailnet")
case ipnstate.TaildropTargetMissingCap:
return "", isOffline, errors.New("cannot send files: missing required Taildrop capability")
case ipnstate.TaildropTargetOffline:
return "", isOffline, errors.New("cannot send files: peer is offline")
case ipnstate.TaildropTargetNoPeerInfo:
return "", isOffline, errors.New("cannot send files: invalid or unrecognized peer")
case ipnstate.TaildropTargetUnsupportedOS:
return "", isOffline, errors.New("cannot send files: target's OS does not support Taildrop")
case ipnstate.TaildropTargetNoPeerAPI:
return "", isOffline, errors.New("cannot send files: target is not advertising a file sharing API")
case ipnstate.TaildropTargetOwnedByOtherUser:
return "", isOffline, errors.New("cannot send files: peer is owned by a different user")
case ipnstate.TaildropTargetUnknown:
fallthrough
default:
return "", isOffline, fmt.Errorf("cannot send files: unknown or indeterminate reason")
}
return errors.New("unknown target; not in your Tailnet")
}
const maxSniff = 4 << 20

View File

@@ -19,7 +19,7 @@ import (
var funnelCmd = func() *ffcli.Command {
se := &serveEnv{lc: &localClient}
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
// change is limited to make a revert easier and full cleanup to come after the relase.
// change is limited to make a revert easier and full cleanup to come after the release.
// TODO(tylersmalley): cleanup and removal of newFunnelCommand as of 2023-10-16
return newServeV2Command(se, funnel)
}

View File

@@ -130,7 +130,7 @@ func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.Fla
}
// localServeClient is an interface conforming to the subset of
// tailscale.LocalClient. It includes only the methods used by the
// local.Client. It includes only the methods used by the
// serve command.
//
// The purpose of this interface is to allow tests to provide a mock.

View File

@@ -850,7 +850,7 @@ func TestVerifyFunnelEnabled(t *testing.T) {
}
}
// fakeLocalServeClient is a fake tailscale.LocalClient for tests.
// fakeLocalServeClient is a fake local.Client for tests.
// It's not a full implementation, just enough to test the serve command.
//
// The fake client is stateful, and is used to test manipulating

View File

@@ -84,10 +84,6 @@ func runSSH(ctx context.Context, args []string) error {
// of failing. But for now:
return fmt.Errorf("no system 'ssh' command found: %w", err)
}
tailscaleBin, err := os.Executable()
if err != nil {
return err
}
knownHostsFile, err := writeKnownHosts(st)
if err != nil {
return err
@@ -116,7 +112,9 @@ func runSSH(ctx context.Context, args []string) error {
argv = append(argv,
"-o", fmt.Sprintf("ProxyCommand %q %s nc %%h %%p",
tailscaleBin,
// os.Executable() would return the real running binary but in case tailscale is built with the ts_include_cli tag,
// we need to return the started symlink instead
os.Args[0],
socketArg,
))
}

View File

@@ -27,8 +27,8 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/health/healthmsg"
"tailscale.com/internal/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil"
@@ -1097,12 +1097,6 @@ func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
return
}
func init() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
}
// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//

View File

@@ -60,7 +60,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
💣 go4.org/mem from tailscale.com/client/local+
go4.org/netipx from tailscale.com/net/tsaddr
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/netmon+
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
@@ -70,7 +70,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
tailscale.com from tailscale.com/version
💣 tailscale.com/atomicfile from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale from tailscale.com/client/web+
tailscale.com/client/local from tailscale.com/client/tailscale+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/client/web from tailscale.com/cmd/tailscale/cli
tailscale.com/clientupdate from tailscale.com/client/web+
@@ -85,16 +86,17 @@ 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/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/client/tailscale from tailscale.com/cmd/tailscale/cli
tailscale.com/internal/noiseconn from tailscale.com/cmd/tailscale/cli
tailscale.com/ipn from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn from tailscale.com/client/local+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/kube/kubetypes from tailscale.com/envknob
tailscale.com/licenses from tailscale.com/client/web+
tailscale.com/metrics from tailscale.com/derp+
@@ -109,7 +111,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/netknob from tailscale.com/net/netns
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
@@ -119,12 +121,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/tlsdial/blockblame from tailscale.com/net/tlsdial
tailscale.com/net/tsaddr from tailscale.com/client/web+
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/paths from tailscale.com/client/tailscale+
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/paths from tailscale.com/client/local+
💣 tailscale.com/safesocket from tailscale.com/client/local+
tailscale.com/syncs from tailscale.com/cmd/tailscale/cli+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tailcfg from tailscale.com/client/local+
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
tailscale.com/tka from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/local+
tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tstime from tailscale.com/control/controlhttp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
@@ -133,7 +135,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/dnstype from tailscale.com/tailcfg+
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/ipn+
tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/util/testenv+
tailscale.com/types/logger from tailscale.com/client/web+
tailscale.com/types/netmap from tailscale.com/ipn+
@@ -193,14 +195,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
W golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
golang.org/x/exp/maps from tailscale.com/util/syspolicy/internal/metrics+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
@@ -211,6 +212,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/http2/hpack from net/http+
golang.org/x/net/icmp from tailscale.com/net/ping
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
golang.org/x/net/internal/socks from golang.org/x/net/proxy
@@ -243,7 +245,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/aes from crypto/internal/hpke+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
@@ -252,34 +254,61 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/hpke from crypto/tls
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/subtle from crypto/cipher+
crypto/tls from github.com/miekg/dns+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
DW database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from crypto/internal/nistec+
embed from github.com/peterbourgon/ff/v3+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -304,23 +333,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall
internal/asan from syscall+
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/aes+
internal/byteorder from crypto/cipher+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/aes+
internal/cpu from crypto/internal/fips140deps/cpu+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/aes+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime
internal/goexperiment from runtime+
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall
internal/msan from syscall+
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -329,18 +357,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/mitchellh/go-ps+
@@ -366,7 +397,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os from crypto/internal/sysrand+
os/exec from github.com/coreos/go-iptables/iptables+
os/signal from tailscale.com/cmd/tailscale/cli
os/user from archive/tar+
@@ -377,8 +408,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from tailscale.com+
runtime/internal/math from runtime
runtime/internal/sys from runtime
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+
@@ -395,3 +424,4 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -10,7 +10,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/middleware/private/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
@@ -32,10 +31,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/internal/auth from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
L github.com/aws/aws-sdk-go-v2/internal/auth/smithy from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/context from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/awsrulesfn from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/aws-sdk-go-v2/internal/middleware from github.com/aws/aws-sdk-go-v2/service/sso+
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
@@ -70,15 +71,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/metrics from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/private/requestcompression from github.com/aws/aws-sdk-go-v2/config
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/tracing from github.com/aws/aws-sdk-go-v2/aws/middleware+
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
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -90,6 +92,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 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/gaissmai/bart/internal/bitset from github.com/gaissmai/bart+
github.com/gaissmai/bart/internal/sparse from github.com/gaissmai/bart
github.com/go-json-experiment/json from tailscale.com/types/opt+
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+
@@ -111,7 +115,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
L 💣 github.com/illarion/gonotify/v2 from tailscale.com/net/dns
L 💣 github.com/illarion/gonotify/v3 from tailscale.com/net/dns
L github.com/illarion/gonotify/v3/syscallf from github.com/illarion/gonotify/v3
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/feature/tap
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
@@ -152,9 +157,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
LD github.com/tailscale/golang-x-crypto/internal/poly1305 from github.com/tailscale/golang-x-crypto/ssh
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@@ -184,7 +186,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
💣 go4.org/mem from tailscale.com/client/local+
go4.org/netipx from github.com/tailscale/wf+
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/cmd/tailscaled+
@@ -208,7 +210,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/header/parse+
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/stack+
gvisor.dev/gvisor/pkg/tcpip/internal/tcp from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
gvisor.dev/gvisor/pkg/tcpip/network/internal/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
gvisor.dev/gvisor/pkg/tcpip/network/internal/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -233,7 +235,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/appc from tailscale.com/ipn/ipnlocal
💣 tailscale.com/atomicfile from tailscale.com/ipn+
LD tailscale.com/chirp from tailscale.com/cmd/tailscaled
tailscale.com/client/tailscale from tailscale.com/client/web+
tailscale.com/client/local from tailscale.com/client/tailscale+
tailscale.com/client/tailscale from tailscale.com/derp
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
tailscale.com/clientupdate from tailscale.com/client/web+
@@ -251,12 +254,12 @@ 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 from tailscale.com/client/local+
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/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web+
tailscale.com/feature from tailscale.com/feature/wakeonlan+
tailscale.com/feature/capture from tailscale.com/feature/condregister
@@ -267,12 +270,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/tailscale+
tailscale.com/ipn from tailscale.com/client/local+
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/ipnlocal from tailscale.com/cmd/tailscaled+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
tailscale.com/ipn/ipnstate from tailscale.com/client/local+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
@@ -310,7 +314,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
💣 tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/netutil from tailscale.com/client/local+
tailscale.com/net/packet from tailscale.com/net/connstats+
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
tailscale.com/net/ping from tailscale.com/net/netcheck+
@@ -328,21 +332,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/omit from tailscale.com/ipn/conffile
tailscale.com/paths from tailscale.com/client/tailscale+
tailscale.com/paths from tailscale.com/client/local+
💣 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/safesocket from tailscale.com/client/local+
LD tailscale.com/sessionrecording from tailscale.com/ssh/tailssh
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/tailcfg from tailscale.com/client/local+
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
tailscale.com/tempfork/httprec from tailscale.com/control/controlclient
tailscale.com/tka from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/local+
tailscale.com/tsconst from tailscale.com/net/netmon+
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
tailscale.com/tstime from tailscale.com/control/controlclient+
@@ -354,7 +358,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/empty from tailscale.com/ipn+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
tailscale.com/types/key from tailscale.com/client/tailscale+
tailscale.com/types/key from tailscale.com/client/local+
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
tailscale.com/types/logger from tailscale.com/appc+
tailscale.com/types/logid from tailscale.com/cmd/tailscaled+
@@ -439,20 +443,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf+
LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/curve25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from tailscale.com/control/controlbase
golang.org/x/crypto/internal/alias from golang.org/x/crypto/chacha20+
golang.org/x/crypto/internal/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/sha3 from crypto/internal/mlkem768+
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
LD golang.org/x/crypto/ssh/internal/bcrypt_pbkdf from golang.org/x/crypto/ssh
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
@@ -466,6 +469,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
golang.org/x/net/icmp from tailscale.com/net/ping+
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/internal/httpcommon from golang.org/x/net/http2
golang.org/x/net/internal/iana from golang.org/x/net/icmp+
golang.org/x/net/internal/socket from golang.org/x/net/icmp+
golang.org/x/net/internal/socks from golang.org/x/net/proxy
@@ -499,7 +503,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/aes from crypto/internal/hpke+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509+
@@ -508,34 +512,61 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
crypto/internal/alias from crypto/aes+
crypto/internal/bigmod from crypto/ecdsa+
crypto/internal/boring from crypto/aes+
crypto/internal/boring/bbig from crypto/ecdsa+
crypto/internal/boring/sig from crypto/internal/boring
crypto/internal/edwards25519 from crypto/ed25519
crypto/internal/edwards25519/field from crypto/ecdh+
crypto/internal/entropy from crypto/internal/fips140/drbg
crypto/internal/fips140 from crypto/internal/fips140/aes+
crypto/internal/fips140/aes from crypto/aes+
crypto/internal/fips140/aes/gcm from crypto/cipher+
crypto/internal/fips140/alias from crypto/cipher+
crypto/internal/fips140/bigmod from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/check from crypto/internal/fips140/aes+
crypto/internal/fips140/drbg from crypto/internal/fips140/aes/gcm+
crypto/internal/fips140/ecdh from crypto/ecdh
crypto/internal/fips140/ecdsa from crypto/ecdsa
crypto/internal/fips140/ed25519 from crypto/ed25519
crypto/internal/fips140/edwards25519 from crypto/internal/fips140/ed25519
crypto/internal/fips140/edwards25519/field from crypto/ecdh+
crypto/internal/fips140/hkdf from crypto/internal/fips140/tls13+
crypto/internal/fips140/hmac from crypto/hmac+
crypto/internal/fips140/mlkem from crypto/tls
crypto/internal/fips140/nistec from crypto/elliptic+
crypto/internal/fips140/nistec/fiat from crypto/internal/fips140/nistec
crypto/internal/fips140/rsa from crypto/rsa
crypto/internal/fips140/sha256 from crypto/internal/fips140/check+
crypto/internal/fips140/sha3 from crypto/internal/fips140/hmac+
crypto/internal/fips140/sha512 from crypto/internal/fips140/ecdsa+
crypto/internal/fips140/subtle from crypto/internal/fips140/aes+
crypto/internal/fips140/tls12 from crypto/tls
crypto/internal/fips140/tls13 from crypto/tls
crypto/internal/fips140deps/byteorder from crypto/internal/fips140/aes+
crypto/internal/fips140deps/cpu from crypto/internal/fips140/aes+
crypto/internal/fips140deps/godebug from crypto/internal/fips140+
crypto/internal/fips140hash from crypto/ecdsa+
crypto/internal/fips140only from crypto/cipher+
crypto/internal/hpke from crypto/tls
crypto/internal/mlkem768 from crypto/tls
crypto/internal/nistec from crypto/ecdh+
crypto/internal/nistec/fiat from crypto/internal/nistec
crypto/internal/impl from crypto/internal/fips140/aes+
crypto/internal/randutil from crypto/dsa+
crypto/internal/sysrand from crypto/internal/entropy+
crypto/md5 from crypto/tls+
crypto/rand from crypto/ed25519+
crypto/rc4 from crypto/tls+
crypto/rsa from crypto/tls+
crypto/sha1 from crypto/tls+
crypto/sha256 from crypto/tls+
crypto/sha3 from crypto/internal/fips140hash
crypto/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/subtle from crypto/cipher+
crypto/tls from github.com/aws/aws-sdk-go-v2/aws/transport/http+
crypto/tls/internal/fips140tls from crypto/tls
crypto/x509 from crypto/tls+
D crypto/x509/internal/macos from crypto/x509
crypto/x509/pkix from crypto/x509+
DW database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from crypto/internal/nistec+
embed from github.com/tailscale/web-client-prebuilt+
encoding from encoding/gob+
encoding/asn1 from crypto/x509+
encoding/base32 from github.com/fxamacker/cbor/v2+
@@ -557,23 +588,22 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
html from html/template+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from syscall
internal/asan from syscall+
internal/bisect from internal/godebug
internal/bytealg from bytes+
internal/byteorder from crypto/aes+
internal/byteorder from crypto/cipher+
internal/chacha8rand from math/rand/v2+
internal/concurrent from unique
internal/coverage/rtcov from runtime
internal/cpu from crypto/aes+
internal/cpu from crypto/internal/fips140deps/cpu+
internal/filepathlite from os+
internal/fmtsort from fmt+
internal/goarch from crypto/aes+
internal/goarch from crypto/internal/fips140deps/cpu+
internal/godebug from archive/tar+
internal/godebugs from internal/godebug+
internal/goexperiment from runtime
internal/goexperiment from runtime+
internal/goos from crypto/x509+
internal/itoa from internal/poll+
internal/msan from syscall
internal/msan from syscall+
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
@@ -583,18 +613,21 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
internal/runtime/exithook from runtime
internal/runtime/maps from reflect+
internal/runtime/math from internal/runtime/maps+
internal/runtime/sys from crypto/subtle+
L internal/runtime/syscall from runtime+
internal/saferio from debug/pe+
internal/singleflight from net
internal/stringslite from embed+
internal/sync from sync+
internal/syscall/execenv from os+
LD internal/syscall/unix from crypto/rand+
W internal/syscall/windows from crypto/rand+
LD internal/syscall/unix from crypto/internal/sysrand+
W internal/syscall/windows from crypto/internal/sysrand+
W internal/syscall/windows/registry from mime+
W internal/syscall/windows/sysdll from internal/syscall/windows+
internal/testlog from os
internal/unsafeheader from internal/reflectlite+
internal/weak from unique
io from archive/tar+
io/fs from archive/tar+
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
@@ -621,7 +654,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/netip from github.com/tailscale/wireguard-go/conn+
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
net/url from crypto/x509+
os from crypto/rand+
os from crypto/internal/sysrand+
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
os/signal from tailscale.com/cmd/tailscaled
os/user from archive/tar+
@@ -632,8 +665,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
runtime/internal/math from runtime
runtime/internal/sys from runtime
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
slices from tailscale.com/appc+
@@ -652,3 +683,4 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
unicode/utf8 from bufio+
unique from net/netip
unsafe from bytes+
weak from unique

View File

@@ -17,7 +17,6 @@ func TestOmitSSH(t *testing.T) {
Tags: "ts_omit_ssh",
BadDeps: map[string]string{
"tailscale.com/ssh/tailssh": msg,
"golang.org/x/crypto/ssh": msg,
"tailscale.com/sessionrecording": msg,
"github.com/anmitsu/go-shlex": msg,
"github.com/creack/pty": msg,

View File

@@ -30,7 +30,7 @@ import (
"syscall"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/control/controlclient"
"tailscale.com/drive/driveimpl"
@@ -621,7 +621,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
}
lb.ConfigureWebClient(&tailscale.LocalClient{
lb.ConfigureWebClient(&local.Client{
Socket: args.socketpath,
UseSocketOnly: args.socketpath != paths.DefaultTailscaledSocket(),
})

View File

@@ -44,6 +44,7 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/drive/driveimpl"
"tailscale.com/envknob"
"tailscale.com/ipn/desktop"
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
@@ -335,6 +336,13 @@ func beWindowsSubprocess() bool {
sys.Set(driveimpl.NewFileSystemForRemote(log.Printf))
if sessionManager, err := desktop.NewSessionManager(log.Printf); err == nil {
sys.Set(sessionManager)
} else {
// Errors creating the session manager are unexpected, but not fatal.
log.Printf("[unexpected]: error creating a desktop session manager: %v", err)
}
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
if err != nil {

View File

@@ -10,6 +10,7 @@ package main
import (
"bufio"
"bytes"
"cmp"
"context"
"encoding/json"
"errors"
@@ -22,13 +23,7 @@ import (
"sort"
"strings"
"time"
"unicode"
"github.com/dave/courtney/scanner"
"github.com/dave/courtney/shared"
"github.com/dave/courtney/tester"
"github.com/dave/patsy"
"github.com/dave/patsy/vos"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/util/slicesx"
)
@@ -65,11 +60,12 @@ type packageTests struct {
}
type goTestOutput struct {
Time time.Time
Action string
Package string
Test string
Output string
Time time.Time
Action string
ImportPath string
Package string
Test string
Output string
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
@@ -117,42 +113,43 @@ func runTests(ctx context.Context, attempt int, pt *packageTests, goTestArgs, te
for s.Scan() {
var goOutput goTestOutput
if err := json.Unmarshal(s.Bytes(), &goOutput); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
// `go test -json` outputs invalid JSON when a build fails.
// In that case, discard the the output and start reading again.
// The build error will be printed to stderr.
// See: https://github.com/golang/go/issues/35169
if _, ok := err.(*json.SyntaxError); ok {
fmt.Println(s.Text())
continue
}
panic(err)
return fmt.Errorf("failed to parse go test output %q: %w", s.Bytes(), err)
}
pkg := goOutput.Package
pkg := cmp.Or(
goOutput.Package,
"build:"+goOutput.ImportPath, // can be "./cmd" while Package is "tailscale.com/cmd" so use separate namespace
)
pkgTests := resultMap[pkg]
if pkgTests == nil {
pkgTests = make(map[string]*testAttempt)
pkgTests = map[string]*testAttempt{
"": {}, // Used for start time and build logs.
}
resultMap[pkg] = pkgTests
}
if goOutput.Test == "" {
switch goOutput.Action {
case "start":
pkgTests[""] = &testAttempt{start: goOutput.Time}
case "fail", "pass", "skip":
pkgTests[""].start = goOutput.Time
case "build-output":
pkgTests[""].logs.WriteString(goOutput.Output)
case "build-fail", "fail", "pass", "skip":
for _, test := range pkgTests {
if test.testName != "" && test.outcome == "" {
test.outcome = "fail"
ch <- test
}
}
outcome := goOutput.Action
if outcome == "build-fail" {
outcome = "FAIL"
}
pkgTests[""].logs.WriteString(goOutput.Output)
ch <- &testAttempt{
pkg: goOutput.Package,
outcome: goOutput.Action,
outcome: outcome,
start: pkgTests[""].start,
end: goOutput.Time,
logs: pkgTests[""].logs,
pkgFinished: true,
}
}
@@ -221,6 +218,9 @@ func main() {
}
toRun := []*nextRun{firstRun}
printPkgOutcome := func(pkg, outcome string, attempt int, runtime time.Duration) {
if pkg == "" {
return // We reach this path on a build error.
}
if outcome == "skip" {
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
return
@@ -238,30 +238,6 @@ func main() {
fmt.Printf("%s\t%s\t%.3fs\n", outcome, pkg, runtime.Seconds())
}
// Check for -coverprofile argument and filter it out
combinedCoverageFilename := ""
filteredGoTestArgs := make([]string, 0, len(goTestArgs))
preceededByCoverProfile := false
for _, arg := range goTestArgs {
if arg == "-coverprofile" {
preceededByCoverProfile = true
} else if preceededByCoverProfile {
combinedCoverageFilename = strings.TrimSpace(arg)
preceededByCoverProfile = false
} else {
filteredGoTestArgs = append(filteredGoTestArgs, arg)
}
}
goTestArgs = filteredGoTestArgs
runningWithCoverage := combinedCoverageFilename != ""
if runningWithCoverage {
fmt.Printf("Will log coverage to %v\n", combinedCoverageFilename)
}
// Keep track of all test coverage files. With each retry, we'll end up
// with additional coverage files that will be combined when we finish.
coverageFiles := make([]string, 0)
for len(toRun) > 0 {
var thisRun *nextRun
thisRun, toRun = toRun[0], toRun[1:]
@@ -275,27 +251,13 @@ func main() {
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\nflakytest failures JSON: %s\n\n", thisRun.attempt, j)
}
goTestArgsWithCoverage := testArgs
if runningWithCoverage {
coverageFile := fmt.Sprintf("/tmp/coverage_%d.out", thisRun.attempt)
coverageFiles = append(coverageFiles, coverageFile)
goTestArgsWithCoverage = make([]string, len(goTestArgs), len(goTestArgs)+2)
copy(goTestArgsWithCoverage, goTestArgs)
goTestArgsWithCoverage = append(
goTestArgsWithCoverage,
fmt.Sprintf("-coverprofile=%v", coverageFile),
"-covermode=set",
"-coverpkg=./...",
)
}
toRetry := make(map[string][]*testAttempt) // pkg -> tests to retry
for _, pt := range thisRun.tests {
ch := make(chan *testAttempt)
runErr := make(chan error, 1)
go func() {
defer close(runErr)
runErr <- runTests(ctx, thisRun.attempt, pt, goTestArgsWithCoverage, testArgs, ch)
runErr <- runTests(ctx, thisRun.attempt, pt, goTestArgs, testArgs, ch)
}()
var failed bool
@@ -314,6 +276,7 @@ func main() {
// when a package times out.
failed = true
}
os.Stdout.ReadFrom(&tr.logs)
printPkgOutcome(tr.pkg, tr.outcome, thisRun.attempt, tr.end.Sub(tr.start))
continue
}
@@ -372,107 +335,4 @@ func main() {
}
toRun = append(toRun, nextRun)
}
if runningWithCoverage {
intermediateCoverageFilename := "/tmp/coverage.out_intermediate"
if err := combineCoverageFiles(intermediateCoverageFilename, coverageFiles); err != nil {
fmt.Printf("error combining coverage files: %v\n", err)
os.Exit(2)
}
if err := processCoverageWithCourtney(intermediateCoverageFilename, combinedCoverageFilename, testArgs); err != nil {
fmt.Printf("error processing coverage with courtney: %v\n", err)
os.Exit(3)
}
fmt.Printf("Wrote combined coverage to %v\n", combinedCoverageFilename)
}
}
func combineCoverageFiles(intermediateCoverageFilename string, coverageFiles []string) error {
combinedCoverageFile, err := os.OpenFile(intermediateCoverageFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("create /tmp/coverage.out: %w", err)
}
defer combinedCoverageFile.Close()
w := bufio.NewWriter(combinedCoverageFile)
defer w.Flush()
for fileNumber, coverageFile := range coverageFiles {
f, err := os.Open(coverageFile)
if err != nil {
return fmt.Errorf("open %v: %w", coverageFile, err)
}
defer f.Close()
in := bufio.NewReader(f)
line := 0
for {
r, _, err := in.ReadRune()
if err != nil {
if err != io.EOF {
return fmt.Errorf("read %v: %w", coverageFile, err)
}
break
}
// On all but the first coverage file, skip the coverage file header
if fileNumber > 0 && line == 0 {
continue
}
if r == '\n' {
line++
}
// filter for only printable characters because coverage file sometimes includes junk on 2nd line
if unicode.IsPrint(r) || r == '\n' {
if _, err := w.WriteRune(r); err != nil {
return fmt.Errorf("write %v: %w", combinedCoverageFile.Name(), err)
}
}
}
}
return nil
}
// processCoverageWithCourtney post-processes code coverage to exclude less
// meaningful sections like 'if err != nil { return err}', as well as
// anything marked with a '// notest' comment.
//
// instead of running the courtney as a separate program, this embeds
// courtney for easier integration.
func processCoverageWithCourtney(intermediateCoverageFilename, combinedCoverageFilename string, testArgs []string) error {
env := vos.Os()
setup := &shared.Setup{
Env: vos.Os(),
Paths: patsy.NewCache(env),
TestArgs: testArgs,
Load: intermediateCoverageFilename,
Output: combinedCoverageFilename,
}
if err := setup.Parse(testArgs); err != nil {
return fmt.Errorf("parse args: %w", err)
}
s := scanner.New(setup)
if err := s.LoadProgram(); err != nil {
return fmt.Errorf("load program: %w", err)
}
if err := s.ScanPackages(); err != nil {
return fmt.Errorf("scan packages: %w", err)
}
t := tester.New(setup)
if err := t.Load(); err != nil {
return fmt.Errorf("load: %w", err)
}
if err := t.ProcessExcludes(s.Excludes); err != nil {
return fmt.Errorf("process excludes: %w", err)
}
if err := t.Save(); err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}

View File

@@ -11,6 +11,7 @@ import (
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
)
@@ -154,24 +155,24 @@ func TestBuildError(t *testing.T) {
t.Fatalf("writing package: %s", err)
}
buildErr := []byte("builderror_test.go:3:1: expected declaration, found derp\nFAIL command-line-arguments [setup failed]")
wantErr := "builderror_test.go:3:1: expected declaration, found derp\nFAIL"
// Confirm `go test` exits with code 1.
goOut, err := exec.Command("go", "test", testfile).CombinedOutput()
if code, ok := errExitCode(err); !ok || code != 1 {
t.Fatalf("go test %s: expected error with exit code 0 but got: %v", testfile, err)
t.Fatalf("go test %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
}
if !bytes.Contains(goOut, buildErr) {
t.Fatalf("go test %s: expected build error containing %q but got:\n%s", testfile, buildErr, goOut)
if !strings.Contains(string(goOut), wantErr) {
t.Fatalf("go test %s: got output %q, want output containing %q", testfile, goOut, wantErr)
}
// Confirm `testwrapper` exits with code 1.
twOut, err := cmdTestwrapper(t, testfile).CombinedOutput()
if code, ok := errExitCode(err); !ok || code != 1 {
t.Fatalf("testwrapper %s: expected error with exit code 0 but got: %v", testfile, err)
t.Fatalf("testwrapper %s: got exit code %d, want 1 (err: %v)", testfile, code, err)
}
if !bytes.Contains(twOut, buildErr) {
t.Fatalf("testwrapper %s: expected build error containing %q but got:\n%s", testfile, buildErr, twOut)
if !strings.Contains(string(twOut), wantErr) {
t.Fatalf("testwrapper %s: got output %q, want output containing %q", testfile, twOut, wantErr)
}
if testing.Verbose() {

View File

@@ -22,7 +22,7 @@ import (
"log"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
@@ -37,7 +37,7 @@ var (
func main() {
flag.Parse()
lc := tailscale.LocalClient{Socket: *flagSocket}
lc := local.Client{Socket: *flagSocket}
if lc.Socket != "" {
lc.UseSocketOnly = true
}

View File

@@ -176,6 +176,10 @@ func runEsbuild(buildOptions esbuild.BuildOptions) esbuild.BuildResult {
// wasm_exec.js runtime helper library from the Go toolchain.
func setupEsbuildWasmExecJS(build esbuild.PluginBuild) {
wasmExecSrcPath := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js")
if _, err := os.Stat(wasmExecSrcPath); os.IsNotExist(err) {
// Go 1.24+ location:
wasmExecSrcPath = filepath.Join(runtime.GOROOT(), "lib", "wasm", "wasm_exec.js")
}
build.OnResolve(esbuild.OnResolveOptions{
Filter: "./wasm_exec$",
}, func(args esbuild.OnResolveArgs) (esbuild.OnResolveResult, error) {

View File

@@ -35,7 +35,7 @@ import (
"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -75,7 +75,7 @@ func main() {
}
var (
lc *tailscale.LocalClient
lc *local.Client
st *ipnstate.Status
err error
watcherChan chan error
@@ -84,7 +84,7 @@ func main() {
lns []net.Listener
)
if *flagUseLocalTailscaled {
lc = &tailscale.LocalClient{}
lc = &local.Client{}
st, err = lc.StatusWithoutPeers(ctx)
if err != nil {
log.Fatalf("getting status: %v", err)
@@ -212,7 +212,7 @@ func main() {
// serveOnLocalTailscaled starts a serve session using an already-running
// tailscaled instead of starting a fresh tsnet server, making something
// listening on clientDNSName:dstPort accessible over serve/funnel.
func serveOnLocalTailscaled(ctx context.Context, lc *tailscale.LocalClient, st *ipnstate.Status, dstPort uint16, shouldFunnel bool) (cleanup func(), watcherChan chan error, err error) {
func serveOnLocalTailscaled(ctx context.Context, lc *local.Client, st *ipnstate.Status, dstPort uint16, shouldFunnel bool) (cleanup func(), watcherChan chan error, err error) {
// In order to support funneling out in local tailscaled mode, we need
// to add a serve config to forward the listeners we bound above and
// allow those forwarders to be funneled out.
@@ -275,7 +275,7 @@ func serveOnLocalTailscaled(ctx context.Context, lc *tailscale.LocalClient, st *
}
type idpServer struct {
lc *tailscale.LocalClient
lc *local.Client
loopbackURL string
serverURL string // "https://foo.bar.ts.net"
funnel bool
@@ -328,7 +328,7 @@ type authRequest struct {
// allowRelyingParty validates that a relying party identified either by a
// known remoteAddr or a valid client ID/secret pair is allowed to proceed
// with the authorization flow associated with this authRequest.
func (ar *authRequest) allowRelyingParty(r *http.Request, lc *tailscale.LocalClient) error {
func (ar *authRequest) allowRelyingParty(r *http.Request, lc *local.Client) error {
if ar.localRP {
ra, err := netip.ParseAddrPort(r.RemoteAddr)
if err != nil {

View File

@@ -30,7 +30,7 @@ import (
"time"
"tailscale.com/atomicfile"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/hostinfo"
"tailscale.com/util/mak"
"tailscale.com/util/must"
@@ -64,7 +64,7 @@ func serveCmd(w http.ResponseWriter, cmd string, args ...string) {
}
type localClientRoundTripper struct {
lc tailscale.LocalClient
lc local.Client
}
func (rt *localClientRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {

View File

@@ -1003,7 +1003,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
if persist == c.persist {
newPersist := persist.AsStruct()
newPersist.NodeID = nm.SelfNode.StableID()
newPersist.UserProfile = nm.UserProfiles[nm.User()]
if up, ok := nm.UserProfiles[nm.User()]; ok {
newPersist.UserProfile = *up.AsStruct()
}
c.persist = newPersist.View()
persist = c.persist

View File

@@ -77,7 +77,7 @@ type mapSession struct {
peers map[tailcfg.NodeID]tailcfg.NodeView
lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView
lastPacketFilterRules views.Slice[tailcfg.FilterRule] // concatenation of all namedPacketFilters
namedPacketFilters map[string]views.Slice[tailcfg.FilterRule]
lastParsedPacketFilter []filter.Match
@@ -89,7 +89,6 @@ type mapSession struct {
lastPopBrowserURL string
lastTKAInfo *tailcfg.TKAInfo
lastNetmapSummary string // from NetworkMap.VeryConcise
lastMaxExpiry time.Duration
}
// newMapSession returns a mostly unconfigured new mapSession.
@@ -104,7 +103,7 @@ func newMapSession(privateNodeKey key.NodePrivate, nu NetmapUpdater, controlKnob
privateNodeKey: privateNodeKey,
publicNodeKey: privateNodeKey.Public(),
lastDNSConfig: new(tailcfg.DNSConfig),
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfileView{},
// Non-nil no-op defaults, to be optionally overridden by the caller.
logf: logger.Discard,
@@ -195,10 +194,6 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
ms.updateStateFromResponse(resp)
// Occasionally clean up old userprofile if it grows too much
// from e.g. ephemeral tagged nodes.
ms.cleanLastUserProfile()
if ms.tryHandleIncrementally(resp) {
ms.occasionallyPrintSummary(ms.lastNetmapSummary)
return nil
@@ -294,8 +289,9 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
}
for _, up := range resp.UserProfiles {
ms.lastUserProfile[up.ID] = up
ms.lastUserProfile[up.ID] = up.View()
}
// TODO(bradfitz): clean up old user profiles? maybe not worth it.
if dm := resp.DERPMap; dm != nil {
ms.vlogf("netmap: new map contains DERP map")
@@ -387,9 +383,6 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo
}
if resp.MaxKeyDuration > 0 {
ms.lastMaxExpiry = resp.MaxKeyDuration
}
}
var (
@@ -544,32 +537,6 @@ func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserI
}
}
// cleanLastUserProfile deletes any entries from lastUserProfile
// that are not referenced by any peer or the self node.
//
// This is expensive enough that we don't do this on every message
// from the server, but only when it's grown enough to matter.
func (ms *mapSession) cleanLastUserProfile() {
if len(ms.lastUserProfile) < len(ms.peers)*2 {
// Hasn't grown enough to be worth cleaning.
return
}
keep := set.Set[tailcfg.UserID]{}
if node := ms.lastNode; node.Valid() {
keep.Add(node.User())
}
for _, n := range ms.peers {
keep.Add(n.User())
keep.Add(n.Sharer())
}
for userID := range ms.lastUserProfile {
if !keep.Contains(userID) {
delete(ms.lastUserProfile, userID)
}
}
}
var debugPatchifyPeer = envknob.RegisterBool("TS_DEBUG_PATCHIFY_PEER")
// patchifyPeersChanged mutates resp to promote PeersChanged entries to PeersChangedPatch
@@ -837,7 +804,7 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: peerViews,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfileView),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
@@ -848,7 +815,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
DERPMap: ms.lastDERPMap,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
MaxKeyDuration: ms.lastMaxExpiry,
}
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {

View File

@@ -36,6 +36,7 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
@@ -1319,7 +1320,7 @@ func (c *sclient) requestMeshUpdate() {
}
}
var localClient tailscale.LocalClient
var localClient local.Client
// isMeshPeer reports whether the client is a trusted mesh peer
// node in the DERP region.
@@ -1827,6 +1828,14 @@ func (c *sclient) setWriteDeadline() {
// of connected peers.
d = privilegedWriteTimeout
}
if d == 0 {
// A zero value should disable the write deadline per
// --tcp-write-timeout docs. The flag should only be applicable for
// non-mesh connections, again per its docs. If mesh happened to use a
// zero value constant above it would be a bug, so we don't bother
// with a condition on c.canMesh.
return
}
// Ignore the error from setting the write deadline. In practice,
// setting the deadline will only fail if the connection is closed
// or closing, so the subsequent Write() will fail anyway.

View File

@@ -98,6 +98,7 @@ func ServeNoContent(w http.ResponseWriter, r *http.Request) {
w.Header().Set(NoContentResponseHeader, "response "+challenge)
}
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
w.WriteHeader(http.StatusNoContent)
}
@@ -105,7 +106,7 @@ func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
c == '.' || c == '-' || c == '_' || c == ':'
}
const (

View File

@@ -16,9 +16,12 @@
<string id="SINCE_V1_62">Tailscale version 1.62.0 and later</string>
<string id="SINCE_V1_74">Tailscale version 1.74.0 and later</string>
<string id="SINCE_V1_78">Tailscale version 1.78.0 and later</string>
<string id="SINCE_V1_82">Tailscale version 1.82.0 and later</string>
<string id="Tailscale_Category">Tailscale</string>
<string id="UI_Category">UI customization</string>
<string id="Settings_Category">Settings</string>
<string id="AllowedWithAudit">Allowed (with audit)</string>
<string id="NotAllowed">Not Allowed</string>
<string id="LoginURL">Require using a specific Tailscale coordination server</string>
<string id="LoginURL_Help"><![CDATA[This policy can be used to require the use of a particular Tailscale coordination server.
@@ -98,6 +101,14 @@ If you disable this policy, then Run Unattended is always disabled and the menu
If you do not configure this policy, then Run Unattended depends on what is selected in the Preferences submenu.
See https://tailscale.com/kb/1315/mdm-keys#set-unattended-mode and https://tailscale.com/kb/1088/run-unattended for more details.]]></string>
<string id="AlwaysOn">Restrict users from disconnecting Tailscale (always-on mode)</string>
<string id="AlwaysOn_Help"><![CDATA[This policy setting controls whether a user can disconnect Tailscale.
If you enable this policy setting, users will not be allowed to disconnect Tailscale, and it will remain in a connected state as long as they are logged in, even if they close or terminate the GUI. Optionally, you can allow users to temporarily disconnect Tailscale by requiring them to provide a reason, which will be logged for auditing purposes.
If necessary, it can be used along with Unattended Mode to keep Tailscale connected regardless of whether a user is logged in. This can be used to facilitate remote access to a device or ensure connectivity to a Domain Controller before a user logs in.
If you disable or don't configure this policy setting, users will be allowed to disconnect Tailscale at their will.]]></string>
<string id="ExitNodeAllowLANAccess">Allow Local Network Access when an Exit Node is in use</string>
<string id="ExitNodeAllowLANAccess_Help"><![CDATA[This policy can be used to require that the Allow Local Network Access setting is configured a certain way.
@@ -265,6 +276,10 @@ See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more d
<label>Auth Key:</label>
</textBox>
</presentation>
<presentation id="AlwaysOn">
<text>The options below allow configuring exceptions where disconnecting Tailscale is permitted.</text>
<dropdownList refId="AlwaysOn_OverrideWithReason" noSort="true" defaultItem="0">Disconnects with reason:</dropdownList>
</presentation>
<presentation id="ExitNodeID">
<textBox refId="ExitNodeIDPrompt">
<label>Exit Node:</label>

View File

@@ -54,6 +54,10 @@
displayName="$(string.SINCE_V1_78)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_82"
displayName="$(string.SINCE_V1_82)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
</definitions>
</supportedOn>
<categories>
@@ -98,7 +102,7 @@
<parentCategory ref="Settings_Category" />
<supportedOn ref="SINCE_V1_56" />
<elements>
<text id="ExitNodeIDPrompt" valueName="ExitNodeID" required="true" />>
<text id="ExitNodeIDPrompt" valueName="ExitNodeID" required="true" />
</elements>
</policy>
<policy name="AllowedSuggestedExitNodes" class="Machine" displayName="$(string.AllowedSuggestedExitNodes)" explainText="$(string.AllowedSuggestedExitNodes_Help)" presentation="$(presentation.AllowedSuggestedExitNodes)" key="Software\Policies\Tailscale\AllowedSuggestedExitNodes">
@@ -128,6 +132,30 @@
<string>never</string>
</disabledValue>
</policy>
<policy name="AlwaysOn" class="Machine" displayName="$(string.AlwaysOn)" explainText="$(string.AlwaysOn_Help)" presentation="$(presentation.AlwaysOn)" key="Software\Policies\Tailscale" valueName="AlwaysOn.Enabled">
<parentCategory ref="Settings_Category" />
<supportedOn ref="SINCE_V1_82" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
<elements>
<enum id="AlwaysOn_OverrideWithReason" valueName="AlwaysOn.OverrideWithReason">
<item displayName="$(string.NotAllowed)">
<value>
<decimal value="0" />
</value>
</item>
<item displayName="$(string.AllowedWithAudit)">
<value>
<decimal value="1" />
</value>
</item>
</enum>
</elements>
</policy>
<policy name="ExitNodeAllowLANAccess" class="Machine" displayName="$(string.ExitNodeAllowLANAccess)" explainText="$(string.ExitNodeAllowLANAccess_Help)" key="Software\Policies\Tailscale" valueName="ExitNodeAllowLANAccess">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />

87
go.mod
View File

@@ -1,6 +1,6 @@
module tailscale.com
go 1.23.1
go 1.24.0
require (
filippo.io/mkcert v1.4.4
@@ -10,10 +10,10 @@ require (
github.com/andybalholm/brotli v1.1.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/atotto/clipboard v0.1.4
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.5
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0
github.com/aws/aws-sdk-go-v2 v1.36.0
github.com/aws/aws-sdk-go-v2/config v1.29.5
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7
github.com/bramvdbogaerde/go-scp v1.4.0
github.com/cilium/ebpf v0.15.0
@@ -21,8 +21,6 @@ require (
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/creack/pty v1.1.23
github.com/dave/courtney v0.4.0
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/distribution/reference v0.6.0
@@ -33,7 +31,7 @@ require (
github.com/fogleman/gg v1.3.0
github.com/frankban/quicktest v1.14.6
github.com/fxamacker/cbor/v2 v2.7.0
github.com/gaissmai/bart v0.11.1
github.com/gaissmai/bart v0.18.0
github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288
github.com/go-logr/zapr v1.3.0
github.com/go-ole/go-ole v1.3.0
@@ -47,9 +45,10 @@ require (
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806
github.com/google/uuid v1.6.0
github.com/goreleaser/nfpm/v2 v2.33.1
github.com/hashicorp/raft v1.7.2
github.com/hdevalence/ed25519consensus v0.2.0
github.com/illarion/gonotify/v2 v2.0.3
github.com/inetaf/tcpproxy v0.0.0-20250121183218-48c7e53d7ac4
github.com/illarion/gonotify/v3 v3.0.2
github.com/inetaf/tcpproxy v0.0.0-20250203165043-ded522cbd03f
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
github.com/jellydator/ttlcache/v3 v3.1.0
github.com/jsimonetti/rtnetlink v1.4.0
@@ -76,12 +75,13 @@ require (
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
github.com/tailscale/depaware v0.0.0-20250112153213-b748de04d81b
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20250110151924-54977352e4a6
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19
@@ -94,20 +94,20 @@ require (
go.uber.org/zap v1.27.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.32.0
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8
golang.org/x/mod v0.22.0
golang.org/x/net v0.34.0
golang.org/x/oauth2 v0.25.0
golang.org/x/sync v0.10.0
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab
golang.org/x/term v0.28.0
golang.org/x/time v0.9.0
golang.org/x/tools v0.29.0
golang.org/x/crypto v0.33.0
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
golang.org/x/mod v0.23.0
golang.org/x/net v0.35.0
golang.org/x/oauth2 v0.26.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0
golang.org/x/term v0.29.0
golang.org/x/time v0.10.0
golang.org/x/tools v0.30.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
gopkg.in/square/go-jose.v2 v2.6.0
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633
honnef.co/go/tools v0.5.1
k8s.io/api v0.32.0
k8s.io/apimachinery v0.32.0
@@ -128,15 +128,13 @@ require (
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
github.com/bombsimon/wsl/v4 v4.2.1 // indirect
github.com/butuzov/mirror v1.1.0 // indirect
github.com/catenacyber/perfsprint v0.7.1 // indirect
github.com/ccojocar/zxcvbn-go v1.0.2 // indirect
github.com/ckaznocha/intrange v0.1.0 // indirect
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
github.com/dave/brenda v1.1.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -149,6 +147,11 @@ require (
github.com/golangci/plugin-module-register v0.1.1 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-metrics v0.5.4 // indirect
github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect
github.com/hashicorp/golang-lru v0.6.0 // indirect
github.com/jjti/go-spancheck v0.5.3 // indirect
github.com/karamaru-alpha/copyloopvar v1.0.8 // indirect
github.com/macabu/inamedparam v0.1.3 // indirect
@@ -188,21 +191,21 @@ require (
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.6.0 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 // indirect
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.1 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
@@ -384,8 +387,8 @@ require (
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/image v0.23.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/text v0.22.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect

217
go.sum
View File

@@ -61,8 +61,9 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs=
github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.2 h1:vUG4lAyuPCXO0TLbXvPv7EB7cNK1QV/luu55UHLrrn8=
github.com/DataDog/zstd v1.5.2/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
github.com/Djarvur/go-err113 v0.1.0 h1:uCRZZOdMQ0TZPHYTdYpoC0bLYJKPEHPUJ8MeAa51lNU=
github.com/Djarvur/go-err113 v0.1.0/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/GaijinEntertainment/go-exhaustruct/v3 v3.2.0 h1:sATXp1x6/axKxz2Gjxv8MALP0bXaNRfQinEwyfMcx8c=
@@ -114,6 +115,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -123,65 +126,50 @@ github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5Fc
github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.22/go.mod h1:mN7Li1wxaPxSSy4Xkr6stFuinJGf3VZW3ZSNvO0q6sI=
github.com/aws/aws-sdk-go-v2/config v1.26.5 h1:lodGSevz7d+kkFJodfauThRxK9mdJbyutUxGq1NNhvw=
github.com/aws/aws-sdk-go-v2/config v1.26.5/go.mod h1:DxHrz6diQJOc9EwDslVRh84VjjrE17g+pVZXUeSxaDU=
github.com/aws/aws-sdk-go-v2/credentials v1.13.21/go.mod h1:90Dk1lJoMyspa/EDUrldTxsPns0wn6+KpRKpdAWc0uA=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64 h1:9QJQs36z61YB8nxGwRDfWXEDYbU6H7jdI6zFiAX1vag=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64/go.mod h1:4Q7R9MFpXRdjO3YnAfUTdnuENs32WzBkASt6VxSYDYQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0 h1:L5h2fymEdVJYvn6hYO8Jx48YmC6xVmjmgHJV3oGKgmc=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk=
github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg=
github.com/aws/aws-sdk-go-v2/config v1.29.5 h1:4lS2IB+wwkj5J43Tq/AwvnscBerBJtQQ6YS7puzCI1k=
github.com/aws/aws-sdk-go-v2/config v1.29.5/go.mod h1:SNzldMlDVbN6nWxM7XsUiNXPSa1LWlqiXtvh/1PrJGg=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58 h1:/d7FUpAPU8Lf2KUdjniQvfNdlMID0Sd9pS23FJ3SS9Y=
github.com/aws/aws-sdk-go-v2/credentials v1.17.58/go.mod h1:aVYW33Ow10CyMQGFgC0ptMRIqJWvJ4nxZb0sUiuQT/A=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58 h1:/BsEGAyMai+KdXS+CMHlLhB5miAO19wOqE6tj8azWPM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.58/go.mod h1:KHM3lfl/sAJBCoLI1Lsg5w4SD2VDYWwQi7vxbKhw7TI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw=
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3 h1:JBod0SnNqcWQ0+uAyzeRFG1zCHotW8DukumYYyNy0zo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.75.3/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7 h1:a8HvP/+ew3tKwSXqL3BCSjiuicr+XTU2eFYeogV9GJE=
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7/go.mod h1:Q7XIWsMo0JcMpI/6TGD6XXcXcV1DbTj6e9BKNntIMIM=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.9/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.9/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
github.com/aws/aws-sdk-go-v2/service/sts v1.18.10/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13 h1:3LXNnmtH3TURctC23hnC0p/39Q5gre3FI7BNOiDcVWc=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.13/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w=
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bkielbasa/cyclop v1.2.1 h1:AeF71HZDob1P2/pRm1so9cd1alZnrpyc4q2uP2l0gJY=
github.com/bkielbasa/cyclop v1.2.1/go.mod h1:K/dT/M0FPAiYjBgQGau7tz+3TMh4FWAEqlMhzFWCrgM=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
@@ -227,6 +215,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/ckaznocha/intrange v0.1.0 h1:ZiGBhvrdsKpoEfzh9CjBfDSZof6QB0ORY5tXasUtiew=
github.com/ckaznocha/intrange v0.1.0/go.mod h1:Vwa9Ekex2BrEQMg6zlrWwbs/FtYw7eS5838Q7UjK7TQ=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -244,6 +234,8 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creachadair/mds v0.17.1 h1:lXQbTGKmb3nE3aK6OEp29L1gCx6B5ynzlQ6c1KOBurc=
github.com/creachadair/mds v0.17.1/go.mod h1:4b//mUiL8YldH6TImXjmW45myzTLNS1LLjOmrk888eg=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -253,14 +245,6 @@ github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18C
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/daixiang0/gci v0.12.3 h1:yOZI7VAxAGPQmkb1eqt5g/11SUlwoat1fSblGLmdiQc=
github.com/daixiang0/gci v0.12.3/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI=
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4=
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms=
github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw=
github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM=
github.com/dave/courtney v0.4.0 h1:Vb8hi+k3O0h5++BR96FIcX0x3NovRbnhGd/dRr8inBk=
github.com/dave/courtney v0.4.0/go.mod h1:3WSU3yaloZXYAxRuWt8oRyVb9SaRiMBt5Kz/2J227tM=
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs=
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -309,6 +293,7 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/evanw/esbuild v0.19.11 h1:mbPO1VJ/df//jjUd+p/nRLYCpizXxXb2w/zZMShxa2k=
github.com/evanw/esbuild v0.19.11/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
@@ -327,8 +312,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
github.com/ghostiam/protogetter v0.3.5 h1:+f7UiF8XNd4w3a//4DnusQ2SZjPkUjxkMEfjbxOK4Ug=
github.com/ghostiam/protogetter v0.3.5/go.mod h1:7lpeDnEJ1ZjL/YtyoN99ljO4z0pd3H0d18/t2dPBxHw=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
@@ -548,13 +533,30 @@ github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Rep
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I=
github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY=
github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI=
github.com/hashicorp/go-msgpack/v2 v2.1.2 h1:4Ee8FTp834e+ewB71RDrQ0VKpyFdrKOjvYtnQ/ltVj0=
github.com/hashicorp/go-msgpack/v2 v2.1.2/go.mod h1:upybraOAblm4S7rx0+jeNy+CWWhzywQsSRV5033mMu4=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o8jga4=
github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/raft v1.7.2 h1:pyvxhfJ4R8VIAlHKvLoKQWElZspsCVT6YWuxVxsPAgc=
github.com/hashicorp/raft v1.7.2/go.mod h1:DfvCGFxpAUPE0L4Uc8JLlTPtc3GzSbdH0MTJCLgnmJQ=
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -565,15 +567,15 @@ github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI=
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/illarion/gonotify/v2 v2.0.3 h1:B6+SKPo/0Sw8cRJh1aLzNEeNVFfzE3c6N+o+vyxM+9A=
github.com/illarion/gonotify/v2 v2.0.3/go.mod h1:38oIJTgFqupkEydkkClkbL6i5lXV/bxdH9do5TALPEE=
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inetaf/tcpproxy v0.0.0-20250121183218-48c7e53d7ac4 h1:5u/LhBmv8Y+BhTTADTuh8ma0DcZ3zzx+GINbMeMG9nM=
github.com/inetaf/tcpproxy v0.0.0-20250121183218-48c7e53d7ac4/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI=
github.com/inetaf/tcpproxy v0.0.0-20250203165043-ded522cbd03f h1:hPcDyz0u+Zo14n0fpJggxL9JMAmZIK97TVLcLJLPMDI=
github.com/inetaf/tcpproxy v0.0.0-20250203165043-ded522cbd03f/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
@@ -599,6 +601,7 @@ github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -671,8 +674,12 @@ github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -754,6 +761,8 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
@@ -782,8 +791,10 @@ github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyf
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
@@ -794,6 +805,7 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
@@ -801,6 +813,7 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
@@ -901,6 +914,7 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
@@ -921,8 +935,8 @@ github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca h1:ecjHwH73Yvqf/oIdQ2vxAX+zc6caQsYdPzsxNW1J3G8=
github.com/tailscale/golang-x-crypto v0.0.0-20250218230618-9a281fd8faca/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
@@ -933,6 +947,8 @@ github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb h1:Rtklwm6HUlCtf/MR2MB9iY4FoA16acWWlC5pLrTVa90=
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb/go.mod h1:R8iCVJnbOB05pGexHK/bKHneIRHpZ3jLl7wMQ0OM/jw=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
@@ -957,12 +973,15 @@ github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966 h1:quvGphlmUVU+n
github.com/timakin/bodyclose v0.0.0-20230421092635-574207250966/go.mod h1:27bSVNWSBOHm+qRp1T9qzaIpsWEP6TbUnei/43HK+PQ=
github.com/timonwong/loggercheck v0.9.4 h1:HKKhqrjcVj8sxL7K77beXh0adEm6DLjV/QOGeMXEVi4=
github.com/timonwong/loggercheck v0.9.4/go.mod h1:caz4zlPcgvpEkXgVnAJGowHAMW2NwHaNlpS8xDbVhTg=
github.com/tink-crypto/tink-go/v2 v2.1.0 h1:QXFBguwMwTIaU17EgZpEJWsUSc60b1BAGTzBIoMdmok=
github.com/tink-crypto/tink-go/v2 v2.1.0/go.mod h1:y1TnYFt1i2eZVfx4OGc+C+EMp4CoKWAw2VSEuoicHHI=
github.com/tomarrell/wrapcheck/v2 v2.8.3 h1:5ov+Cbhlgi7s/a42BprYoxsr73CbdMUTzE3bRDFASUs=
github.com/tomarrell/wrapcheck/v2 v2.8.3/go.mod h1:g9vNIyhb5/9TQgumxQyOEqDHsmGYcGsVMOx/xGkqdMo=
github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw=
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa h1:unMPGGK/CRzfg923allsikmvk2l7beBeFPUNC4RVX/8=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa/go.mod h1:Zj4Tt22fJVn/nz/y6Ergm1SahR9dio1Zm/D2/S0TmXM=
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
@@ -1058,8 +1077,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1070,16 +1089,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f h1:phY1HzDcf18Aq9A8KkmRtY9WvOFIxN8wgfvy6Zm1DV8=
golang.org/x/exp/typeparams v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -1107,8 +1126,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -1148,16 +1167,16 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1171,8 +1190,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1191,6 +1210,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1216,9 +1236,12 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1231,16 +1254,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab h1:BMkEEWYOjkvOX7+YKOGbp6jCyQ5pR2j0Ah47p1Vdsx4=
golang.org/x/sys v0.29.1-0.20250107080300-1c14dcadc3ab/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1251,13 +1274,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1322,8 +1345,8 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1453,8 +1476,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633 h1:2gap+Kh/3F47cO6hAu3idFvsJ0ue6TRcEi2IUkv/F8k=
gvisor.dev/gvisor v0.0.0-20250205023644-9414b50a5633/go.mod h1:5DMfjtclAbTIjbXqO1qCe2K5GKKxWz2JHvCChuTcJEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -1 +1 @@
tailscale.go1.23
tailscale.go1.24

View File

@@ -1 +1 @@
64f7854906c3121fe3ada3d05f1936d3420d6ffa
a529f1c329a97596448310cd52ab64047294b9d5

View File

@@ -0,0 +1,83 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tailscale provides a minimal control plane API client for internal
// use. A full client for 3rd party use is available at
// tailscale.com/client/tailscale/v2. The internal client is provided to avoid
// having to import that whole package.
package tailscale
import (
"errors"
"io"
"net/http"
tsclient "tailscale.com/client/tailscale"
)
// maxSize is the maximum read size (10MB) of responses from the server.
const maxReadSize = 10 << 20
func init() {
tsclient.I_Acknowledge_This_API_Is_Unstable = true
}
// AuthMethod is an alias to tailscale.com/client/tailscale.
type AuthMethod = tsclient.AuthMethod
// Device is an alias to tailscale.com/client/tailscale.
type Device = tsclient.Device
// DeviceFieldsOpts is an alias to tailscale.com/client/tailscale.
type DeviceFieldsOpts = tsclient.DeviceFieldsOpts
// Key is an alias to tailscale.com/client/tailscale.
type Key = tsclient.Key
// KeyCapabilities is an alias to tailscale.com/client/tailscale.
type KeyCapabilities = tsclient.KeyCapabilities
// KeyDeviceCapabilities is an alias to tailscale.com/client/tailscale.
type KeyDeviceCapabilities = tsclient.KeyDeviceCapabilities
// KeyDeviceCreateCapabilities is an alias to tailscale.com/client/tailscale.
type KeyDeviceCreateCapabilities = tsclient.KeyDeviceCreateCapabilities
// ErrResponse is an alias to tailscale.com/client/tailscale.
type ErrResponse = tsclient.ErrResponse
// NewClient is an alias to tailscale.com/client/tailscale.
func NewClient(tailnet string, auth AuthMethod) *Client {
return &Client{
Client: tsclient.NewClient(tailnet, auth),
}
}
// Client is a wrapper of tailscale.com/client/tailscale.
type Client struct {
*tsclient.Client
}
// HandleErrorResponse is an alias to tailscale.com/client/tailscale.
func HandleErrorResponse(b []byte, resp *http.Response) error {
return tsclient.HandleErrorResponse(b, resp)
}
// SendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func SendRequest(c *Client, req *http.Request) ([]byte, *http.Response, error) {
resp, err := c.Do(req)
if err != nil {
return nil, resp, err
}
defer resp.Body.Close()
// Read response. Limit the response to 10MB.
// This limit is carried over from client/tailscale/tailscale.go.
body := io.LimitReader(resp.Body, maxReadSize+1)
b, err := io.ReadAll(body)
if len(b) > maxReadSize {
err = errors.New("API response too large")
}
return b, resp, err
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"tailscale.com/tailcfg"
"tailscale.com/util/httpm"
)
// VIPService is a Tailscale VIPService with Tailscale API JSON representation.
type VIPService struct {
// Name is a VIPService name in form svc:<leftmost-label-of-service-DNS-name>.
Name tailcfg.ServiceName `json:"name,omitempty"`
// Addrs are the IP addresses of the VIP Service. There are two addresses:
// the first is IPv4 and the second is IPv6.
// When creating a new VIP Service, the IP addresses are optional: if no
// addresses are specified then they will be selected. If an IPv4 address is
// specified at index 0, then that address will attempt to be used. An IPv6
// address can not be specified upon creation.
Addrs []string `json:"addrs,omitempty"`
// Comment is an optional text string for display in the admin panel.
Comment string `json:"comment,omitempty"`
// Ports are the ports of a VIPService that will be configured via Tailscale serve config.
// If set, any node wishing to advertise this VIPService must have this port configured via Tailscale serve.
Ports []string `json:"ports,omitempty"`
// Tags are optional ACL tags that will be applied to the VIPService.
Tags []string `json:"tags,omitempty"`
}
// GetVIPService retrieves a VIPService by its name. It returns 404 if the VIPService is not found.
func (client *Client) GetVIPService(ctx context.Context, name tailcfg.ServiceName) (*VIPService, error) {
path := client.BuildTailnetURL("vip-services", name.String())
req, err := http.NewRequestWithContext(ctx, httpm.GET, path, nil)
if err != nil {
return nil, fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return nil, fmt.Errorf("error making Tailsale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, HandleErrorResponse(b, resp)
}
svc := &VIPService{}
if err := json.Unmarshal(b, svc); err != nil {
return nil, err
}
return svc, nil
}
// CreateOrUpdateVIPService creates or updates a VIPService by its name. Caller must ensure that, if the
// VIPService already exists, the VIPService is fetched first to ensure that any auto-allocated IP addresses are not
// lost during the update. If the VIPService was created without any IP addresses explicitly set (so that they were
// auto-allocated by Tailscale) any subsequent request to this function that does not set any IP addresses will error.
func (client *Client) CreateOrUpdateVIPService(ctx context.Context, svc *VIPService) error {
data, err := json.Marshal(svc)
if err != nil {
return err
}
path := client.BuildTailnetURL("vip-services", svc.Name.String())
req, err := http.NewRequestWithContext(ctx, httpm.PUT, path, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
}
return nil
}
// DeleteVIPService deletes a VIPService by its name. It returns an error if the VIPService
// does not exist or if the deletion fails.
func (client *Client) DeleteVIPService(ctx context.Context, name tailcfg.ServiceName) error {
path := client.BuildTailnetURL("vip-services", name.String())
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return fmt.Errorf("error creating new HTTP request: %w", err)
}
b, resp, err := SendRequest(client, req)
if err != nil {
return fmt.Errorf("error making Tailscale API request: %w", err)
}
// If status code was not successful, return the error.
if resp.StatusCode != http.StatusOK {
return HandleErrorResponse(b, resp)
}
return nil
}

6
ipn/desktop/doc.go Normal file
View File

@@ -0,0 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package desktop facilitates interaction with the desktop environment
// and user sessions. As of 2025-02-06, it is only implemented for Windows.
package desktop

24
ipn/desktop/mksyscall.go Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
//sys setLastError(dwErrorCode uint32) = kernel32.SetLastError
//sys registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) [atom==0] = user32.RegisterClassExW
//sys createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) [hWnd==0] = user32.CreateWindowExW
//sys defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.DefWindowProcW
//sys setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) [res==0 && e1!=0] = user32.SetWindowLongPtrW
//sys getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) [res==0 && e1!=0] = user32.GetWindowLongPtrW
//sys sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.SendMessageW
//sys getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) = user32.GetMessageW
//sys translateMessage(lpMsg *_MSG) (res bool) = user32.TranslateMessage
//sys dispatchMessage(lpMsg *_MSG) (res uintptr) = user32.DispatchMessageW
//sys destroyWindow(hwnd windows.HWND) (err error) [int32(failretval)==0] = user32.DestroyWindow
//sys postQuitMessage(exitCode int32) = user32.PostQuitMessage
//sys registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) [int32(failretval)==0] = wtsapi32.WTSRegisterSessionNotificationEx
//sys unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) [int32(failretval)==0] = wtsapi32.WTSUnRegisterSessionNotificationEx

58
ipn/desktop/session.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"fmt"
"tailscale.com/ipn/ipnauth"
)
// SessionID is a unique identifier of a desktop session.
type SessionID uint
// SessionStatus is the status of a desktop session.
type SessionStatus int
const (
// ClosedSession is a session that does not exist, is not yet initialized by the OS,
// or has been terminated.
ClosedSession SessionStatus = iota
// ForegroundSession is a session that a user can interact with,
// such as when attached to a physical console or an active,
// unlocked RDP connection.
ForegroundSession
// BackgroundSession indicates that the session is locked, disconnected,
// or otherwise running without user presence or interaction.
BackgroundSession
)
// String implements [fmt.Stringer].
func (s SessionStatus) String() string {
switch s {
case ClosedSession:
return "Closed"
case ForegroundSession:
return "Foreground"
case BackgroundSession:
return "Background"
default:
panic("unreachable")
}
}
// Session is a state of a desktop session at a given point in time.
type Session struct {
ID SessionID // Identifier of the session; can be reused after the session is closed.
Status SessionStatus // The status of the session, such as foreground or background.
User ipnauth.Actor // User logged into the session.
}
// Description returns a human-readable description of the session.
func (s *Session) Description() string {
if maybeUsername, _ := s.User.Username(); maybeUsername != "" { // best effort
return fmt.Sprintf("Session %d - %q (%s)", s.ID, maybeUsername, s.Status)
}
return fmt.Sprintf("Session %d (%s)", s.ID, s.Status)
}

60
ipn/desktop/sessions.go Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"errors"
"runtime"
)
// ErrNotImplemented is returned by [NewSessionManager] when it is not
// implemented for the current GOOS.
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
// SessionInitCallback is a function that is called once per [Session].
// It returns an optional cleanup function that is called when the session
// is about to be destroyed, or nil if no cleanup is needed.
// It is not safe to call SessionManager methods from within the callback.
type SessionInitCallback func(session *Session) (cleanup func())
// SessionStateCallback is a function that reports the initial or updated
// state of a [Session], such as when it transitions between foreground and background.
// It is guaranteed to be called after all registered [SessionInitCallback] functions
// have completed, and before any cleanup functions are called for the same session.
// It is not safe to call SessionManager methods from within the callback.
type SessionStateCallback func(session *Session)
// SessionManager is an interface that provides access to desktop sessions on the current platform.
// It is safe for concurrent use.
type SessionManager interface {
// Init explicitly initializes the receiver.
// Unless the receiver is explicitly initialized, it will be lazily initialized
// on the first call to any other method.
// It is safe to call Init multiple times.
Init() error
// Sessions returns a session snapshot taken at the time of the call.
// Since sessions can be created or destroyed at any time, it may become
// outdated as soon as it is returned.
//
// It is primarily intended for logging and debugging.
// Prefer registering a [SessionInitCallback] or [SessionStateCallback]
// in contexts requiring stronger guarantees.
Sessions() (map[SessionID]*Session, error)
// RegisterInitCallback registers a [SessionInitCallback] that is called for each existing session
// and for each new session that is created, until the returned unregister function is called.
// If the specified [SessionInitCallback] returns a cleanup function, it is called when the session
// is about to be destroyed. The callback function is guaranteed to be called once and only once
// for each existing and new session.
RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error)
// RegisterStateCallback registers a [SessionStateCallback] that is called for each existing session
// and every time the state of a session changes, until the returned unregister function is called.
RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error)
// Close waits for all registered callbacks to complete
// and releases resources associated with the receiver.
Close() error
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package desktop
import "tailscale.com/types/logger"
// NewSessionManager returns a new [SessionManager] for the current platform,
// [ErrNotImplemented] if the platform is not supported, or an error if the
// session manager could not be created.
func NewSessionManager(logger.Logf) (SessionManager, error) {
return nil, ErrNotImplemented
}

View File

@@ -0,0 +1,672 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"tailscale.com/ipn/ipnauth"
"tailscale.com/types/logger"
"tailscale.com/util/must"
"tailscale.com/util/set"
)
// wtsManager is a [SessionManager] implementation for Windows.
type wtsManager struct {
logf logger.Logf
ctx context.Context // cancelled when the manager is closed
ctxCancel context.CancelFunc
initOnce func() error
watcher *sessionWatcher
mu sync.Mutex
sessions map[SessionID]*wtsSession
initCbs set.HandleSet[SessionInitCallback]
stateCbs set.HandleSet[SessionStateCallback]
}
// NewSessionManager returns a new [SessionManager] for the current platform,
func NewSessionManager(logf logger.Logf) (SessionManager, error) {
ctx, ctxCancel := context.WithCancel(context.Background())
m := &wtsManager{
logf: logf,
ctx: ctx,
ctxCancel: ctxCancel,
sessions: make(map[SessionID]*wtsSession),
}
m.watcher = newSessionWatcher(m.ctx, m.logf, m.sessionEventHandler)
m.initOnce = sync.OnceValue(func() error {
if err := waitUntilWTSReady(m.ctx); err != nil {
return fmt.Errorf("WTS is not ready: %w", err)
}
m.mu.Lock()
defer m.mu.Unlock()
if err := m.watcher.Start(); err != nil {
return fmt.Errorf("failed to start session watcher: %w", err)
}
var err error
m.sessions, err = enumerateSessions()
return err // may be nil or non-nil
})
return m, nil
}
// Init implements [SessionManager].
func (m *wtsManager) Init() error {
return m.initOnce()
}
// Sessions implements [SessionManager].
func (m *wtsManager) Sessions() (map[SessionID]*Session, error) {
if err := m.initOnce(); err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
sessions := make(map[SessionID]*Session, len(m.sessions))
for _, s := range m.sessions {
sessions[s.id] = s.AsSession()
}
return sessions, nil
}
// RegisterInitCallback implements [SessionManager].
func (m *wtsManager) RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error) {
if err := m.initOnce(); err != nil {
return nil, err
}
if cb == nil {
return nil, errors.New("nil callback")
}
m.mu.Lock()
defer m.mu.Unlock()
handle := m.initCbs.Add(cb)
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, s := range m.sessions {
if cleanup := cb(s.AsSession()); cleanup != nil {
s.cleanup = append(s.cleanup, cleanup)
}
}
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.initCbs, handle)
}, nil
}
// RegisterStateCallback implements [SessionManager].
func (m *wtsManager) RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error) {
if err := m.initOnce(); err != nil {
return nil, err
}
if cb == nil {
return nil, errors.New("nil callback")
}
m.mu.Lock()
defer m.mu.Unlock()
handle := m.stateCbs.Add(cb)
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, s := range m.sessions {
cb(s.AsSession())
}
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.stateCbs, handle)
}, nil
}
func (m *wtsManager) sessionEventHandler(id SessionID, event uint32) {
m.mu.Lock()
defer m.mu.Unlock()
switch event {
case windows.WTS_SESSION_LOGON:
// The session may have been created after we started watching,
// but before the initial enumeration was performed.
// Do not create a new session if it already exists.
if _, _, err := m.getOrCreateSessionLocked(id); err != nil {
m.logf("[unexpected] getOrCreateSessionLocked(%d): %v", id, err)
}
case windows.WTS_SESSION_LOCK:
if err := m.setSessionStatusLocked(id, BackgroundSession); err != nil {
m.logf("[unexpected] setSessionStatusLocked(%d, BackgroundSession): %v", id, err)
}
case windows.WTS_SESSION_UNLOCK:
if err := m.setSessionStatusLocked(id, ForegroundSession); err != nil {
m.logf("[unexpected] setSessionStatusLocked(%d, ForegroundSession): %v", id, err)
}
case windows.WTS_SESSION_LOGOFF:
if err := m.deleteSessionLocked(id); err != nil {
m.logf("[unexpected] deleteSessionLocked(%d): %v", id, err)
}
}
}
func (m *wtsManager) getOrCreateSessionLocked(id SessionID) (_ *wtsSession, created bool, err error) {
if s, ok := m.sessions[id]; ok {
return s, false, nil
}
s, err := newWTSSession(id, ForegroundSession)
if err != nil {
return nil, false, err
}
m.sessions[id] = s
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, cb := range m.initCbs {
if cleanup := cb(session); cleanup != nil {
s.cleanup = append(s.cleanup, cleanup)
}
}
for _, cb := range m.stateCbs {
cb(session)
}
return s, true, err
}
func (m *wtsManager) setSessionStatusLocked(id SessionID, status SessionStatus) error {
s, _, err := m.getOrCreateSessionLocked(id)
if err != nil {
return err
}
if s.status == status {
return nil
}
s.status = status
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, cb := range m.stateCbs {
cb(session)
}
return nil
}
func (m *wtsManager) deleteSessionLocked(id SessionID) error {
s, ok := m.sessions[id]
if !ok {
return nil
}
s.status = ClosedSession
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks (and [wtsSession.close]!) in a separate goroutine?
for _, cb := range m.stateCbs {
cb(session)
}
delete(m.sessions, id)
return s.close()
}
func (m *wtsManager) Close() error {
m.ctxCancel()
if m.watcher != nil {
err := m.watcher.Stop()
if err != nil {
return err
}
m.watcher = nil
}
m.mu.Lock()
defer m.mu.Unlock()
m.initCbs = nil
m.stateCbs = nil
errs := make([]error, 0, len(m.sessions))
for _, s := range m.sessions {
errs = append(errs, s.close())
}
m.sessions = nil
return errors.Join(errs...)
}
type wtsSession struct {
id SessionID
user *ipnauth.WindowsActor
status SessionStatus
cleanup []func()
}
func newWTSSession(id SessionID, status SessionStatus) (*wtsSession, error) {
var token windows.Token
if err := windows.WTSQueryUserToken(uint32(id), &token); err != nil {
return nil, err
}
user, err := ipnauth.NewWindowsActorWithToken(token)
if err != nil {
return nil, err
}
return &wtsSession{id, user, status, nil}, nil
}
// enumerateSessions returns a map of all active WTS sessions.
func enumerateSessions() (map[SessionID]*wtsSession, error) {
const reserved, version uint32 = 0, 1
var numSessions uint32
var sessionInfos *windows.WTS_SESSION_INFO
if err := windows.WTSEnumerateSessions(_WTS_CURRENT_SERVER_HANDLE, reserved, version, &sessionInfos, &numSessions); err != nil {
return nil, fmt.Errorf("WTSEnumerateSessions failed: %w", err)
}
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessionInfos)))
sessions := make(map[SessionID]*wtsSession, numSessions)
for _, si := range unsafe.Slice(sessionInfos, numSessions) {
status := _WTS_CONNECTSTATE_CLASS(si.State).ToSessionStatus()
if status == ClosedSession {
// The session does not exist as far as we're concerned.
// It may be in the process of being created or destroyed,
// or be a special "listener" session, etc.
continue
}
id := SessionID(si.SessionID)
session, err := newWTSSession(id, status)
if err != nil {
continue
}
sessions[id] = session
}
return sessions, nil
}
func (s *wtsSession) AsSession() *Session {
return &Session{
ID: s.id,
Status: s.status,
// wtsSession owns the user; don't let the caller close it
User: ipnauth.WithoutClose(s.user),
}
}
func (m *wtsSession) close() error {
for _, cleanup := range m.cleanup {
cleanup()
}
m.cleanup = nil
if m.user != nil {
if err := m.user.Close(); err != nil {
return err
}
m.user = nil
}
return nil
}
type sessionEventHandler func(id SessionID, event uint32)
// TODO(nickkhyl): implement a sessionWatcher that does not use the message queue.
// One possible approach is to have the tailscaled service register a HandlerEx function
// and stream SERVICE_CONTROL_SESSIONCHANGE events to the tailscaled subprocess
// (the actual tailscaled backend), exposing these events via [sessionWatcher]/[wtsManager].
//
// See tailscale/corp#26477 for details and tracking.
type sessionWatcher struct {
logf logger.Logf
ctx context.Context // canceled to stop the watcher
ctxCancel context.CancelFunc // cancels the watcher
hWnd windows.HWND // window handle for receiving session change notifications
handler sessionEventHandler // called on session events
mu sync.Mutex
doneCh chan error // written to when the watcher exits; nil if not started
}
func newSessionWatcher(ctx context.Context, logf logger.Logf, handler sessionEventHandler) *sessionWatcher {
ctx, cancel := context.WithCancel(ctx)
return &sessionWatcher{logf: logf, ctx: ctx, ctxCancel: cancel, handler: handler}
}
func (sw *sessionWatcher) Start() error {
sw.mu.Lock()
defer sw.mu.Unlock()
select {
case <-sw.ctx.Done():
return fmt.Errorf("sessionWatcher already stopped: %w", sw.ctx.Err())
default:
}
if sw.doneCh != nil {
// Already started.
return nil
}
sw.doneCh = make(chan error, 1)
startedCh := make(chan error, 1)
go sw.run(startedCh, sw.doneCh)
if err := <-startedCh; err != nil {
return err
}
// Signal the window to unsubscribe from session notifications
// and shut down gracefully when the sessionWatcher is stopped.
context.AfterFunc(sw.ctx, func() {
sendMessage(sw.hWnd, _WM_CLOSE, 0, 0)
})
return nil
}
func (sw *sessionWatcher) run(started, done chan<- error) {
runtime.LockOSThread()
defer func() {
runtime.UnlockOSThread()
close(done)
}()
err := sw.createMessageWindow()
started <- err
if err != nil {
return
}
pumpThreadMessages()
}
// Stop stops the session watcher and waits for it to exit.
func (sw *sessionWatcher) Stop() error {
sw.ctxCancel()
sw.mu.Lock()
doneCh := sw.doneCh
sw.doneCh = nil
sw.mu.Unlock()
if doneCh != nil {
return <-doneCh
}
return nil
}
const watcherWindowClassName = "Tailscale-SessionManager"
var watcherWindowClassName16 = sync.OnceValue(func() *uint16 {
return must.Get(syscall.UTF16PtrFromString(watcherWindowClassName))
})
var registerSessionManagerWindowClass = sync.OnceValue(func() error {
var hInst windows.Handle
if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
return fmt.Errorf("GetModuleHandle: %w", err)
}
wc := _WNDCLASSEX{
CbSize: uint32(unsafe.Sizeof(_WNDCLASSEX{})),
HInstance: hInst,
LpfnWndProc: syscall.NewCallback(sessionWatcherWndProc),
LpszClassName: watcherWindowClassName16(),
}
if _, err := registerClassEx(&wc); err != nil {
return fmt.Errorf("RegisterClassEx(%q): %w", watcherWindowClassName, err)
}
return nil
})
func (sw *sessionWatcher) createMessageWindow() error {
if err := registerSessionManagerWindowClass(); err != nil {
return err
}
_, err := createWindowEx(
0, // dwExStyle
watcherWindowClassName16(), // lpClassName
nil, // lpWindowName
0, // dwStyle
0, // x
0, // y
0, // nWidth
0, // nHeight
_HWND_MESSAGE, // hWndParent; message-only window
0, // hMenu
0, // hInstance
unsafe.Pointer(sw), // lpParam
)
if err != nil {
return fmt.Errorf("CreateWindowEx: %w", err)
}
return nil
}
func (sw *sessionWatcher) wndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
switch msg {
case _WM_CREATE:
err := registerSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd, _NOTIFY_FOR_ALL_SESSIONS)
if err != nil {
sw.logf("[unexpected] failed to register for session notifications: %v", err)
return ^uintptr(0)
}
sw.logf("registered for session notifications")
case _WM_WTSSESSION_CHANGE:
sw.handler(SessionID(lParam), uint32(wParam))
return 0
case _WM_CLOSE:
if err := destroyWindow(hWnd); err != nil {
sw.logf("[unexpected] failed to destroy window: %v", err)
}
return 0
case _WM_DESTROY:
err := unregisterSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd)
if err != nil {
sw.logf("[unexpected] failed to unregister session notifications callback: %v", err)
}
sw.logf("unregistered from session notifications")
return 0
case _WM_NCDESTROY:
sw.hWnd = 0
postQuitMessage(0) // quit the message loop for this thread
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
func (sw *sessionWatcher) setHandle(hwnd windows.HWND) error {
sw.hWnd = hwnd
setLastError(0)
_, err := setWindowLongPtr(sw.hWnd, _GWLP_USERDATA, uintptr(unsafe.Pointer(sw)))
return err // may be nil or non-nil
}
func sessionWatcherByHandle(hwnd windows.HWND) *sessionWatcher {
val, _ := getWindowLongPtr(hwnd, _GWLP_USERDATA)
return (*sessionWatcher)(unsafe.Pointer(val))
}
func sessionWatcherWndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
if msg == _WM_NCCREATE {
cs := (*_CREATESTRUCT)(unsafe.Pointer(lParam))
sw := (*sessionWatcher)(unsafe.Pointer(cs.CreateParams))
if sw == nil {
return 0
}
if err := sw.setHandle(hWnd); err != nil {
return 0
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
if sw := sessionWatcherByHandle(hWnd); sw != nil {
return sw.wndProc(hWnd, msg, wParam, lParam)
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
func pumpThreadMessages() {
var msg _MSG
for getMessage(&msg, 0, 0, 0) != 0 {
translateMessage(&msg)
dispatchMessage(&msg)
}
}
// waitUntilWTSReady waits until the Windows Terminal Services (WTS) is ready.
// This is necessary because the WTS API functions may fail if called before
// the WTS is ready.
//
// https://web.archive.org/web/20250207011738/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotificationex
func waitUntilWTSReady(ctx context.Context) error {
eventName16, err := windows.UTF16PtrFromString(`Global\TermSrvReadyEvent`)
if err != nil {
return err
}
event, err := windows.OpenEvent(windows.SYNCHRONIZE, false, eventName16)
if err != nil {
return err
}
defer windows.CloseHandle(event)
return waitForContextOrHandle(ctx, event)
}
// waitForContextOrHandle waits for either the context to be done or a handle to be signaled.
func waitForContextOrHandle(ctx context.Context, handle windows.Handle) error {
contextDoneEvent, cleanup, err := channelToEvent(ctx.Done())
if err != nil {
return err
}
defer cleanup()
handles := []windows.Handle{contextDoneEvent, handle}
waitCode, err := windows.WaitForMultipleObjects(handles, false, windows.INFINITE)
if err != nil {
return err
}
waitCode -= windows.WAIT_OBJECT_0
if waitCode == 0 { // contextDoneEvent
return ctx.Err()
}
return nil
}
// channelToEvent returns an auto-reset event that is set when the channel
// becomes receivable, including when the channel is closed.
func channelToEvent[T any](c <-chan T) (evt windows.Handle, cleanup func(), err error) {
evt, err = windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, nil, err
}
cancel := make(chan struct{})
go func() {
select {
case <-cancel:
return
case <-c:
}
windows.SetEvent(evt)
}()
cleanup = func() {
close(cancel)
windows.CloseHandle(evt)
}
return evt, cleanup, nil
}
type _WNDCLASSEX struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance windows.Handle
HIcon windows.Handle
HCursor windows.Handle
HbrBackground windows.Handle
LpszMenuName *uint16
LpszClassName *uint16
HIconSm windows.Handle
}
type _CREATESTRUCT struct {
CreateParams uintptr
Instance windows.Handle
Menu windows.Handle
Parent windows.HWND
Cy int32
Cx int32
Y int32
X int32
Style int32
Name *uint16
ClassName *uint16
ExStyle uint32
}
type _POINT struct {
X, Y int32
}
type _MSG struct {
HWnd windows.HWND
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt _POINT
}
const (
_WM_CREATE = 1
_WM_DESTROY = 2
_WM_CLOSE = 16
_WM_NCCREATE = 129
_WM_QUIT = 18
_WM_NCDESTROY = 130
// _WM_WTSSESSION_CHANGE is a message sent to windows that have registered
// for session change notifications, informing them of changes in session state.
//
// https://web.archive.org/web/20250207012421/https://learn.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
_WM_WTSSESSION_CHANGE = 0x02B1
)
const _GWLP_USERDATA = -21
const _HWND_MESSAGE = ^windows.HWND(2)
// _NOTIFY_FOR_ALL_SESSIONS indicates that the window should receive
// session change notifications for all sessions on the specified server.
const _NOTIFY_FOR_ALL_SESSIONS = 1
// _WTS_CURRENT_SERVER_HANDLE indicates that the window should receive
// session change notifications for the host itself rather than a remote server.
const _WTS_CURRENT_SERVER_HANDLE = windows.Handle(0)
// _WTS_CONNECTSTATE_CLASS represents the connection state of a session.
//
// https://web.archive.org/web/20250206082427/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ne-wtsapi32-wts_connectstate_class
type _WTS_CONNECTSTATE_CLASS int32
// ToSessionStatus converts cs to a [SessionStatus].
func (cs _WTS_CONNECTSTATE_CLASS) ToSessionStatus() SessionStatus {
switch cs {
case windows.WTSActive:
return ForegroundSession
case windows.WTSDisconnected:
return BackgroundSession
default:
// The session does not exist as far as we're concerned.
return ClosedSession
}
}

View File

@@ -0,0 +1,159 @@
// Code generated by 'go generate'; DO NOT EDIT.
package desktop
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
moduser32 = windows.NewLazySystemDLL("user32.dll")
modwtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
procSetLastError = modkernel32.NewProc("SetLastError")
procCreateWindowExW = moduser32.NewProc("CreateWindowExW")
procDefWindowProcW = moduser32.NewProc("DefWindowProcW")
procDestroyWindow = moduser32.NewProc("DestroyWindow")
procDispatchMessageW = moduser32.NewProc("DispatchMessageW")
procGetMessageW = moduser32.NewProc("GetMessageW")
procGetWindowLongPtrW = moduser32.NewProc("GetWindowLongPtrW")
procPostQuitMessage = moduser32.NewProc("PostQuitMessage")
procRegisterClassExW = moduser32.NewProc("RegisterClassExW")
procSendMessageW = moduser32.NewProc("SendMessageW")
procSetWindowLongPtrW = moduser32.NewProc("SetWindowLongPtrW")
procTranslateMessage = moduser32.NewProc("TranslateMessage")
procWTSRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSRegisterSessionNotificationEx")
procWTSUnRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSUnRegisterSessionNotificationEx")
)
func setLastError(dwErrorCode uint32) {
syscall.Syscall(procSetLastError.Addr(), 1, uintptr(dwErrorCode), 0, 0)
return
}
func createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) {
r0, _, e1 := syscall.Syscall12(procCreateWindowExW.Addr(), 12, uintptr(dwExStyle), uintptr(unsafe.Pointer(lpClassName)), uintptr(unsafe.Pointer(lpWindowName)), uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(nWidth), uintptr(nHeight), uintptr(hWndParent), uintptr(hMenu), uintptr(hInstance), uintptr(lpParam))
hWnd = windows.HWND(r0)
if hWnd == 0 {
err = errnoErr(e1)
}
return
}
func defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
r0, _, _ := syscall.Syscall6(procDefWindowProcW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
res = uintptr(r0)
return
}
func destroyWindow(hwnd windows.HWND) (err error) {
r1, _, e1 := syscall.Syscall(procDestroyWindow.Addr(), 1, uintptr(hwnd), 0, 0)
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}
func dispatchMessage(lpMsg *_MSG) (res uintptr) {
r0, _, _ := syscall.Syscall(procDispatchMessageW.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
res = uintptr(r0)
return
}
func getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) {
r0, _, _ := syscall.Syscall6(procGetMessageW.Addr(), 4, uintptr(unsafe.Pointer(lpMsg)), uintptr(hwnd), uintptr(msgMin), uintptr(msgMax), 0, 0)
ret = int32(r0)
return
}
func getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) {
r0, _, e1 := syscall.Syscall(procGetWindowLongPtrW.Addr(), 2, uintptr(hwnd), uintptr(index), 0)
res = uintptr(r0)
if res == 0 && e1 != 0 {
err = errnoErr(e1)
}
return
}
func postQuitMessage(exitCode int32) {
syscall.Syscall(procPostQuitMessage.Addr(), 1, uintptr(exitCode), 0, 0)
return
}
func registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) {
r0, _, e1 := syscall.Syscall(procRegisterClassExW.Addr(), 1, uintptr(unsafe.Pointer(windowClass)), 0, 0)
atom = uint16(r0)
if atom == 0 {
err = errnoErr(e1)
}
return
}
func sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
r0, _, _ := syscall.Syscall6(procSendMessageW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
res = uintptr(r0)
return
}
func setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) {
r0, _, e1 := syscall.Syscall(procSetWindowLongPtrW.Addr(), 3, uintptr(hwnd), uintptr(index), uintptr(newLong))
res = uintptr(r0)
if res == 0 && e1 != 0 {
err = errnoErr(e1)
}
return
}
func translateMessage(lpMsg *_MSG) (res bool) {
r0, _, _ := syscall.Syscall(procTranslateMessage.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
res = r0 != 0
return
}
func registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) {
r1, _, e1 := syscall.Syscall(procWTSRegisterSessionNotificationEx.Addr(), 3, uintptr(hServer), uintptr(hwnd), uintptr(flags))
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}
func unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) {
r1, _, e1 := syscall.Syscall(procWTSUnRegisterSessionNotificationEx.Addr(), 2, uintptr(hServer), uintptr(hwnd), 0)
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
//go:generate go run tailscale.com/cmd/viewer -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// Package ipn implements the interactions between the Tailscale cloud
// control plane and the local network stack.

View File

@@ -17,6 +17,29 @@ import (
"tailscale.com/types/ptr"
)
// Clone makes a deep copy of LoginProfile.
// The result aliases no memory with the original.
func (src *LoginProfile) Clone() *LoginProfile {
if src == nil {
return nil
}
dst := new(LoginProfile)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {

View File

@@ -18,7 +18,73 @@ import (
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// View returns a read-only view of LoginProfile.
func (p *LoginProfile) View() LoginProfileView {
return LoginProfileView{ж: p}
}
// LoginProfileView provides a read-only view over LoginProfile.
//
// Its methods should only be called if `Valid()` returns true.
type LoginProfileView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *LoginProfile
}
// Valid reports whether v's underlying value is non-nil.
func (v LoginProfileView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LoginProfileView) AsStruct() *LoginProfile {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v LoginProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x LoginProfile
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v LoginProfileView) ID() ProfileID { return v.ж.ID }
func (v LoginProfileView) Name() string { return v.ж.Name }
func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile }
func (v LoginProfileView) Key() StateKey { return v.ж.Key }
func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID }
func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// View returns a read-only view of Prefs.
func (p *Prefs) View() PrefsView {

17
ipn/ipnauth/access.go Normal file
View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
// ProfileAccess is a bitmask representing the requested, required, or granted
// access rights to an [ipn.LoginProfile].
//
// It is not to be written to disk or transmitted over the network in its integer form,
// but rather serialized to a string or other format if ever needed.
type ProfileAccess uint
// Define access rights that might be granted or denied on a per-profile basis.
const (
// Disconnect is required to disconnect (or switch from) a Tailscale profile.
Disconnect = ProfileAccess(1 << iota)
)

View File

@@ -4,12 +4,19 @@
package ipnauth
import (
"context"
"encoding/json"
"fmt"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
)
// AuditLogFunc is any function that can be used to log audit actions performed by an [Actor].
//
// TODO(nickkhyl,barnstar): define a named string type for the action (in tailcfg?) and use it here.
type AuditLogFunc func(action, details string)
// Actor is any actor using the [ipnlocal.LocalBackend].
//
// It typically represents a specific OS user, indicating that an operation
@@ -27,6 +34,19 @@ type Actor interface {
// a connected LocalAPI client. Otherwise, it returns a zero value and false.
ClientID() (_ ClientID, ok bool)
// Context returns the context associated with the actor.
// It carries additional information about the actor
// and is canceled when the actor is done.
Context() context.Context
// CheckProfileAccess checks whether the actor has the necessary access rights
// to perform a given action on the specified Tailscale profile.
// It returns an error if access is denied.
//
// If the auditLogger is non-nil, it is used to write details about the action
// to the audit log when required by the policy.
CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogger AuditLogFunc) error
// IsLocalSystem reports whether the actor is the Windows' Local System account.
//
// Deprecated: this method exists for compatibility with the current (as of 2024-08-27)
@@ -89,3 +109,27 @@ func (id ClientID) MarshalJSON() ([]byte, error) {
func (id *ClientID) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &id.v)
}
type actorWithRequestReason struct {
Actor
ctx context.Context
}
// WithRequestReason returns an [Actor] that wraps the given actor and
// carries the specified request reason in its context.
func WithRequestReason(actor Actor, requestReason string) Actor {
ctx := apitype.RequestReasonKey.WithValue(actor.Context(), requestReason)
return &actorWithRequestReason{Actor: actor, ctx: ctx}
}
// Context implements [Actor].
func (a *actorWithRequestReason) Context() context.Context { return a.ctx }
type withoutCloseActor struct{ Actor }
// WithoutClose returns an [Actor] that does not expose the [ActorCloser] interface.
// In other words, _, ok := WithoutClose(actor).(ActorCloser) will always be false,
// even if the original actor implements [ActorCloser].
func WithoutClose(actor Actor) Actor {
return withoutCloseActor{actor}
}

View File

@@ -0,0 +1,102 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"context"
"errors"
"golang.org/x/sys/windows"
"tailscale.com/ipn"
"tailscale.com/types/lazy"
)
// WindowsActor implements [Actor].
var _ Actor = (*WindowsActor)(nil)
// WindowsActor represents a logged in Windows user.
type WindowsActor struct {
ctx context.Context
cancelCtx context.CancelFunc
token WindowsToken
uid ipn.WindowsUserID
username lazy.SyncValue[string]
}
// NewWindowsActorWithToken returns a new [WindowsActor] for the user
// represented by the given [windows.Token].
// It takes ownership of the token.
func NewWindowsActorWithToken(t windows.Token) (_ *WindowsActor, err error) {
tok := newToken(t)
uid, err := tok.UID()
if err != nil {
t.Close()
return nil, err
}
ctx, cancelCtx := context.WithCancel(context.Background())
return &WindowsActor{ctx: ctx, cancelCtx: cancelCtx, token: tok, uid: uid}, nil
}
// UserID implements [Actor].
func (a *WindowsActor) UserID() ipn.WindowsUserID {
return a.uid
}
// Username implements [Actor].
func (a *WindowsActor) Username() (string, error) {
return a.username.GetErr(a.token.Username)
}
// ClientID implements [Actor].
func (a *WindowsActor) ClientID() (_ ClientID, ok bool) {
// TODO(nickkhyl): assign and return a client ID when the actor
// represents a connected LocalAPI client.
return NoClientID, false
}
// Context implements [Actor].
func (a *WindowsActor) Context() context.Context {
return a.ctx
}
// CheckProfileAccess implements [Actor].
func (a *WindowsActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
if profile.LocalUserID() != a.UserID() {
// TODO(nickkhyl): return errors of more specific types and have them
// translated to the appropriate HTTP status codes in the API handler.
return errors.New("the target profile does not belong to the user")
}
return nil
}
// IsLocalSystem implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (a *WindowsActor) IsLocalSystem() bool {
// https://web.archive.org/web/2024/https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
const systemUID = ipn.WindowsUserID("S-1-5-18")
return a.uid == systemUID
}
// IsLocalAdmin implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (a *WindowsActor) IsLocalAdmin(operatorUID string) bool {
return a.token.IsElevated()
}
// Close releases resources associated with the actor
// and cancels its context.
func (a *WindowsActor) Close() error {
if a.token != nil {
if err := a.token.Close(); err != nil {
return err
}
a.token = nil
}
a.cancelCtx()
return nil
}

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