Compare commits

...

130 Commits

Author SHA1 Message Date
Will Norris
4dba0e3153 cmd/containerboot: add TS_WEBUI var to run web UI
Because the --webclient flag is only defined as part of 'tailscale set',
and containerboot only calls 'tailscale set' if TS_AUTH_ONCE is set,
this requires both TS_AUTH_ONCE and TS_WEBUI to be set to true in order
to activate the web client.

Updates #10261

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-31 12:48:56 -08:00
Percy Wegmann
fad6bae764 ipnlocal: log failure to get ssh host keys
When reporting ssh host keys to control, log a warning
if we're unable to get the SSH host keys.

Updates tailscale/escalations#21

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-01-30 16:57:16 -06:00
Chris Palmer
9744ad47e3 cmd/hello: avoid deprecated apis (#10957)
Updates #cleanup

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-29 14:18:49 -08:00
Will Norris
13f8a669d5 cmd/gitops-pusher: fix logic for checking credentials
gitops-pusher supports authenticating with an API key or OAuth
credentials (added in #7393). You shouldn't ever use both of those
together, so we error if both are set.

In tailscale/gitops-acl-action#24, OAuth support is being added to the
GitHub action. In that environment, both the TS_API_KEY and OAuth
variables will be set, even if they are empty values.  This causes an
error in gitops-pusher which expects only one to be set.

Update gitops-pusher to check that only one set of environment variables
are non-empty, rather than just checking if they are set.

Updates #7393

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-29 13:01:29 -08:00
Charlotte Brandhorst-Satzkorn
cce189bde1 words: i like the direction this list is taking
Updates tailscale/corp#14698

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-01-26 19:42:37 -08:00
Andrew Lytvynov
fbfc3b7e51 cmd/tailscale/cli: run Watch with NotifyNoPrivateKeys (#10950)
When running as non-root non-operator user, you get this error:
```
$ tailscale serve 8080
Access denied: watch IPN bus access denied, must set ipn.NotifyNoPrivateKeys when not running as admin/root or operator

Use 'sudo tailscale serve 8080' or 'tailscale up --operator=$USER' to not require root.
```

It should fail, but the error message is confusing.

With this fix:
```
$ tailscale serve 8080
sending serve config: Access denied: serve config denied

Use 'sudo tailscale serve 8080' or 'tailscale up --operator=$USER' to not require root.
```

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-25 14:59:34 -07:00
James Tucker
0f3b2e7b86 util/expvarx: add a time and concurrency limiting expvar.Func wrapper
expvarx.SafeFunc wraps an expvar.Func with a time limit. On reaching the
time limit, calls to Value return nil, and no new concurrent calls to
the underlying expvar.Func will be started until the call completes.

Updates tailscale/corp#16999
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-24 17:47:16 -08:00
Andrew Dunham
fd94d96e2b net/portmapper: support legacy "urn:dslforum-org" portmapping services
These are functionally the same as the "urn:schemas-upnp-org" services
with a few minor changes, and are still used by older devices. Support
them to improve our ability to obtain an external IP on such networks.

Updates #10911

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I05501fad9d6f0a3b8cf19fc95eee80e7d16cc2cf
2024-01-23 21:29:29 -05:00
Irbe Krumina
75f1d3e7d7 ipn/ipnlocal: fix failing test (#10937)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-23 19:02:07 +00:00
Irbe Krumina
6ee956333f ipn/ipnlocal: fix proxy path that matches mount point (#10864)
Don't append a trailing slash to a request path
to the reverse proxy that matches the mount point exactly.

Updates tailscale/tailscale#10730

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-23 18:12:56 +00:00
Jordan Whited
8b47322acc wgengine/magicsock: implement probing of UDP path lifetime (#10844)
This commit implements probing of UDP path lifetime on the tail end of
an active direct connection. Probing configuration has two parts -
Cliffs, which are various timeout cliffs of interest, and
CycleCanStartEvery, which limits how often a probing cycle can start,
per-endpoint. Initially a statically defined default configuration will
be used. The default configuration has cliffs of 10s, 30s, and 60s,
with a CycleCanStartEvery of 24h. Probing results are communicated via
clientmetric counters. Probing is off by default, and can be enabled
via control knob. Probing is purely informational and does not yet
drive any magicsock behaviors.

Updates #540

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-01-23 09:37:32 -08:00
James Tucker
0e2cb76abe appc: add test to ensure that individual IPs are not removed during route updates
If control advised the connector to advertise a route that had already
been discovered by DNS it would be incorrectly removed. Now those routes
are preserved.

Updates tailscale/corp#16833

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-22 17:50:55 -08:00
Charlotte Brandhorst-Satzkorn
ce4553b988 appc,ipn/ipnlocal: optimize preference adjustments when routes update
This change allows us to perform batch modification for new route
advertisements and route removals. Additionally, we now handle the case
where newly added routes are covered by existing ranges.

This change also introduces a new appctest package that contains some
shared functions used for testing.

Updates tailscale/corp#16833

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-01-22 17:37:16 -08:00
Irbe Krumina
370ec6b46b cmd/k8s-operator: don't proceed with Ingress that has no valid backends (#10919)
Do not provision resources for a tailscale Ingress that has no valid backends.

Updates tailscale/tailscale#10910

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-22 19:20:23 +00:00
Andrew Dunham
b45089ad85 net/portmapper: handle cases where we have no supported clients
This no longer results in a nil pointer exception when we get a valid
UPnP response with no supported clients.

Updates #10911

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I6e3715a49a193ff5261013871ad7fff197a4d77e
2024-01-22 12:46:19 -05:00
James Tucker
4e822c031f go.toolchain.rev: bump Tailscale Go version to 1.21.6
Updates tailscale/go#83

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 18:30:35 -08:00
Flakes Updater
b787c27c00 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-19 18:24:58 -08:00
James Tucker
7e3bcd297e go.mod,wgengine/netstack: bump gvisor
Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 18:23:53 -08:00
David Anderson
17eae5b0d3 tool/gocross: force use of our custom toolchain
The new 'toolchain' directive in go.mod can sometimes force
the use of an upstream toolchain against our wishes. Concurrently,
some of our dependencies have added the 'toolchain' directive, which
transitively adds it to our own go.mod. Force all uses of gocross to
ignore that directive and stick to our customized toolchain.

Updates #cleanup

Signed-off-by: David Anderson <danderson@tailscale.com>
2024-01-19 18:23:21 -08:00
David Anderson
ae79b2e784 tsweb: add a helper to validate redirect URLs
We issue redirects in a few different places, it's time to have
a common helper to do target validation.

Updates tailscale/corp#16875

Signed-off-by: David Anderson <danderson@tailscale.com>
2024-01-19 17:57:54 -08:00
Claire Wang
213d696db0 magicsock: mute noisy expected peer mtu related error (#10870) 2024-01-19 20:04:22 -05:00
kari-ts
62b056d677 VERSION.txt: this is v1.59.0 (#10884)
* VERSION.txt: this is v1.58.0

Signed-off-by: kari-ts <kari@tailscale.com>

* VERSION.txt: this is v1.59.0

---------

Signed-off-by: kari-ts <kari@tailscale.com>
2024-01-19 17:03:50 -08:00
Flakes Updater
5b4eb47300 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-19 16:53:05 -08:00
James Tucker
457102d070 go.mod: bump most deps for start of cycle
Plan9 CI is disabled. 3p dependencies do not build for the target.
Contributor enthusiasm appears to have ceased again, and no usage has
been made.

Skipped gvisor, nfpm, and k8s.

Updates #5794
Updates #8043

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-19 16:51:39 -08:00
Andrew Dunham
7a0392a8a3 wgengine/netstack: expose gVisor metrics through expvar
When tailscaled is run with "-debug 127.0.0.1:12345", these metrics are
available at:
    http://localhost:12345/debug/metrics

Updates #8210

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I19db6c445ac1f8344df2bc1066a3d9c9030606f8
2024-01-19 18:36:54 -05:00
as2643
832e5c781d util/nocasemaps: add AppendSliceElem method to nocasemaps (#10871)
Updates #7667

Signed-off-by: Anishka Singh <anishkasingh66@gmail.com>
2024-01-19 15:30:12 -08:00
ChandonPierre
2ce596ea7a cmd/k8s-operator/deploy: allow modifying operator tags via Helm values
Updates tailscale/tailscale#10659

Signed-off-by: Chandon Pierre <cpierre@coreweave.com>
2024-01-19 21:22:23 +00:00
Andrew Dunham
2ac7c0161b util/slicesx: add Filter function
For use in corp, where we appear to have re-implemented this in a few
places with varying signatures.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id863a87e674f3caa87945519be8e09650e9c1d76
2024-01-19 11:17:05 -05:00
Irbe Krumina
2aec4f2c43 ./github/workflows/kubemanifests.yaml: fix the paths whose changes should trigger test runs (#10885)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-19 01:51:10 +00:00
James Tucker
8250582fe6 ipn/ipnlocal: make app connector configuration concurrent
If there are routes changes as a side effect of an app connector
configuration update, the connector configuration may want to reenter a
lock, so must be started asynchronously.

Updates tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-18 12:26:58 -08:00
James Tucker
38a1cf748a control/controlclient,util/execqueue: extract execqueue into a package
This is a useful primitive for asynchronous execution of ordered work I
want to use in another change.

Updates tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-18 12:08:13 -08:00
Flakes Updater
32f01acc79 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-17 20:06:58 -08:00
James Tucker
24df1ef1ee appc,ipn/ipnlocal,types/appctype: implement control provided routes
Control can now send down a set of routes along with the domains, and
the routes will be advertised, with any newly overlapped routes being
removed to reduce the size of the routing table.

Fixes tailscale/corp#16833
Signed-off-by: James Tucker <james@tailscale.com>
2024-01-17 14:40:09 -08:00
Andrea Gottardo
543e7ed596 licenses: mention tvOS in apple.md (#10872)
Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
2024-01-16 18:32:20 -08:00
License Updater
3eba895293 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-16 15:43:51 -08:00
License Updater
9fa2c4605f licenses: update android licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-16 15:43:14 -08:00
Joe Tsai
c25968e1c5 all: make use of ctxkey everywhere (#10846)
Also perform minor cleanups on the ctxkey package itself.
Provide guidance on when to use ctxkey.Key[T] over ctxkey.New.
Also, allow for interface kinds because the value wrapping trick
also happens to fix edge cases with interfaces in Go.

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-01-16 13:56:23 -08:00
Joe Tsai
7732377cd7 tstime/rate: implement Value.{Marshal,Unmarshal}JSON (#8481)
Implement support for marshaling and unmarshaling a Value.

Updates tailscale/corp#8427

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-01-16 13:48:34 -08:00
Irbe Krumina
1c3c3d6752 cmd/k8s-operator: warn if unsupported Ingress Exact path type is used. (#10865)
To reduce the likelihood of breaking users,
if we implement stricter Exact path type matching in the future.

Updates tailscale/tailscale#10730

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-16 17:02:34 +00:00
Irbe Krumina
50b52dbd7d cmd/k8s-operator: sync StatefulSet labels to their Pods (#10861)
So that users have predictable label values to use when configuring network policies.

Updates tailscale/tailscale#10854

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-16 12:51:10 +00:00
Irbe Krumina
d0492fdee5 cmd/k8s-operator: adds a tailscale IngressClass resource, prints warning if class not found. (#10823)
* cmd/k8s-operator/deploy: deploy a Tailscale IngressClass resource.

Some Ingress validating webhooks reject Ingresses with
.spec.ingressClassName for which there is no matching IngressClass.

Additionally, validate that the expected IngressClass is present,
when parsing a tailscale `Ingress`. 
We currently do not utilize the IngressClass,
however we might in the future at which point
we might start requiring that the right class
for this controller instance actually exists.

Updates tailscale/tailscale#10820

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <anton@tailscale.com>
2024-01-16 12:48:15 +00:00
License Updater
381430eeca licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-13 14:02:42 -08:00
Joe Tsai
241a541864 util/ctxkey: add package for type-safe context keys (#10841)
The lack of type-safety in context.WithValue leads to the common pattern
of defining of package-scoped type to ensure global uniqueness:

	type fooKey struct{}

	func withFoo(ctx context, v Foo) context.Context {
		return context.WithValue(ctx, fooKey{}, v)
	}

	func fooValue(ctx context) Foo {
		v, _ := ctx.Value(fooKey{}).(Foo)
		return v
	}

where usage becomes:

	ctx = withFoo(ctx, foo)
	foo := fooValue(ctx)

With many different context keys, this can be quite tedious.

Using generics, we can simplify this as:

	var fooKey = ctxkey.New("mypkg.fooKey", Foo{})

where usage becomes:

	ctx = fooKey.WithValue(ctx, foo)
	foo := fooKey.Value(ctx)

See https://go.dev/issue/49189

Updates #cleanup

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2024-01-12 17:35:48 -08:00
kari-ts
c9fd166cc6 net/netmon: when a new network is added, trigger netmon update (#10840)
Fixes #10107
2024-01-12 16:03:04 -08:00
Will Norris
236531c5fc ipn/ipnserver: always allow Windows SYSTEM user to connect
When establishing connections to the ipnserver, we validate that the
local user is allowed to connect.  If Tailscale is currently being
managed by a different user (primarily for multi-user Windows installs),
we don't allow the connection.

With the new device web UI, the inbound connection is coming from
tailscaled itself, which is often running as "NT AUTHORITY\SYSTEM".
In this case, we still want to allow the connection, even though it
doesn't match the user running the Tailscale GUI. The SYSTEM user has
full access to everything on the system anyway, so this doesn't escalate
privileges.

Eventually, we want the device web UI to run outside of the tailscaled
process, at which point this exception would probably not be needed.

Updates tailscale/corp#16393

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-12 14:37:53 -08:00
James Tucker
7100b6e721 derp: optimize another per client field alignment
Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:05:39 -08:00
James Tucker
ee20327496 derp: remove unused per-client struct field
Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:05:31 -08:00
OSS Updater
d841ddcb13 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-01-12 16:04:58 -05:00
James Tucker
a7f65b40c5 derp: optimize field order to reduce GC cost
See the field alignment lints for more information.
Reductions are 64->24 and 64->32 respectively.

Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-12 13:04:50 -08:00
Charlotte Brandhorst-Satzkorn
e6910974ca cmd/tailscale/cli: add description to exit-node CLI command
This change adds a description to the exit-node CLI command. This
description will be displayed when using `tailscale -h` and `tailscale
exit-node -h`.

Fixes #10787

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2024-01-12 10:06:09 -08:00
Irbe Krumina
169778e23b cmd/k8s-operator: minor fix in name gen (#10830)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-12 10:08:22 +00:00
Will Norris
b89c113365 client/web: skip connectivity check on https
The manage client always listens on http (non-secure) port 5252.  If the
login client is loaded over https, then the connectivity check to `/ok`
will fail with a mixed-content error. Mixed-content enforcement is a
browser setting that we have no control over, so there's no way around
this.

In this case of the login client being loaded over https, we skip the
connectivity check entirely.  We will always render the sign-in button,
though we don't know for sure if the user has connectivity, so we
provide some additional help text in case they have trouble signing in.

Updates hassio-addons/addon-tailscale#314

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-11 14:51:29 -08:00
James Tucker
ff9c1ebb4a derp: reduce excess goroutines blocking on broadcasts
Observed on one busy derp node, there were 600 goroutines blocked
writing to this channel, which represents not only more blocked routines
than we need, but also excess wake-ups downstream as the latent
goroutines writes represent no new work.

Updates #self

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-11 14:47:17 -08:00
Irbe Krumina
5cc1bfe82d cmd/k8s-operator: remove configuration knob for Connector (#10791)
The configuration knob (that defaulted to Connector being disabled)
was added largely because the Connector CRD had to be installed in a separate step.
Now when the CRD has been added to both chart and static manifest, we can have it on by default.

Updates tailscale/tailscale#10878

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-11 20:03:53 +00:00
Irbe Krumina
469af614b0 cmd/k8s-operator: fix base truncating for extra long Service names (#10825)
cmd/k8s-operator: fix base truncating for extra long Service names

StatefulSet names for ingress/egress proxies are calculated
using Kubernetes name generator and the parent resource name
as a base.
The name generator also cuts the base, but has a higher max cap.
This commit fixes a bug where, if we get a shortened base back
from the generator, we cut off too little as the base that we
have cut will be passed into the generator again, which will
then itself cut less because the base is shorter- so we end up
with a too long name again.

Updates tailscale/tailscale#10807

Co-authored-by: Maisem Ali <maisem@tailscale.com>
Signed-off-by: Irbe Krumina <irbekrm@gmail.com>
2024-01-11 20:02:03 +00:00
Sonia Appasamy
331a6d105f client/web: add initial types for using peer capabilities
Sets up peer capability types for future use within the web client
views and APIs.

Updates tailscale/corp#16695

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-11 11:20:24 -05:00
Andrew Dunham
6540d1f018 wgengine/router: look up absolute path to netsh.exe on Windows
This is in response to logs from a customer that show that we're unable
to run netsh due to the following error:

    router: firewall: adding Tailscale-Process rule to allow UDP for "C:\\Program Files\\Tailscale\\tailscaled.exe" ...
    router: firewall: error adding Tailscale-Process rule: exec: "netsh": cannot run executable found relative to current directory:

There's approximately no reason to ever dynamically look up the path of
a system utility like netsh.exe, so instead let's first look for it
in the System32 directory and only if that fails fall back to the
previous behaviour.

Updates #10804

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I68cfeb4cab091c79ccff3187d35f50359a690573
2024-01-10 20:20:19 -05:00
Irbe Krumina
ca48db0d60 Makefile,build_docker.sh: allow to configure target platform. (#10806)
Build dev tailscale and k8s-operator images for linux/amd64 only by default,
make it possible to configure target build platform via PLATFORM var.

Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-10 19:19:20 +00:00
Flakes Updater
91c7dfe85c go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-10 09:25:32 -08:00
Andrew Lytvynov
86e476c8d1 version/mkversion: allow version override with $TS_VERSION_OVERRIDE (#10799)
This is useful to build local binaries with custom versions to test
version-specific logic (like updates).

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

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-10 09:03:11 -08:00
Andrew Lytvynov
4ec6a78551 go.mod: update golang-x-crypto fork (#10786)
Pick up a bunch of recent upstream commits.

Updates #8593

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-10 09:02:33 -08:00
Will Norris
84ab040f02 safesocket: detect macsys from within tailscaled
Use the helper method from the version package to detect that we are
running the macsys network extension. This method does the same check
for the HOME environment variable (which works fine in most cases) as
well as the name of the executable (which is needed for the web client).

Updates tailscale/corp#16393

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-10 08:15:40 -08:00
OSS Updater
e7d52eb2f8 go.mod: update web-client-prebuilt module
Signed-off-by: OSS Updater <noreply+oss-updater@tailscale.com>
2024-01-10 10:59:53 -05:00
Irbe Krumina
35f49ac99e cmd/k8s-operator: add Connector CRD to Helm chart and static manifests (#10775)
cmd/k8s-operator: add CRD to chart and static manifest

Add functionality to insert CRD to chart at package time.
Insert CRD to static manifests as this is where they are currently consumed from.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-10 14:20:22 +00:00
Sonia Appasamy
ea9c7f991a cli/set: add printout when web client started
Prints a helpful message with the web UI's address when running
tailscale set --webclient.

Updates tailscale/corp#16345

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-09 17:31:06 -05:00
Rhea Ghosh
4ce33c9758 taildrop: remove breaking abstraction layers for apple (#10728)
Removes the avoidFinalRename logic and all associated code as it is no longer required by the Apple clients.
Enables resume logic to be usable for Apple clients.

Fixes tailscale/corp#14772

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2024-01-09 14:11:34 -06:00
Andrew Lytvynov
7df9af2f5c .github/workflows/govulncheck: migrate to a Github App (#10793)
Send failures to a new channel using a github app token instead of
webhook URL.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-09 11:51:08 -08:00
Andrew Dunham
20f3f706a4 net/netutil: allow 16-bit 4via6 site IDs
The prefix has space for 32-bit site IDs, but the validateViaPrefix
function would previously have disallowed site IDs greater than 255.

Fixes tailscale/corp#16470

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4cdb0711dafb577fae72d86c4014cf623fa538ef
2024-01-09 10:30:46 -05:00
Irbe Krumina
05093ea7d9 cmd/k8s-operator,k8s-operator: allow the operator to deploy exit nodes via Connector custom resource (#10724)
cmd/k8s-operator/deploy/crds,k8s-operator/apis/v1alpha1: allow to define an exit node via Connector CR.

Make it possible to define an exit node to be deployed to a Kubernetes cluster
via Connector Custom resource.

Also changes to Connector API so that one Connector corresponds
to one Tailnet node that can be either a subnet router or an exit
node or both.

The Kubernetes operator parses Connector custom resource and,
if .spec.isExitNode is set, configures that Tailscale node deployed
for that connector as an exit node.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Anton Tolchanov <anton@tailscale.com>
2024-01-09 14:13:22 +00:00
James Tucker
953fa80c6f cmd/{derper,stund},net/stunserver: add standalone stun server
Add a standalone server for STUN that can be hosted independently of the
derper, and factor that back into the derper.

Fixes #8434
Closes #8435
Closes #10745

Signed-off-by: James Tucker <james@tailscale.com>
2024-01-08 16:22:33 -08:00
Will Norris
569b91417f client/web: ensure path prefix has a leading slash
This is simply an extra check to prevent hypothetical issues if a prefix
such as `--prefix="javascript:alert(1)"` was provided.  This isn't
really necessary since the prefix is a configuration flag provided by
the device owner, not user input.  But it does enforce that we are
always interpreting the provided value as a path relative to the root.

Fixes: tailscale/corp#16268

Signed-off-by: Will Norris <will@tailscale.com>
2024-01-08 12:04:02 -08:00
License Updater
e26ee6952f licenses: update win/apple licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-08 11:49:52 -08:00
License Updater
7b113a2d06 licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply+license-updater@tailscale.com>
2024-01-08 11:47:47 -08:00
Andrew Lytvynov
d96e0a553f tstest/integration: add tests for auto-update defaulting behavior (#10763)
Updates #16244

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-08 09:32:18 -08:00
Sonia Appasamy
55d302b48e client/web: rename Disconnect to Log out
For consistency w/ the CLI command. And to be more accurate to what
is actually happening on this action - node key is expired.

Also updates the disconnected view shown after logout.

Updates #10261

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2024-01-08 12:28:03 -05:00
Irbe Krumina
133699284e cmd/containerboot: add EXPERIMENTAL_TS_CONFIGFILE_PATH env var to allow passing tailscaled config in a file (#10759)
* cmd/containerboot: optionally configure tailscaled with a configfile.

If EXPERIMENTAL_TS_CONFIGFILE_PATH env var is set,
only run tailscaled with the provided config file.
Do not run 'tailscale up' or 'tailscale set'.

* cmd/containerboot: store containerboot accept_dns val in bool pointer

So that we can distinguish between the value being set to
false explicitly bs being unset.

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-08 16:14:06 +00:00
Adrian Dewhurst
c05c4bdce4 ipn: apply ControlURL policy before login
Unlike most prefs, the ControlURL policy needs to take effect before
login. This resolves an issue where on first start, even when the
ControlURL policy is set, it will generate a login URL to the Tailscale
SaaS server.

Updates tailscale/coral#118
Fixes #10736

Change-Id: I6da2a521f64028c15dbb6ac8175839fc3cc4e858
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-01-05 19:58:01 -05:00
Adrian Dewhurst
d50303bef7 docs: add Windows administrative template
To make setting Windows policies easier, this adds ADMX policy
descriptions.

Fixes #6495
Updates ENG-2515

Change-Id: If4613c9d8ec734afec8bd781575e24b4aef9bb73
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-01-05 19:44:19 -05:00
Andrew Dunham
35c303227a net/dns/resolver: add ID to verbose logs in forwarder
To make it easier to correlate the starting/ending log messages.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I2802d53ad98e19bc8914bc58f8c04d4443227b26
2024-01-05 15:25:49 -05:00
Rhea Ghosh
dbe70962b1 taildrop: Allow category Z unicode characters (#10750)
This will expand the unicode character categories that we allow for valid filenames to go from "L, M, N, P, S, and the ASCII space character" to "L, M, N, P, S, Zs"

Fixes #10105

Signed-off-by: Rhea Ghosh <rhea@tailscale.com>
2024-01-05 12:53:24 -06:00
Andrew Dunham
d3574a350f cmd/tailscale, ipn/ipnlocal: add 'debug dial-types' command
This command allows observing whether a given dialer ("SystemDial",
"UserDial", etc.) will successfully obtain a connection to a provided
host, from inside tailscaled itself. This is intended to help debug a
variety of issues from subnet routers to split DNS setups.

Updates #9619

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie01ebb5469d3e287eac633ff656783960f697b84
2024-01-05 13:42:59 -05:00
Aaron Klotz
aed2cfec4e util/winutil: add some missing docs to restartmgr errors
Just a quick #cleanup.

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-01-05 10:12:08 -08:00
Andrew Dunham
46bdbb3878 cmd/tailscaled, tsnet: don't return an interface containing a nil pointer
This tripped me up when I was testing something and wrote:

    if conn != nil {
        conn.Close()
    }

In netstack mode, when an error occurred we were getting a non-nil error
and a non-nil interface that contained a nil pointer. Instead, just
return a nil interface value.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Id9ef3dd24529e0e8c53adc60ed914c31fbb10cc4
2024-01-05 11:44:17 -05:00
Andrew Lytvynov
29e98e18f8 ssh/tailssh: use a local error instead of gossh.ErrDenied (#10743)
ErrDenied was added in [our fork of
x/crypto/ssh](acc6f8fe8d)
to short-circuit auth attempts once one fails.

In the case of our callbacks, this error is returned when SSH policy
check determines that a connection should not be allowed. Both
`NoClientAuthCallback` and `PublicKeyHandler` check the policy and will
fail anyway. The `fakePasswordHandler` returns true only if
`NoClientAuthCallback` succeeds the policy check, so it checks it
indirectly too.

The difference here is that a client might attempt all 2-3 auth methods
instead of just `none` but will fail to authenticate regardless.

Updates #8593

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-01-05 08:02:42 -08:00
James 'zofrex' Sanderson
124dc10261 controlclient,tailcfg,types: expose MaxKeyDuration via localapi (#10401)
Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2024-01-05 12:06:12 +00:00
Andrea Gottardo
d9aeb30281 net/interfaces: handle iOS network transitions (#10680)
Updates #8022
Updates #6075

On iOS, we currently rely on delegated interface information to figure out the default route interface.  The NetworkExtension framework in iOS seems to set the delegate interface only once, upon the *creation* of the VPN tunnel. If a network transition (e.g. from Wi-Fi to Cellular) happens while the tunnel is connected, it will be ignored and we will still try to set Wi-Fi as the default route because the delegated interface is not getting updated as connectivity transitions.

Here we work around this on the Swift side with a NWPathMonitor instance that observes the interface name of the first currently satisfied network path. Our Swift code will call into `UpdateLastKnownDefaultRouteInterface`, so we can rely on that when it is set.

If for any reason the Swift machinery didn't work and we don't get any updates, here we also have some fallback logic: we try finding a hardcoded Wi-Fi interface called en0. If en0 is down, we fall back to cellular (pdp_ip0) as a last resort. This doesn't handle all edge cases like USB-Ethernet adapters or multiple Ethernet interfaces, but it is good enough to ensure connectivity isn't broken.

I tested this on iPhones and iPads running iOS 17.1 and it appears to work. Switching between different cellular plans on a dual SIM configuration also works (the interface name remains pdp_ip0).

Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
2024-01-04 09:40:18 -08:00
James 'zofrex' Sanderson
10c595d962 ipn/ipnlocal: refresh node key without blocking if cap enabled (#10529)
Updates tailscale/corp#16016

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
Co-authored-by: Maisem Ali <maisem@tailscale.com>
2024-01-04 17:29:04 +00:00
Irbe Krumina
3a9450bc06 cmd/containerboot: don't parse empty subnet routes (#10738)
Updates#cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-04 12:17:15 +00:00
Irbe Krumina
5a2eb26db3 cmd/containerboot: ensure that subnet routes can be unset. (#10734)
A Tailnet node can be told to stop advertise subnets by passing
an empty string to --advertise-routes flag.
Respect an explicitly passed empty value to TS_ROUTES env var
so that users have a way to stop containerboot acting as a subnet
router without recreating it.
Distinguish between TS_ROUTES being unset and empty.

Updates tailscale/tailscale#10708

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-04 09:17:04 +00:00
Aaron Klotz
e32a064659 cmd/tailscaled: don't create a network monitor in the parent tailscaled on Windows
The service is only used as a watchdog and for piping logs from the child
process. We shouldn't be creating a network monitor in that case.

Fixes #10732

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2024-01-03 11:57:34 -08:00
Andrew Dunham
fa3639783c net/portmapper: check returned epoch from PMP and PCP protocols
If the epoch that we see during a Probe is less than the existing epoch,
it means that the gateway has either restarted or reset its
configuration, and an existing mapping is no longer valid. Reset any
saved mapping(s) if we detect this case so that a future
createOrGetMapping will not attempt to re-use it.

Updates #10597

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ie3cddaf625cb94a29885f7a1eeea25dbf6b97b47
2024-01-03 14:17:50 -05:00
Jordan Whited
b084888e4d wgengine/magicsock: fix typos in docs (#10729)
Updates #cleanup

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2024-01-03 10:50:38 -08:00
Chris Palmer
1f1ab74250 tsweb: use object-src instead of plugin-types (#10719)
plugin-types is deprecated, and setting object-src: 'none' is best
practice. This should result in no functional change.

Fixes #10718

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-03 09:00:57 -08:00
Adrian Dewhurst
3d57c885bf logpolicy: use syspolicy to override LogTarget
Previously, for Windows clients only, a registry value named LogTarget
could override the log server, but only if the environment variable was
unset.

To allow administrators to enforce using a particular log server, switch
this to make the registry value take precedence over the environment
variable, and switch to the newer syspolicy.GetString so that the log
target can be specified by a GPO more easily.

Updates ENG-2515

Change-Id: Ia618986b0e07715d7db4c6df170a24d511c904c9
Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2024-01-03 10:34:35 -05:00
Flakes Updater
1406a9d494 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2024-01-02 22:50:03 -08:00
Irbe Krumina
e72f2b7791 go.{mod,sum}: bump mkctr (#10722)
go get github.com/tailscale/mkctr@bf50773ba7349ced8de812c3d5437e8618bd4fa7

Updates tailscale/tailscale#9902

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2024-01-03 06:30:13 +00:00
Anton Tolchanov
1d22265f69 release: add shebang to the debian postinst script
Seems like an omission, since we have it in postrm and prerm.

Fixes #10705

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-01-03 02:30:00 +00:00
Chris Palmer
5deeb56b95 cmd/tailscale/cli: document usage more clearly (#10681)
The IP argument is required; only the port is optional.

Updates #10605

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2024-01-02 17:43:08 -08:00
Aaron Klotz
5812093d31 util/winutil: publicize existing functions for opening read-only connections to the Windows Service Control Manager
We're going to need to access these from code outside winutil.

Updates #10215

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-12-22 10:52:50 -08:00
Andrew Dunham
cae6edf485 ipn/ipnlocal: fix data race with capForcedNetfilter field
Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I1fdad454198d7ea4a898dbff3062818b0db35167
2023-12-21 21:51:09 -05:00
Andrew Lytvynov
2716250ee8 all: cleanup unused code, part 2 (#10670)
And enable U1000 check in staticcheck.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-21 17:40:03 -08:00
Nick Khyl
c9836b454d net/netmon: fix goroutine leak in winMon if the monitor is never started
When the portable Monitor creates a winMon via newOSMon, we register
address and route change callbacks with Windows. Once a callback is hit,
it starts a goroutine that attempts to send the event into messagec and returns.
The newly started goroutine then blocks until it can send to the channel.
However, if the monitor is never started and winMon.Receive is never called,
the goroutines remain indefinitely blocked, leading to goroutine leaks and
significant memory consumption in the tailscaled service process on Windows.
Unlike the tailscaled subprocess, the service process creates but never starts
a Monitor.

This PR adds a check within the callbacks to confirm the monitor's active status,
and exits immediately if the monitor hasn't started.

Updates #9864

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2023-12-21 16:36:52 -06:00
Andrew Lytvynov
2e956713de safesocket: remove ConnectionStrategy (#10662)
This type seems to be a migration shim for TCP tailscaled sockets
(instead of unix/windows pipes). The `port` field was never set, so it
was effectively used as a string (`path` field).
Remove the whole type and simplify call sites to pass the socket path
directly to `safesocket.Connect`.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-21 12:55:14 -08:00
Andrew Lytvynov
1302bd1181 all: cleanup unused code, part 1 (#10661)
Run `staticcheck` with `U1000` to find unused code. This cleans up about
a half of it. I'll do the other half separately to keep PRs manageable.

Updates #cleanup

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-20 14:50:30 -08:00
Andrew Dunham
3c333f6341 net/portmapper: add logs about obtained mapping(s)
This logs additional information about what mapping(s) are obtained
during the creation process, including whether we return an existing
cached mapping.

Updates #10597

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I9ff25071f064c91691db9ab0b9365ccc5f948d6e
2023-12-20 17:19:08 -05:00
David Crawshaw
f815d66a88 api.md: add docs for setting an IP address
Updates tailscale/corp#16453

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-12-20 12:59:17 -08:00
Andrew Dunham
01286af82b net/interfaces: better handle multiple interfaces in LikelyHomeRouterIP
Currently, we get the "likely home router" gateway IP and then iterate
through all IPs for all interfaces trying to match IPs to determine the
source IP. However, on many platforms we know what interface the gateway
is through, and thus we don't need to iterate through all interfaces
checking IPs. Instead, use the IP address of the associated interface.

This better handles the case where we have multiple interfaces on a
system all connected to the same gateway, and where the first interface
that we visit (as iterated by ForeachInterfaceAddress) isn't also the
default internet route.

Updates #8992

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I8632f577f1136930f4ec60c76376527a19a47d1f
2023-12-20 15:33:58 -05:00
Andrew Lytvynov
7a2eb22e94 ipn: remove use of reflect.MethodByName (#10652)
Using reflect.MethodByName disables some linked deadcode optimizations
and makes our binaries much bigger.
Difference before/after this commit:
```
-rwxr-xr-x  1 awly awly  30M Dec 19 15:28 tailscaled.after*
-rwxr-xr-x  1 awly awly  43M Dec 19 15:27 tailscaled.before*
```

Fixes #10627

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-20 07:12:26 -08:00
Andrew Dunham
09136e5995 net/netutil: add function to check rp_filter value (#5703)
Updates #4432


Change-Id: Ifc332a5747fc1feffdbb87437308cf8ecb21b0b0

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2023-12-20 00:02:37 -05:00
Paul Scott
65f2d32300 api.md: add device.postureIdentity field
Updates tailscale/corp#15445

Signed-off-by: Paul Scott <paul@tailscale.com>
2023-12-19 22:01:20 +00:00
Paul Scott
03f22cd9fa client/tailscale: add Device.PostureIdentity field
New API fields being added in tailscale/corp#15445.

Updates tailscale/corp#15203

Signed-off-by: Paul Scott <paul@tailscale.com>
2023-12-19 22:01:20 +00:00
Nick Khyl
5e3126f510 tool/gocross: make all Windows DLLs build with static libgcc
In this commit, we have updated the build process for our Windows DLLs
to link statically with libgcc, ensuring our Windows DLLs are self-contained.

Updates #10617

Signed-off-by: Nick Khyl <nickk@tailscale.com>
2023-12-19 15:10:28 -06:00
James Tucker
0957258f84 appc,ipn: prevent undesirable route advertisements
Individual route advertisements that are covered by existing routes are
no longer advertised. If an upstream returns 0.0.0.0, 127.x, and other
common unwanted addresses those are also rejected.

Updates #16425
Signed-off-by: James Tucker <james@tailscale.com>
2023-12-19 10:33:25 -08:00
Gavin Greenwalt
865ee25a57 cmd/tailscale/cli: update debug.go (#10644)
redundant run "portmap debugging" word 'debugging'

Signed-off-by: Gavin Greenwalt <gavin@sfstudios.com>
2023-12-19 11:29:52 -05:00
Andrew Dunham
a661287c4b util/cmpx: remove code that's in the stdlib now
The cmpx.Compare function (and associated interface) are now available
in the standard library as cmp.Compare. Remove our version of it and use
the version from the standard library.

Updates #cleanup

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4be3ac63d466c05eb7a0babb25cb0d41816fbd53
2023-12-19 09:18:53 -05:00
Andrew Lytvynov
945cf836ee ipn: apply tailnet-wide default for auto-updates (#10508)
When auto-update setting in local Prefs is unset, apply the tailnet
default value from control. This only happens once, when we apply the
default (or when the user manually overrides it), tailnet default no
longer affects the node.

Updates #16244

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2023-12-18 14:57:03 -08:00
Andrew Dunham
d05a572db4 net/portmapper: handle multiple UPnP discovery responses
Instead of taking the first UPnP response we receive and using that to
create port mappings, store all received UPnP responses, sort and
deduplicate them, and then try all of them to obtain an external
address.

Updates #10602

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I783ccb1834834ee2a9ecbae2b16d801f2354302f
2023-12-18 16:02:46 -05:00
Irbe Krumina
38b4eb9419 cmd/k8s-operator/deploy/chart: document passing multiple proxy tags + log level values (#10624)
Updates #cleanup

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-18 10:28:06 +00:00
Flakes Updater
dc2792aaee go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply+flakes-updater@tailscale.com>
2023-12-15 22:48:28 -08:00
as2643
3fb6ee7fdb tailscale/logtail: redact public ipv6 and ipv4 ip addresses within tailscaled. (#10531)
Updates #15664

Signed-off-by: Anishka Singh <anishkasingh66@gmail.com>
2023-12-15 15:15:49 -08:00
James Tucker
3a635db06e cmd/connector-gen: add helper tool for wide app connector configurations
connector-gen can initially generate connector ACL snippets and
advertise-routes flags for Github and AWS based on their public IP /
domain data.

Updates ENG-2425
Signed-off-by: James Tucker <james@tailscale.com>
2023-12-15 09:29:42 -08:00
James Tucker
706e30d49e disco: correct noun for nacl box type in disco docs
Updates #cleanup

Signed-off-by: James Tucker <james@tailscale.com>
2023-12-14 16:41:53 -08:00
Sonia Appasamy
c6a274611e client/web: use Tailscale IP known by peer node
Throughout the web UI, we present the tailscale addresses for the
self node. In the case of the node being shared out with a user
from another tailnet, the peer viewer may actually know the node
by a different IP than the node knows itself as (Tailscale IPs
can be configured as desired on a tailnet level). This change
includes two fixes:

1. Present the self node's addresses in the frontend as the addresses
   the viewing node knows it as (i.e. the addresses the viewing node
   uses to access the web client).

2. We currently redirect the viewer to the Tailscale IPv4 address if
   viewing it by MagicDNS name, or any other name that maps to the
   Tailscale node. When doing this redirect, which is primarily added
   for DNS rebinding protection, we now check the address the peer
   knows this node as, and redirect to specifically that IP.

Fixes tailscale/corp#16402

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-14 16:36:33 -05:00
Jordan Whited
685b853763 wgengine/magicsock: fix handling of derp.PeerGoneMessage (#10589)
The switch in Conn.runDerpReader() on the derp.ReceivedMessage type
contained cases other than derp.ReceivedPacket that fell through to
writing to c.derpRecvCh, which should only be reached for
derp.ReceivedPacket. This can result in the last/previous
derp.ReceivedPacket to be re-handled, effectively creating a duplicate
packet. If the last derp.ReceivedPacket happens to be a
disco.CallMeMaybe it may result in a disco ping scan towards the
originating peer on the endpoints contained.

The change in this commit moves the channel write on c.derpRecvCh and
subsequent select awaiting the result into the derp.ReceivedMessage
case, preventing it from being reached from any other case. Explicit
continue statements are also added to non-derp.ReceivedPacket cases
where they were missing, in order to signal intent to the reader.

Fixes #10586

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-12-14 12:54:19 -08:00
Andrew Dunham
3ae562366b ipn/ipnlocal: fix usage of slices.Compact
Fixes #10595

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I4e96e9c43b8dedb5f88b03368c01b0e46723e15b
2023-12-14 14:11:21 -05:00
Irbe Krumina
1a08ea5990 cmd/k8s-operator: operator can create subnetrouter (#9505)
* k8s-operator,cmd/k8s-operator,Makefile,scripts,.github/workflows: add Connector kube CRD.

Connector CRD allows users to configure the Tailscale Kubernetes operator
to deploy a subnet router to expose cluster CIDRs or
other CIDRs available from within the cluster
to their tailnet.

Also adds various CRD related machinery to
generate CRD YAML, deep copy implementations etc.

Engineers will now have to run
'make kube-generate-all` after changing kube files
to ensure that all generated files are up to date.

* cmd/k8s-operator,k8s-operator: reconcile Connector resources

Reconcile Connector resources, create/delete subnetrouter resources in response to changes to Connector(s).

Connector reconciler will not be started unless
ENABLE_CONNECTOR env var is set to true.
This means that users who don't want to use the alpha
Connector custom resource don't have to install the Connector
CRD to their cluster.
For users who do want to use it the flow is:
- install the CRD
- install the operator (via Helm chart or using static manifests).
For Helm users set .values.enableConnector to true, for static
manifest users, set ENABLE_CONNECTOR to true in the static manifest.

Updates tailscale/tailscale#502


Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2023-12-14 13:51:59 +00:00
Chris Palmer
b62a3fc895 client/web: keep redirects on-site (#10525)
Ensure we don't create Location: header URLs that have leading //, which is a
schema-less reference to arbitrary 3rd-party sites. That is, //example.com/foo
redirects off-site, while /example.com/foo is an on-site path URL.

Fixes tailscale/corp#16268

Signed-off-by: Chris Palmer <cpalmer@tailscale.com>
2023-12-13 14:28:50 -08:00
Andrew Dunham
727acf96a6 net/netcheck: use DERP frames as a signal for home region liveness
This uses the fact that we've received a frame from a given DERP region
within a certain time as a signal that the region is stil present (and
thus can still be a node's PreferredDERP / home region) even if we don't
get a STUN response from that region during a netcheck.

This should help avoid DERP flaps that occur due to losing STUN probes
while still having a valid and active TCP connection to the DERP server.

RELNOTE=Reduce home DERP flapping when there's still an active connection

Updates #8603

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: If7da6312581e1d434d5c0811697319c621e187a0
2023-12-13 16:33:46 -05:00
Andrew Dunham
bac4890467 net/portmapper: be smarter about selecting a UPnP device
Previously, we would select the first WANIPConnection2 (and related)
client from the root device, without any additional checks. However,
some routers expose multiple UPnP devices in various states, and simply
picking the first available one can result in attempting to perform a
portmap with a device that isn't functional.

Instead, mimic what the miniupnpc code does, and prefer devices that are
(a) reporting as Connected, and (b) have a valid external IP address.
For our use-case, we additionally prefer devices that have an external
IP address that's a public address, to increase the likelihood that we
can obtain a direct connection from peers.

Finally, we split out fetching the root device (getUPnPRootDevice) from
selecting the best service within that root device (selectBestService),
and add some extensive tests for various UPnP server behaviours.

RELNOTE=Improve UPnP portmapping when multiple UPnP services exist

Updates #8364

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I71795cd80be6214dfcef0fe83115a5e3fe4b8753
2023-12-13 16:32:29 -05:00
Sonia Appasamy
971fa8dc56 VERSION.txt: this is v1.57.0
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2023-12-13 15:30:30 -05:00
261 changed files with 10907 additions and 2923 deletions

View File

@@ -47,6 +47,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
# Install a more recent Go that understands modern go.mod content.
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2

View File

@@ -22,17 +22,30 @@ jobs:
- name: Scan source code for known vulnerabilities
run: PATH=$PWD/tool/:$PATH "$(./tool/go env GOPATH)/bin/govulncheck" -test ./...
- uses: ruby/action-slack@v3.2.1
with:
payload: >
{
"attachments": [{
"title": "${{ job.status }}: ${{ github.workflow }}",
"title_link": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks",
"text": "${{ github.repository }}@${{ github.sha }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Post to slack
if: failure() && github.event_name == 'schedule'
uses: slackapi/slack-github-action@v1.24.0
env:
SLACK_BOT_TOKEN: ${{ secrets.GOVULNCHECK_BOT_TOKEN }}
with:
channel-id: 'C05PXRM304B'
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Govulncheck failed in ${{ github.repository }}"
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "View results"
},
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
}
]
}

View File

@@ -2,7 +2,8 @@ name: "Kubernetes manifests"
on:
pull_request:
paths:
- './cmd/k8s-operator/'
- './cmd/k8s-operator/**'
- './k8s-operator/**'
- '.github/workflows/kubemanifests.yaml'
# Cancel workflow run if there is a newer push to the same PR for which it is
@@ -24,7 +25,7 @@ jobs:
./tool/helm lint "tailscale-operator-${VERSION_SHORT}.tgz"
- name: Verify that static manifests are up to date
run: |
./tool/go generate tailscale.com/cmd/k8s-operator
make kube-generate-all
echo
echo
git diff --name-only --exit-code || (echo "Static manifests for Tailscale Kubernetes operator are out of date. Please run 'go generate tailscale.com/cmd/k8s-operator' and commit the diff."; exit 1)
git diff --name-only --exit-code || (echo "Generated files for Tailscale Kubernetes operator are out of date. Please run 'make kube-generate-all' and commit the diff."; exit 1)

View File

@@ -241,9 +241,9 @@ jobs:
goarch: amd64
- goos: openbsd
goarch: amd64
# Plan9
- goos: plan9
goarch: amd64
# Plan9 (disabled until 3p dependencies are fixed)
# - goos: plan9
# goarch: amd64
runs-on: ubuntu-22.04
steps:

View File

@@ -3,6 +3,8 @@ SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
TAGS ?= "latest"
PLATFORM ?= "flyio" ## flyio==linux/amd64. Set to "" to build all platforms.
vet: ## Run go vet
./tool/go vet ./...
@@ -18,7 +20,8 @@ updatedeps: ## Update depaware deps
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
tailscale.com/cmd/derper \
tailscale.com/cmd/stund
depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
@@ -26,7 +29,8 @@ depaware: ## Run depaware checks
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
tailscale.com/cmd/derper \
tailscale.com/cmd/stund
buildwindows: ## Build tailscale CLI for windows/amd64
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
@@ -54,6 +58,21 @@ check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ##
staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
kube-generate-all: kube-generate-deepcopy ## Refresh generated files for Tailscale Kubernetes Operator
./tool/go generate ./cmd/k8s-operator
# Tailscale operator watches Connector custom resources in a Kubernetes cluster
# and caches them locally. Caching is done implicitly by controller-runtime
# library (the middleware used by Tailscale operator to create kube control
# loops). When a Connector resource is GET/LIST-ed from within our control loop,
# the request goes through the cache. To ensure that cache contents don't get
# modified by control loops, controller-runtime deep copies the requested
# object. In order for this to work, Connector must implement deep copy
# functionality so we autogenerate it here.
# https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/cache/internal/cache_reader.go#L86-L89
kube-generate-deepcopy: ## Refresh generated deepcopy functionality for Tailscale kube API types
./scripts/kube-deepcopy.sh
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH}
@@ -71,7 +90,7 @@ publishdevimage: ## Build and publish tailscale image to location specified by $
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@@ -79,7 +98,7 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"

View File

@@ -1 +1 @@
1.55.0
1.59.0

59
api.md
View File

@@ -60,6 +60,8 @@ The Tailscale API does not currently support pagination. All results are returne
- Update tags: [`POST /api/v2/device/{deviceID}/tags`](#update-device-tags)
- **Key**
- Update device key: [`POST /api/v2/device/{deviceID}/key`](#update-device-key)
- **IP Address**
- Set device IPv4 address: [`POST /api/v2/device/{deviceID}/ip`](#set-device-ipv4-address)
**[Tailnet](#tailnet)**
- [**Policy File**](#policy-file)
@@ -277,6 +279,15 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
// tailnet lock is not enabled.
// Learn more about tailnet lock at https://tailscale.com/kb/1226/.
"tailnetLockKey": "",
// postureIdentity contains extra identifiers from the device when the tailnet
// it is connected to has device posture identification collection enabled.
// If the device has not opted-in to posture identification collection, this
// will contain {"disabled": true}.
// Learn more about posture identity at https://tailscale.com/kb/1326/device-identity
"postureIdentity": {
"serialNumbers": ["CP74LFQJXM"]
}
}
```
@@ -328,6 +339,7 @@ Currently, there are two supported options:
- `enabledRoutes`
- `advertisedRoutes`
- `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
- `postureIdentity`
### Request example
@@ -590,7 +602,7 @@ If the tags supplied in the `POST` call do not exist in the tailnet policy file,
}
```
<a href="device-key-post"><a>
<a href="device-key-post"></a>
## Update device key
@@ -644,6 +656,51 @@ curl "https://api.tailscale.com/api/v2/device/11055/key" \
The response is 2xx on success. The response body is currently an empty JSON object.
## Set device IPv4 address
``` http
POST /api/v2/device/{deviceID}/ip
```
Set the Tailscale IPv4 address of the device.
### Parameters
#### `deviceid` (required in URL path)
The ID of the device.
#### `ipv4` (optional in `POST` body)
Provide a new IPv4 address for the device.
When a device is added to a tailnet, its Tailscale IPv4 address is set at random either from the CGNAT range, or a subset of the CGNAT range specified by an [ip pool](https://tailscale.com/kb/1304/ip-pool).
This endpoint can be used to replace the existing IPv4 address with a specific value.
``` jsonc
{
"ipv4": "100.80.0.1"
}
```
This action will break any existing connections to this machine.
You will need to reconnect to this machine using the new IP address.
You may also need to flush your DNS cache.
This returns a 2xx code on success, with an empty JSON object in the response body.
### Request example
``` sh
curl "https://api.tailscale.com/api/v2/device/11055/ip" \
-u "tskey-api-xxxxx:" \
--data-binary '{"ipv4": "100.80.0.1"}'
```
### Response
The response is 2xx on success. The response body is currently an empty JSON object.
# Tailnet
A tailnet is your private network, composed of all the devices on it and their configuration.

View File

@@ -10,6 +10,7 @@
package appc
import (
"context"
"net/netip"
"slices"
"strings"
@@ -20,14 +21,18 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/execqueue"
)
// RouteAdvertiser is an interface that allows the AppConnector to advertise
// newly discovered routes that need to be served through the AppConnector.
type RouteAdvertiser interface {
// AdvertiseRoute adds a new route advertisement if the route is not already
// being advertised.
AdvertiseRoute(netip.Prefix) error
// AdvertiseRoute adds one or more route advertisements skipping any that
// are already advertised.
AdvertiseRoute(...netip.Prefix) error
// UnadvertiseRoute removes any matching route advertisements.
UnadvertiseRoute(...netip.Prefix) error
}
// AppConnector is an implementation of an AppConnector that performs
@@ -45,12 +50,19 @@ type AppConnector struct {
// mu guards the fields that follow
mu sync.Mutex
// domains is a map of lower case domain names with no trailing dot, to a
// list of resolved IP addresses.
domains map[string][]netip.Addr
// controlRoutes is the list of routes that were last supplied by control.
controlRoutes []netip.Prefix
// wildcards is the list of domain strings that match subdomains.
wildcards []string
// queue provides ordering for update operations
queue execqueue.ExecQueue
}
// NewAppConnector creates a new AppConnector.
@@ -61,11 +73,33 @@ func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser) *AppConn
}
}
// UpdateDomains replaces the current set of configured domains with the
// supplied set of domains. Domains must not contain a trailing dot, and should
// be lower case. If the domain contains a leading '*' label it matches all
// subdomains of a domain.
// UpdateDomainsAndRoutes starts an asynchronous update of the configuration
// given the new domains and routes.
func (e *AppConnector) UpdateDomainsAndRoutes(domains []string, routes []netip.Prefix) {
e.queue.Add(func() {
// Add the new routes first.
e.updateRoutes(routes)
e.updateDomains(domains)
})
}
// UpdateDomains asynchronously replaces the current set of configured domains
// with the supplied set of domains. Domains must not contain a trailing dot,
// and should be lower case. If the domain contains a leading '*' label it
// matches all subdomains of a domain.
func (e *AppConnector) UpdateDomains(domains []string) {
e.queue.Add(func() {
e.updateDomains(domains)
})
}
// Wait waits for the currently scheduled asynchronous configuration changes to
// complete.
func (e *AppConnector) Wait(ctx context.Context) {
e.queue.Wait(ctx)
}
func (e *AppConnector) updateDomains(domains []string) {
e.mu.Lock()
defer e.mu.Unlock()
@@ -97,6 +131,46 @@ func (e *AppConnector) UpdateDomains(domains []string) {
e.logf("handling domains: %v and wildcards: %v", xmaps.Keys(e.domains), e.wildcards)
}
// updateRoutes merges the supplied routes into the currently configured routes. The routes supplied
// by control for UpdateRoutes are supplemental to the routes discovered by DNS resolution, but are
// also more often whole ranges. UpdateRoutes will remove any single address routes that are now
// covered by new ranges.
func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
e.mu.Lock()
defer e.mu.Unlock()
// If there was no change since the last update, no work to do.
if slices.Equal(e.controlRoutes, routes) {
return
}
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
return
}
var toRemove []netip.Prefix
nextRoute:
for _, r := range routes {
for _, addr := range e.domains {
for _, a := range addr {
if r.Contains(a) && netip.PrefixFrom(a, a.BitLen()) != r {
pfx := netip.PrefixFrom(a, a.BitLen())
toRemove = append(toRemove, pfx)
continue nextRoute
}
}
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
e.controlRoutes = routes
}
// Domains returns the currently configured domain list.
func (e *AppConnector) Domains() views.Slice[string] {
e.mu.Lock()
@@ -132,6 +206,7 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
return
}
nextAnswer:
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
@@ -206,9 +281,18 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
if slices.Contains(addrs, addr) {
continue
}
// TODO(raggi): check for existing prefixes
for _, route := range e.controlRoutes {
if route.Contains(addr) {
// record the new address associated with the domain for faster matching in subsequent
// requests and for diagnostic records.
e.mu.Lock()
e.domains[domain] = append(addrs, addr)
e.mu.Unlock()
continue nextAnswer
}
}
if err := e.routeAdvertiser.AdvertiseRoute(netip.PrefixFrom(addr, addr.BitLen())); err != nil {
e.logf("failed to advertise route for %v: %v", addr, err)
e.logf("failed to advertise route for %s: %v: %v", domain, addr, err)
continue
}
e.logf("[v2] advertised route for %v: %v", domain, addr)
@@ -217,5 +301,4 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
e.domains[domain] = append(addrs, addr)
e.mu.Unlock()
}
}

View File

@@ -4,6 +4,7 @@
package appc
import (
"context"
"net/netip"
"reflect"
"slices"
@@ -11,12 +12,17 @@ import (
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc/appctest"
"tailscale.com/util/mak"
"tailscale.com/util/must"
)
func TestUpdateDomains(t *testing.T) {
ctx := context.Background()
a := NewAppConnector(t.Logf, nil)
a.UpdateDomains([]string{"example.com"})
a.Wait(ctx)
if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
@@ -24,6 +30,7 @@ func TestUpdateDomains(t *testing.T) {
addr := netip.MustParseAddr("192.0.0.8")
a.domains["example.com"] = append(a.domains["example.com"], addr)
a.UpdateDomains([]string{"example.com"})
a.Wait(ctx)
if got, want := a.domains["example.com"], []netip.Addr{addr}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
@@ -31,15 +38,66 @@ func TestUpdateDomains(t *testing.T) {
// domains are explicitly downcased on set.
a.UpdateDomains([]string{"UP.EXAMPLE.COM"})
a.Wait(ctx)
if got, want := xmaps.Keys(a.domains), []string{"up.example.com"}; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
}
func TestDomainRoutes(t *testing.T) {
rc := &routeCollector{}
func TestUpdateRoutes(t *testing.T) {
ctx := context.Background()
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.UpdateDomains([]string{"example.com"})
a.updateDomains([]string{"*.example.com"})
// This route should be collapsed into the range
a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.0.2.1"))
a.Wait(ctx)
if !slices.Equal(rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}) {
t.Fatalf("got %v, want %v", rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
}
// This route should not be collapsed or removed
a.ObserveDNSResponse(dnsResponse("b.example.com.", "192.0.0.1"))
a.Wait(ctx)
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
a.updateRoutes(routes)
slices.SortFunc(rc.Routes(), prefixCompare)
rc.SetRoutes(slices.Compact(rc.Routes()))
slices.SortFunc(routes, prefixCompare)
// Ensure that the non-matching /32 is preserved, even though it's in the domains table.
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Errorf("added routes: got %v, want %v", rc.Routes(), routes)
}
// Ensure that the contained /32 is removed, replaced by the /24.
wantRemoved := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}
if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) {
t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes())
}
}
func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")})
rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
a.updateRoutes(routes)
if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
t.Fatalf("got %v, want %v", rc.Routes(), routes)
}
}
func TestDomainRoutes(t *testing.T) {
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.updateDomains([]string{"example.com"})
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
want := map[string][]netip.Addr{
@@ -52,51 +110,63 @@ func TestDomainRoutes(t *testing.T) {
}
func TestObserveDNSResponse(t *testing.T) {
rc := &routeCollector{}
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
// a has no domains configured, so it should not advertise any routes
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
if got, want := rc.routes, ([]netip.Prefix)(nil); !slices.Equal(got, want) {
if got, want := rc.Routes(), ([]netip.Prefix)(nil); !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
a.UpdateDomains([]string{"example.com"})
a.updateDomains([]string{"example.com"})
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
if got, want := rc.routes, wantRoutes; !slices.Equal(got, want) {
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
if got, want := rc.routes, wantRoutes; !slices.Equal(got, want) {
if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
// don't re-advertise routes that have already been advertised
a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1"))
if !slices.Equal(rc.routes, wantRoutes) {
t.Errorf("got %v; want %v", rc.routes, wantRoutes)
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
}
// don't advertise addresses that are already in a control provided route
pfx := netip.MustParsePrefix("192.0.2.0/24")
a.updateRoutes([]netip.Prefix{pfx})
wantRoutes = append(wantRoutes, pfx)
a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1"))
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
}
if !slices.Contains(a.domains["example.com"], netip.MustParseAddr("192.0.2.1")) {
t.Errorf("missing %v from %v", "192.0.2.1", a.domains["exmaple.com"])
}
}
func TestWildcardDomains(t *testing.T) {
rc := &routeCollector{}
rc := &appctest.RouteCollector{}
a := NewAppConnector(t.Logf, rc)
a.UpdateDomains([]string{"*.example.com"})
a.updateDomains([]string{"*.example.com"})
a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8"))
if got, want := rc.routes, []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
t.Errorf("routes: got %v; want %v", got, want)
}
if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
t.Errorf("wildcards: got %v; want %v", got, want)
}
a.UpdateDomains([]string{"*.example.com", "example.com"})
a.updateDomains([]string{"*.example.com", "example.com"})
if _, ok := a.domains["foo.example.com"]; !ok {
t.Errorf("expected foo.example.com to be preserved in domains due to wildcard")
}
@@ -105,7 +175,7 @@ func TestWildcardDomains(t *testing.T) {
}
// There was an early regression where the wildcard domain was added repeatedly, this guards against that.
a.UpdateDomains([]string{"*.example.com", "example.com"})
a.updateDomains([]string{"*.example.com", "example.com"})
if len(a.wildcards) != 1 {
t.Errorf("expected only one wildcard domain, got %v", a.wildcards)
}
@@ -148,15 +218,13 @@ func dnsResponse(domain, address string) []byte {
return must.Get(b.Finish())
}
// routeCollector is a test helper that collects the list of routes advertised
type routeCollector struct {
routes []netip.Prefix
func prefixEqual(a, b netip.Prefix) bool {
return a == b
}
// routeCollector implements RouteAdvertiser
var _ RouteAdvertiser = (*routeCollector)(nil)
func (rc *routeCollector) AdvertiseRoute(pfx netip.Prefix) error {
rc.routes = append(rc.routes, pfx)
return nil
func prefixCompare(a, b netip.Prefix) int {
if a.Addr().Compare(b.Addr()) == 0 {
return a.Bits() - b.Bits()
}
return a.Addr().Compare(b.Addr())
}

49
appc/appctest/appctest.go Normal file
View File

@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package appctest
import (
"net/netip"
"slices"
)
// RouteCollector is a test helper that collects the list of routes advertised
type RouteCollector struct {
routes []netip.Prefix
removedRoutes []netip.Prefix
}
func (rc *RouteCollector) AdvertiseRoute(pfx ...netip.Prefix) error {
rc.routes = append(rc.routes, pfx...)
return nil
}
func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error {
routes := rc.routes
rc.routes = rc.routes[:0]
for _, r := range routes {
if !slices.Contains(toRemove, r) {
rc.routes = append(rc.routes, r)
} else {
rc.removedRoutes = append(rc.removedRoutes, r)
}
}
return nil
}
// RemovedRoutes returns the list of routes that were removed.
func (rc *RouteCollector) RemovedRoutes() []netip.Prefix {
return rc.removedRoutes
}
// Routes returns the ordered list of routes that were added, including
// possible duplicates.
func (rc *RouteCollector) Routes() []netip.Prefix {
return rc.routes
}
func (rc *RouteCollector) SetRoutes(routes []netip.Prefix) error {
rc.routes = routes
return nil
}

View File

@@ -32,6 +32,7 @@ PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
case "$TARGET" in
client)
@@ -50,6 +51,7 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/containerboot
;;
operator)
@@ -65,6 +67,7 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/operator
;;
*)

View File

@@ -71,6 +71,17 @@ type Device struct {
AdvertisedRoutes []string `json:"advertisedRoutes"` // Empty for external devices.
ClientConnectivity *ClientConnectivity `json:"clientConnectivity"`
// PostureIdentity contains extra identifiers collected from the device when
// the tailnet has the device posture identification features enabled. If
// Tailscale have attempted to collect this from the device but it has not
// opted in, PostureIdentity will have Disabled=true.
PostureIdentity *DevicePostureIdentity `json:"postureIdentity"`
}
type DevicePostureIdentity struct {
Disabled bool `json:"disabled,omitempty"`
SerialNumbers []string `json:"serialNumbers,omitempty"`
}
// DeviceFieldsOpts determines which fields should be returned in the response.

View File

@@ -102,8 +102,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
return safesocket.Connect(s)
return safesocket.Connect(lc.socket())
}
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.

View File

@@ -8,6 +8,7 @@ import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
@@ -232,3 +233,55 @@ func (s *Server) newSessionID() (string, error) {
}
return "", errors.New("too many collisions generating new session; please refresh page")
}
type peerCapabilities map[capFeature]bool // value is true if the peer can edit the given feature
// canEdit is true if the peerCapabilities grant edit access
// to the given feature.
func (p peerCapabilities) canEdit(feature capFeature) bool {
if p == nil {
return false
}
if p[capFeatureAll] {
return true
}
return p[feature]
}
type capFeature string
const (
// The following values should not be edited.
// New caps can be added, but existing ones should not be changed,
// as these exact values are used by users in tailnet policy files.
capFeatureAll capFeature = "*" // grants peer management of all features
capFeatureFunnel capFeature = "funnel" // grants peer serve/funnel management
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnet capFeature = "subnet" // grants peer subnet routes management
capFeatureExitNode capFeature = "exitnode" // grants peer ability to advertise-as and use exit nodes
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
)
type capRule struct {
CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
}
// toPeerCapabilities parses out the web ui capabilities from the
// given whois response.
func toPeerCapabilities(whois *apitype.WhoIsResponse) (peerCapabilities, error) {
caps := peerCapabilities{}
if whois == nil {
return caps, nil
}
rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
}
for _, c := range rules {
for _, f := range c.CanEdit {
caps[capFeature(strings.ToLower(f))] = true
}
}
return caps, nil
}

View File

@@ -5,6 +5,7 @@ import React from "react"
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
import LoginToggle from "src/components/login-toggle"
import DeviceDetailsView from "src/components/views/device-details-view"
import DisconnectedView from "src/components/views/disconnected-view"
import HomeView from "src/components/views/home-view"
import LoginView from "src/components/views/login-view"
import SSHView from "src/components/views/ssh-view"
@@ -74,9 +75,7 @@ function WebClient({
/>
</FeatureRoute>
<Route path="/disconnected">
<Card className="mt-8">
<EmptyState description="You have been disconnected" />
</Card>
<DisconnectedView />
</Route>
<Route>
<Card className="mt-8">

View File

@@ -95,9 +95,16 @@ function LoginPopoverContent({
const [canConnectOverTS, setCanConnectOverTS] = useState<boolean>(false)
const [isRunningCheck, setIsRunningCheck] = useState<boolean>(false)
// Whether the current page is loaded over HTTPS.
// If it is, then the connectivity check to the management client
// will fail with a mixed-content error.
const isHTTPS = window.location.protocol === "https:"
const checkTSConnection = useCallback(() => {
if (auth.viewerIdentity) {
setCanConnectOverTS(true) // already connected over ts
if (auth.viewerIdentity || isHTTPS) {
// Skip the connectivity check if we either already know we're connected over Tailscale,
// or know the connectivity check will fail because the current page is loaded over HTTPS.
setCanConnectOverTS(true)
return
}
// Otherwise, test connection to the ts IP.
@@ -111,7 +118,7 @@ function LoginPopoverContent({
setIsRunningCheck(false)
})
.catch(() => setIsRunningCheck(false))
}, [auth.viewerIdentity, isRunningCheck, node.IPv4])
}, [auth.viewerIdentity, isRunningCheck, node.IPv4, isHTTPS])
/**
* Checking connection for first time on page load.
@@ -193,6 +200,14 @@ function LoginPopoverContent({
You can see most of this device's details. To make changes,
you need to sign in.
</p>
{isHTTPS && (
// we don't know if the user can connect over TS, so
// provide extra tips in case they have trouble.
<p className="text-gray-500 text-xs font-semibold pt-2">
Make sure you are connected to your tailnet, and that your
policy file allows access.
</p>
)}
<SignInButton auth={auth} onClick={handleSignInClick} />
</>
)}

View File

@@ -226,24 +226,22 @@ function DisconnectDialog() {
return (
<Dialog
className="max-w-md"
title="Disconnect"
trigger={<Button sizeVariant="small">Disconnect</Button>}
title="Log out"
trigger={<Button sizeVariant="small">Log out</Button>}
>
<Dialog.Form
cancelButton
submitButton="Disconnect"
submitButton="Log out"
destructive
onSubmit={() => {
api({ action: "logout" })
setLocation("/disconnected")
}}
>
You are about to disconnect this device from your tailnet. To reconnect,
you will be required to re-authenticate this device.
<p className="mt-4 text-sm text-text-muted">
Your connection to this web interface will end as soon as you click
disconnect.
</p>
Logging out of this device will disconnect it from your tailnet and
expire its node key. You wont be able to use this web interface until
you re-authenticate the device from either the Tailscale app or the
Tailscale command line interface.
</Dialog.Form>
</Dialog>
)

View File

@@ -0,0 +1,21 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
import React from "react"
import { ReactComponent as TailscaleIcon } from "src/assets/icons/tailscale-icon.svg"
/**
* DisconnectedView is rendered after node logout.
*/
export default function DisconnectedView() {
return (
<>
<TailscaleIcon className="mx-auto" />
<p className="mt-12 text-center text-text-muted">
You logged out of this device. To reconnect it you will have to
re-authenticate the device from either the Tailscale app or the
Tailscale command line interface.
</p>
</>
)
}

View File

@@ -15,6 +15,7 @@ import (
"net/http"
"net/netip"
"os"
"path"
"path/filepath"
"slices"
"strings"
@@ -174,6 +175,14 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
newAuthURL: opts.NewAuthURL,
waitAuthURL: opts.WaitAuthURL,
}
if opts.PathPrefix != "" {
// Enforce that path prefix always has a single leading '/'
// so that it is treated as a relative URL path.
// We strip multiple leading '/' to prevent schema-less offsite URLs like "//example.com".
//
// See https://github.com/tailscale/corp/issues/16268.
s.pathPrefix = "/" + strings.TrimLeft(path.Clean(opts.PathPrefix), "/\\")
}
if s.mode == ManageServerMode {
if opts.NewAuthURL == nil {
return nil, fmt.Errorf("must provide a NewAuthURL implementation")
@@ -306,24 +315,63 @@ func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (han
return true
}
var ipv4 string // store the first IPv4 address we see for redirect later
for _, ip := range st.Self.TailscaleIPs {
if ip.Is4() {
if r.Host == fmt.Sprintf("%s:%d", ip, ListenPort) {
return false
}
ipv4 = ip.String()
}
if ip.Is6() && r.Host == fmt.Sprintf("[%s]:%d", ip, ListenPort) {
return false
}
ipv4, ipv6 := s.selfNodeAddresses(r, st)
if r.Host == fmt.Sprintf("%s:%d", ipv4.String(), ListenPort) {
return false // already accessing over Tailscale IP
}
if r.Host == fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort) {
return false // already accessing over Tailscale IP
}
// Not currently accessing via Tailscale IP,
// redirect them.
var preferV6 bool
if ap, err := netip.ParseAddrPort(r.Host); err == nil {
// If Host was already ipv6, keep them on same protocol.
preferV6 = ap.Addr().Is6()
}
newURL := *r.URL
newURL.Host = fmt.Sprintf("%s:%d", ipv4, ListenPort)
if (preferV6 && ipv6.IsValid()) || !ipv4.IsValid() {
newURL.Host = fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort)
} else {
newURL.Host = fmt.Sprintf("%s:%d", ipv4.String(), ListenPort)
}
http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently)
return true
}
// selfNodeAddresses return the Tailscale IPv4 and IPv6 addresses for the self node.
// st is expected to be a status with peers included.
func (s *Server) selfNodeAddresses(r *http.Request, st *ipnstate.Status) (ipv4, ipv6 netip.Addr) {
for _, ip := range st.Self.TailscaleIPs {
if ip.Is4() {
ipv4 = ip
} else if ip.Is6() {
ipv6 = ip
}
if ipv4.IsValid() && ipv6.IsValid() {
break // found both IPs
}
}
if whois, err := s.lc.WhoIs(r.Context(), r.RemoteAddr); err == nil {
// The source peer connecting to this node may know it by a different
// IP than the node knows itself as. Specifically, this may be the case
// if the peer is coming from a different tailnet (sharee node), as IPs
// are specific to each tailnet.
// Here, we check if the source peer knows the node by a different IP,
// and return the peer's version if so.
if knownIPv4 := whois.Node.SelfNodeV4MasqAddrForThisPeer; knownIPv4 != nil {
ipv4 = *knownIPv4
}
if knownIPv6 := whois.Node.SelfNodeV6MasqAddrForThisPeer; knownIPv6 != nil {
ipv6 = *knownIPv6
}
}
return ipv4, ipv6
}
// authorizeRequest reports whether the request from the web client
// is authorized to be completed.
// It reports true if the request is authorized, and false otherwise.
@@ -402,10 +450,11 @@ type authResponse struct {
// viewerIdentity is the Tailscale identity of the source node
// connected to this web client.
type viewerIdentity struct {
LoginName string `json:"loginName"`
NodeName string `json:"nodeName"`
NodeIP string `json:"nodeIP"`
ProfilePicURL string `json:"profilePicUrl,omitempty"`
LoginName string `json:"loginName"`
NodeName string `json:"nodeName"`
NodeIP string `json:"nodeIP"`
ProfilePicURL string `json:"profilePicUrl,omitempty"`
Capabilities peerCapabilities `json:"capabilities"` // features peer is allowed to edit
}
// serverAPIAuth handles requests to the /api/auth endpoint
@@ -416,10 +465,16 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
session, whois, status, sErr := s.getSession(r)
if whois != nil {
caps, err := toPeerCapabilities(whois)
if err != nil {
http.Error(w, sErr.Error(), http.StatusInternalServerError)
return
}
resp.ViewerIdentity = &viewerIdentity{
LoginName: whois.UserProfile.LoginName,
NodeName: whois.Node.Name,
ProfilePicURL: whois.UserProfile.ProfilePicURL,
Capabilities: caps,
}
if addrs := whois.Node.Addresses; len(addrs) > 0 {
resp.ViewerIdentity.NodeIP = addrs[0].Addr().String()
@@ -666,6 +721,10 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
ACLAllowsAnyIncomingTraffic: s.aclsAllowAccess(filterRules),
}
ipv4, ipv6 := s.selfNodeAddresses(r, st)
data.IPv4 = ipv4.String()
data.IPv6 = ipv6.String()
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn && data.URLPrefix == "" {
// X-Ingress-Path is the path prefix in use for Home Assistant
// https://developers.home-assistant.io/docs/add-ons/presentation#ingress
@@ -678,16 +737,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
} else {
data.ClientVersion = cv
}
for _, ip := range st.TailscaleIPs {
if ip.Is4() {
data.IPv4 = ip.String()
} else if ip.Is6() {
data.IPv6 = ip.String()
}
if data.IPv4 != "" && data.IPv6 != "" {
break
}
}
if st.CurrentTailnet != nil {
data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
data.DomainName = st.CurrentTailnet.Name

View File

@@ -450,6 +450,7 @@ func TestServeAuth(t *testing.T) {
NodeName: remoteNode.Node.Name,
NodeIP: remoteIP,
ProfilePicURL: user.ProfilePicURL,
Capabilities: peerCapabilities{},
}
testControlURL := &defaultControlURL
@@ -939,6 +940,78 @@ func TestServeAPIAuthMetricLogging(t *testing.T) {
}
}
// TestPathPrefix tests that the provided path prefix is normalized correctly.
// If a leading '/' is missing, one should be added.
// If multiple leading '/' are present, they should be collapsed to one.
// Additionally verify that this prevents open redirects when enforcing the path prefix.
func TestPathPrefix(t *testing.T) {
tests := []struct {
name string
prefix string
wantPrefix string
wantLocation string
}{
{
name: "no-leading-slash",
prefix: "javascript:alert(1)",
wantPrefix: "/javascript:alert(1)",
wantLocation: "/javascript:alert(1)/",
},
{
name: "2-slashes",
prefix: "//evil.example.com/goat",
// We must also get the trailing slash added:
wantPrefix: "/evil.example.com/goat",
wantLocation: "/evil.example.com/goat/",
},
{
name: "absolute-url",
prefix: "http://evil.example.com",
// We must also get the trailing slash added:
wantPrefix: "/http:/evil.example.com",
wantLocation: "/http:/evil.example.com/",
},
{
name: "double-dot",
prefix: "/../.././etc/passwd",
// We must also get the trailing slash added:
wantPrefix: "/etc/passwd",
wantLocation: "/etc/passwd/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
options := ServerOpts{
Mode: LoginServerMode,
PathPrefix: tt.prefix,
CGIMode: true,
}
s, err := NewServer(options)
if err != nil {
t.Error(err)
}
// verify provided prefix was normalized correctly
if s.pathPrefix != tt.wantPrefix {
t.Errorf("prefix was not normalized correctly; want=%q, got=%q", tt.wantPrefix, s.pathPrefix)
}
s.logf = t.Logf
r := httptest.NewRequest(httpm.GET, "http://localhost/", nil)
w := httptest.NewRecorder()
s.ServeHTTP(w, r)
res := w.Result()
defer res.Body.Close()
location := w.Header().Get("Location")
if location != tt.wantLocation {
t.Errorf("request got wrong location; want=%q, got=%q", tt.wantLocation, location)
}
})
}
}
func TestRequireTailscaleIP(t *testing.T) {
self := &ipnstate.PeerStatus{
TailscaleIPs: []netip.Addr{
@@ -1007,7 +1080,7 @@ func TestRequireTailscaleIP(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.target, func(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
s.logf = t.Logf
r := httptest.NewRequest(httpm.GET, tt.target, nil)
w := httptest.NewRecorder()
@@ -1025,6 +1098,163 @@ func TestRequireTailscaleIP(t *testing.T) {
}
}
func TestPeerCapabilities(t *testing.T) {
// Testing web.toPeerCapabilities
toPeerCapsTests := []struct {
name string
whois *apitype.WhoIsResponse
wantCaps peerCapabilities
}{
{
name: "empty-whois",
whois: nil,
wantCaps: peerCapabilities{},
},
{
name: "no-webui-caps",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{},
},
},
wantCaps: peerCapabilities{},
},
{
name: "one-webui-cap",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
},
},
{
name: "multiple-webui-cap",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"subnet\",\"exitnode\",\"*\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAll: true,
},
},
{
name: "case=insensitive-caps",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"SSH\",\"sUBnet\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
},
},
{
name: "random-canEdit-contents-dont-error",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"unknown-feature\"]}",
},
},
},
wantCaps: peerCapabilities{
"unknown-feature": true,
},
},
{
name: "no-canEdit-section",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canDoSomething\":[\"*\"]}",
},
},
},
wantCaps: peerCapabilities{},
},
}
for _, tt := range toPeerCapsTests {
t.Run("toPeerCapabilities-"+tt.name, func(t *testing.T) {
got, err := toPeerCapabilities(tt.whois)
if err != nil {
t.Fatalf("unexpected: %v", err)
}
if diff := cmp.Diff(got, tt.wantCaps); diff != "" {
t.Errorf("wrong caps; (-got+want):%v", diff)
}
})
}
// Testing web.peerCapabilities.canEdit
canEditTests := []struct {
name string
caps peerCapabilities
wantCanEdit map[capFeature]bool
}{
{
name: "empty-caps",
caps: nil,
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: false,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: false,
},
},
{
name: "some-caps",
caps: peerCapabilities{capFeatureSSH: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: true,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: true,
},
},
{
name: "wildcard-in-caps",
caps: peerCapabilities{capFeatureAll: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: true,
capFeatureFunnel: true,
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAccount: true,
},
},
}
for _, tt := range canEditTests {
t.Run("canEdit-"+tt.name, func(t *testing.T) {
for f, want := range tt.wantCanEdit {
if got := tt.caps.canEdit(f); got != want {
t.Errorf("wrong canEdit(%s); got=%v, want=%v", f, got, want)
}
}
})
}
}
var (
defaultControlURL = "https://controlplane.tailscale.com"
testAuthPath = "/a/12345"

View File

@@ -0,0 +1,15 @@
# connector-gen
Generate Tailscale app connector configuration details from third party data.
Tailscale app connectors are used to dynamically route traffic for domain names
via specific nodes on a tailnet. For larger upstream domains this may involve a
large number of domains or routes, and fully dynamic discovery may be slower or
involve more manual labor than ideal. This can be accelerated by
pre-configuration of the associated routes, based on data provided by the
target providers, which can be used to set precise `autoApprovers` routes, and
also to pre-populate the subnet routes via `--advertise-routes` avoiding
frequent routing reconfiguration that may otherwise occur while routes are
first being discovered and advertised by the connectors.

View File

@@ -0,0 +1,22 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"fmt"
"strings"
"go4.org/netipx"
)
func advertiseRoutes(set *netipx.IPSet) {
fmt.Println()
prefixes := set.Prefixes()
pfxs := make([]string, 0, len(prefixes))
for _, pfx := range prefixes {
pfxs = append(pfxs, pfx.String())
}
fmt.Printf("--advertise-routes=%s", strings.Join(pfxs, ","))
fmt.Println()
}

68
cmd/connector-gen/aws.go Normal file
View File

@@ -0,0 +1,68 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/netip"
"go4.org/netipx"
)
// See https://docs.aws.amazon.com/vpc/latest/userguide/aws-ip-ranges.html
type AWSMeta struct {
SyncToken string `json:"syncToken"`
CreateDate string `json:"createDate"`
Prefixes []struct {
IPPrefix string `json:"ip_prefix"`
Region string `json:"region"`
Service string `json:"service"`
NetworkBorderGroup string `json:"network_border_group"`
} `json:"prefixes"`
Ipv6Prefixes []struct {
Ipv6Prefix string `json:"ipv6_prefix"`
Region string `json:"region"`
Service string `json:"service"`
NetworkBorderGroup string `json:"network_border_group"`
} `json:"ipv6_prefixes"`
}
func aws() {
r, err := http.Get("https://ip-ranges.amazonaws.com/ip-ranges.json")
if err != nil {
log.Fatal(err)
}
defer r.Body.Close()
var aws AWSMeta
if err := json.NewDecoder(r.Body).Decode(&aws); err != nil {
log.Fatal(err)
}
var ips netipx.IPSetBuilder
for _, prefix := range aws.Prefixes {
ips.AddPrefix(netip.MustParsePrefix(prefix.IPPrefix))
}
for _, prefix := range aws.Ipv6Prefixes {
ips.AddPrefix(netip.MustParsePrefix(prefix.Ipv6Prefix))
}
set, err := ips.IPSet()
if err != nil {
log.Fatal(err)
}
fmt.Println(`"routes": [`)
for _, pfx := range set.Prefixes() {
fmt.Printf(`"%s": ["tag:connector"],%s`, pfx.String(), "\n")
}
fmt.Println(`]`)
advertiseRoutes(set)
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// connector-gen is a tool to generate app connector configuration and flags from service provider address data.
package main
import (
"fmt"
"os"
)
func help() {
fmt.Fprintf(os.Stderr, "Usage: %s [help|github|aws] [subcommand-arguments]\n", os.Args[0])
}
func main() {
if len(os.Args) < 2 {
help()
os.Exit(128)
}
switch os.Args[1] {
case "help", "-h", "--help":
help()
os.Exit(0)
case "github":
github()
case "aws":
aws()
default:
help()
os.Exit(128)
}
}

116
cmd/connector-gen/github.go Normal file
View File

@@ -0,0 +1,116 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/netip"
"slices"
"strings"
"go4.org/netipx"
)
// See https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-githubs-ip-addresses
type GithubMeta struct {
VerifiablePasswordAuthentication bool `json:"verifiable_password_authentication"`
SSHKeyFingerprints struct {
Sha256Ecdsa string `json:"SHA256_ECDSA"`
Sha256Ed25519 string `json:"SHA256_ED25519"`
Sha256Rsa string `json:"SHA256_RSA"`
} `json:"ssh_key_fingerprints"`
SSHKeys []string `json:"ssh_keys"`
Hooks []string `json:"hooks"`
Web []string `json:"web"`
API []string `json:"api"`
Git []string `json:"git"`
GithubEnterpriseImporter []string `json:"github_enterprise_importer"`
Packages []string `json:"packages"`
Pages []string `json:"pages"`
Importer []string `json:"importer"`
Actions []string `json:"actions"`
Dependabot []string `json:"dependabot"`
Domains struct {
Website []string `json:"website"`
Codespaces []string `json:"codespaces"`
Copilot []string `json:"copilot"`
Packages []string `json:"packages"`
} `json:"domains"`
}
func github() {
r, err := http.Get("https://api.github.com/meta")
if err != nil {
log.Fatal(err)
}
var ghm GithubMeta
if err := json.NewDecoder(r.Body).Decode(&ghm); err != nil {
log.Fatal(err)
}
r.Body.Close()
var ips netipx.IPSetBuilder
var lists []string
lists = append(lists, ghm.Hooks...)
lists = append(lists, ghm.Web...)
lists = append(lists, ghm.API...)
lists = append(lists, ghm.Git...)
lists = append(lists, ghm.GithubEnterpriseImporter...)
lists = append(lists, ghm.Packages...)
lists = append(lists, ghm.Pages...)
lists = append(lists, ghm.Importer...)
lists = append(lists, ghm.Actions...)
lists = append(lists, ghm.Dependabot...)
for _, s := range lists {
ips.AddPrefix(netip.MustParsePrefix(s))
}
set, err := ips.IPSet()
if err != nil {
log.Fatal(err)
}
fmt.Println(`"routes": [`)
for _, pfx := range set.Prefixes() {
fmt.Printf(`"%s": ["tag:connector"],%s`, pfx.String(), "\n")
}
fmt.Println(`]`)
fmt.Println()
var domains []string
domains = append(domains, ghm.Domains.Website...)
domains = append(domains, ghm.Domains.Codespaces...)
domains = append(domains, ghm.Domains.Copilot...)
domains = append(domains, ghm.Domains.Packages...)
slices.Sort(domains)
domains = slices.Compact(domains)
var bareDomains []string
for _, domain := range domains {
trimmed := strings.TrimPrefix(domain, "*.")
if trimmed != domain {
bareDomains = append(bareDomains, trimmed)
}
}
domains = append(domains, bareDomains...)
slices.Sort(domains)
domains = slices.Compact(domains)
fmt.Println(`"domains": [`)
for _, domain := range domains {
fmt.Printf(`"%s",%s`, domain, "\n")
}
fmt.Println(`]`)
advertiseRoutes(set)
}

View File

@@ -13,7 +13,10 @@
//
// - TS_AUTHKEY: the authkey to use for login.
// - TS_HOSTNAME: the hostname to request for the node.
// - TS_ROUTES: subnet routes to advertise. To accept routes, use TS_EXTRA_ARGS to pass in --accept-routes.
// - TS_ROUTES: subnet routes to advertise. Explicitly setting it to an empty
// value will cause containerboot to stop acting as a subnet router for any
// previously advertised routes. To accept routes, use TS_EXTRA_ARGS to pass
// in --accept-routes.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination.
// - TS_TAILNET_TARGET_IP: proxy all incoming non-Tailscale traffic to the given
@@ -45,6 +48,13 @@
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
// It cannot be used in conjunction with TS_DEST_IP. The file is watched for changes,
// and will be re-applied when it changes.
// - EXPERIMENTAL_TS_CONFIGFILE_PATH: if specified, a path to tailscaled
// config. If this is set, TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY,
// TS_ROUTES, TS_ACCEPT_DNS env vars must not be set. If this is set,
// containerboot only runs `tailscaled --config <path-to-this-configfile>`
// and not `tailscale up` or `tailscale set`.
// The config file contents are currently read once on container start.
// NB: This env var is currently experimental and the logic will likely change!
//
// When running on Kubernetes, containerboot defaults to storing state in the
// "tailscale" kube secret. To store state on local disk instead, set
@@ -80,6 +90,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/conffile"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/ptr"
@@ -99,46 +110,37 @@ func main() {
tailscale.I_Acknowledge_This_API_Is_Unstable = true
cfg := &settings{
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnv("TS_ROUTES", ""),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
ProxyTo: defaultEnv("TS_DEST_IP", ""),
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
UserspaceMode: defaultBool("TS_USERSPACE", true),
StateDir: defaultEnv("TS_STATE_DIR", ""),
AcceptDNS: defaultBool("TS_ACCEPT_DNS", false),
KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnvStringPointer("TS_ROUTES"),
ServeConfigPath: defaultEnv("TS_SERVE_CONFIG", ""),
ProxyTo: defaultEnv("TS_DEST_IP", ""),
TailnetTargetIP: defaultEnv("TS_TAILNET_TARGET_IP", ""),
TailnetTargetFQDN: defaultEnv("TS_TAILNET_TARGET_FQDN", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
UserspaceMode: defaultBool("TS_USERSPACE", true),
StateDir: defaultEnv("TS_STATE_DIR", ""),
AcceptDNS: defaultEnvBoolPointer("TS_ACCEPT_DNS"),
WebUI: defaultEnvBoolPointer("TS_WEBUI"),
KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
TailscaledConfigFilePath: defaultEnv("EXPERIMENTAL_TS_CONFIGFILE_PATH", ""),
}
if cfg.ProxyTo != "" && cfg.UserspaceMode {
log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE")
}
if cfg.TailnetTargetIP != "" && cfg.UserspaceMode {
log.Fatal("TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE")
}
if cfg.TailnetTargetFQDN != "" && cfg.UserspaceMode {
log.Fatal("TS_TAILNET_TARGET_FQDN is not supported with TS_USERSPACE")
}
if cfg.TailnetTargetFQDN != "" && cfg.TailnetTargetIP != "" {
log.Fatal("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
if err := cfg.validate(); err != nil {
log.Fatalf("invalid configuration: %v", err)
}
if !cfg.UserspaceMode {
if err := ensureTunFile(cfg.Root); err != nil {
log.Fatalf("Unable to create tuntap device file: %v", err)
}
if cfg.ProxyTo != "" || cfg.Routes != "" || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" {
if cfg.ProxyTo != "" || cfg.Routes != nil || cfg.TailnetTargetIP != "" || cfg.TailnetTargetFQDN != "" {
if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.TailnetTargetIP, cfg.TailnetTargetFQDN, cfg.Routes); err != nil {
log.Printf("Failed to enable IP forwarding: %v", err)
log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.")
@@ -168,7 +170,7 @@ func main() {
}
cfg.KubernetesCanPatch = canPatch
if cfg.AuthKey == "" {
if cfg.AuthKey == "" && !isOneStepConfig(cfg) {
key, err := findKeyInKubeSecret(bootCtx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Getting authkey from kube secret: %v", err)
@@ -250,7 +252,7 @@ func main() {
return nil
}
if !cfg.AuthOnce {
if isTwoStepConfigAlwaysAuth(cfg) {
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
@@ -266,6 +268,13 @@ authLoop:
if n.State != nil {
switch *n.State {
case ipn.NeedsLogin:
if isOneStepConfig(cfg) {
// This could happen if this is the
// first time tailscaled was run for
// this device and the auth key was not
// passed via the configfile.
log.Fatalf("invalid state: tailscaled daemon started with a config file, but tailscale is not logged in: ensure you pass a valid auth key in the config file.")
}
if err := authTailscale(); err != nil {
log.Fatalf("failed to auth tailscale: %v", err)
}
@@ -290,7 +299,7 @@ authLoop:
ctx, cancel := contextWithExitSignalWatch()
defer cancel()
if cfg.AuthOnce {
if isTwoStepConfigAuthOnce(cfg) {
// Now that we are authenticated, we can set/reset any of the
// settings that we need to.
if err := tailscaleSet(ctx, cfg); err != nil {
@@ -306,7 +315,7 @@ authLoop:
}
}
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && isTwoStepConfigAuthOnce(cfg) {
// We were told to only auth once, so any secret-bound
// authkey is no longer needed. We don't strictly need to
// wipe it, but it's good hygiene.
@@ -631,6 +640,9 @@ func tailscaledArgs(cfg *settings) []string {
if cfg.HTTPProxyAddr != "" {
args = append(args, "--outbound-http-proxy-listen="+cfg.HTTPProxyAddr)
}
if cfg.TailscaledConfigFilePath != "" {
args = append(args, "--config="+cfg.TailscaledConfigFilePath)
}
if cfg.DaemonExtraArgs != "" {
args = append(args, strings.Fields(cfg.DaemonExtraArgs)...)
}
@@ -641,7 +653,7 @@ func tailscaledArgs(cfg *settings) []string {
// if TS_AUTH_ONCE is set, only the first time containerboot starts.
func tailscaleUp(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "up"}
if cfg.AcceptDNS {
if cfg.AcceptDNS != nil && *cfg.AcceptDNS {
args = append(args, "--accept-dns=true")
} else {
args = append(args, "--accept-dns=false")
@@ -649,8 +661,12 @@ func tailscaleUp(ctx context.Context, cfg *settings) error {
if cfg.AuthKey != "" {
args = append(args, "--authkey="+cfg.AuthKey)
}
if cfg.Routes != "" {
args = append(args, "--advertise-routes="+cfg.Routes)
// --advertise-routes can be passed an empty string to configure a
// device (that might have previously advertised subnet routes) to not
// advertise any routes. Respect an empty string passed by a user and
// use it to explicitly unset the routes.
if cfg.Routes != nil {
args = append(args, "--advertise-routes="+*cfg.Routes)
}
if cfg.Hostname != "" {
args = append(args, "--hostname="+cfg.Hostname)
@@ -673,17 +689,26 @@ func tailscaleUp(ctx context.Context, cfg *settings) error {
// node is in Running state and only if TS_AUTH_ONCE is set.
func tailscaleSet(ctx context.Context, cfg *settings) error {
args := []string{"--socket=" + cfg.Socket, "set"}
if cfg.AcceptDNS {
if cfg.AcceptDNS != nil && *cfg.AcceptDNS {
args = append(args, "--accept-dns=true")
} else {
args = append(args, "--accept-dns=false")
}
if cfg.Routes != "" {
args = append(args, "--advertise-routes="+cfg.Routes)
// --advertise-routes can be passed an empty string to configure a
// device (that might have previously advertised subnet routes) to not
// advertise any routes. Respect an empty string passed by a user and
// use it to explicitly unset the routes.
if cfg.Routes != nil {
args = append(args, "--advertise-routes="+*cfg.Routes)
}
if cfg.Hostname != "" {
args = append(args, "--hostname="+cfg.Hostname)
}
if cfg.WebUI != nil && *cfg.WebUI {
args = append(args, "--webclient=true")
} else {
args = append(args, "--webclient=false")
}
log.Printf("Running 'tailscale set'")
cmd := exec.CommandContext(ctx, "tailscale", args...)
cmd.Stdout = os.Stdout
@@ -714,7 +739,7 @@ func ensureTunFile(root string) error {
}
// ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTargetFQDN, routes string) error {
func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTargetFQDN string, routes *string) error {
var (
v4Forwarding, v6Forwarding bool
)
@@ -745,8 +770,8 @@ func ensureIPForwarding(root, clusterProxyTarget, tailnetTargetiP, tailnetTarget
if tailnetTargetFQDN != "" {
v4Forwarding = true
}
if routes != "" {
for _, route := range strings.Split(routes, ",") {
if routes != nil && *routes != "" {
for _, route := range strings.Split(*routes, ",") {
cidr, err := netip.ParsePrefix(route)
if err != nil {
return fmt.Errorf("invalid subnet route: %v", err)
@@ -850,7 +875,7 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
type settings struct {
AuthKey string
Hostname string
Routes string
Routes *string
// ProxyTo is the destination IP to which all incoming
// Tailscale traffic should be proxied. If empty, no proxying
// is done. This is typically a locally reachable IP.
@@ -862,21 +887,47 @@ type settings struct {
// TailnetTargetFQDN is an MagicDNS name to which all incoming
// non-Tailscale traffic should be proxied. This must be a full Tailnet
// node FQDN.
TailnetTargetFQDN string
ServeConfigPath string
DaemonExtraArgs string
ExtraArgs string
InKubernetes bool
UserspaceMode bool
StateDir string
AcceptDNS bool
KubeSecret string
SOCKSProxyAddr string
HTTPProxyAddr string
Socket string
AuthOnce bool
Root string
KubernetesCanPatch bool
TailnetTargetFQDN string
ServeConfigPath string
DaemonExtraArgs string
ExtraArgs string
InKubernetes bool
UserspaceMode bool
StateDir string
AcceptDNS *bool
WebUI *bool
KubeSecret string
SOCKSProxyAddr string
HTTPProxyAddr string
Socket string
AuthOnce bool
Root string
KubernetesCanPatch bool
TailscaledConfigFilePath string
}
func (s *settings) validate() error {
if s.TailscaledConfigFilePath != "" {
if _, err := conffile.Load(s.TailscaledConfigFilePath); err != nil {
return fmt.Errorf("error validating tailscaled configfile contents: %w", err)
}
}
if s.ProxyTo != "" && s.UserspaceMode {
return errors.New("TS_DEST_IP is not supported with TS_USERSPACE")
}
if s.TailnetTargetIP != "" && s.UserspaceMode {
return errors.New("TS_TAILNET_TARGET_IP is not supported with TS_USERSPACE")
}
if s.TailnetTargetFQDN != "" && s.UserspaceMode {
return errors.New("TS_TAILNET_TARGET_FQDN is not supported with TS_USERSPACE")
}
if s.TailnetTargetFQDN != "" && s.TailnetTargetIP != "" {
return errors.New("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
}
if s.TailscaledConfigFilePath != "" && (s.AcceptDNS != nil || s.AuthKey != "" || s.Routes != nil || s.ExtraArgs != "" || s.Hostname != "") {
return errors.New("EXPERIMENTAL_TS_CONFIGFILE_PATH cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
}
return nil
}
// defaultEnv returns the value of the given envvar name, or defVal if
@@ -888,6 +939,28 @@ func defaultEnv(name, defVal string) string {
return defVal
}
// defaultEnvStringPointer returns a pointer to the given envvar value if set, else
// returns nil. This is useful in cases where we need to distinguish between a
// variable being set to empty string vs unset.
func defaultEnvStringPointer(name string) *string {
if v, ok := os.LookupEnv(name); ok {
return &v
}
return nil
}
// defaultEnvBoolPointer returns a pointer to the given envvar value if set, else
// returns nil. This is useful in cases where we need to distinguish between a
// variable being explicitly set to false vs unset.
func defaultEnvBoolPointer(name string) *bool {
v := os.Getenv(name)
ret, err := strconv.ParseBool(v)
if err != nil {
return nil
}
return &ret
}
func defaultEnvs(names []string, defVal string) string {
for _, name := range names {
if v, ok := os.LookupEnv(name); ok {
@@ -929,3 +1002,27 @@ func contextWithExitSignalWatch() (context.Context, func()) {
}
return ctx, f
}
// isTwoStepConfigAuthOnce returns true if the Tailscale node should be configured
// in two steps and login should only happen once.
// Step 1: run 'tailscaled'
// Step 2):
// A) if this is the first time starting this node run 'tailscale up --authkey <authkey> <config opts>'
// B) if this is not the first time starting this node run 'tailscale set <config opts>'.
func isTwoStepConfigAuthOnce(cfg *settings) bool {
return cfg.AuthOnce && cfg.TailscaledConfigFilePath == ""
}
// isTwoStepConfigAlwaysAuth returns true if the Tailscale node should be configured
// in two steps and we should log in every time it starts.
// Step 1: run 'tailscaled'
// Step 2): run 'tailscale up --authkey <authkey> <config opts>'
func isTwoStepConfigAlwaysAuth(cfg *settings) bool {
return !cfg.AuthOnce && cfg.TailscaledConfigFilePath == ""
}
// isOneStepConfig returns true if the Tailscale node should always be ran and
// configured in a single step by running 'tailscaled <config opts>'
func isOneStepConfig(cfg *settings) bool {
return cfg.TailscaledConfigFilePath != ""
}

View File

@@ -52,6 +52,12 @@ func TestContainerBoot(t *testing.T) {
}
defer kube.Close()
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: func(s string) *string { return &s }("foo"), Version: "alpha0"}
tailscaledConfBytes, err := json.Marshal(tailscaledConf)
if err != nil {
t.Fatalf("error unmarshaling tailscaled config: %v", err)
}
dirs := []string{
"var/lib",
"usr/bin",
@@ -59,6 +65,7 @@ func TestContainerBoot(t *testing.T) {
"dev/net",
"proc/sys/net/ipv4",
"proc/sys/net/ipv6/conf/all",
"etc",
}
for _, path := range dirs {
if err := os.MkdirAll(filepath.Join(d, path), 0700); err != nil {
@@ -73,6 +80,7 @@ func TestContainerBoot(t *testing.T) {
"dev/net/tun": []byte(""),
"proc/sys/net/ipv4/ip_forward": []byte("0"),
"proc/sys/net/ipv6/conf/all/forwarding": []byte("0"),
"etc/tailscaled": tailscaledConfBytes,
}
resetFiles := func() {
for path, content := range files {
@@ -218,6 +226,28 @@ func TestContainerBoot(t *testing.T) {
},
},
},
{
Name: "empty routes",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_ROUTES": "",
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key --advertise-routes=",
},
},
{
Notify: runningNotify,
WantFiles: map[string]string{
"proc/sys/net/ipv4/ip_forward": "0",
"proc/sys/net/ipv6/conf/all/forwarding": "0",
},
},
},
},
{
Name: "routes_kernel_ipv4",
Env: map[string]string{
@@ -288,7 +318,7 @@ func TestContainerBoot(t *testing.T) {
},
},
{
Name: "ingres proxy",
Name: "ingress proxy",
Env: map[string]string{
"TS_AUTHKEY": "tskey-key",
"TS_DEST_IP": "1.2.3.4",
@@ -607,6 +637,21 @@ func TestContainerBoot(t *testing.T) {
},
},
},
{
Name: "experimental tailscaled configfile",
Env: map[string]string{
"EXPERIMENTAL_TS_CONFIGFILE_PATH": filepath.Join(d, "etc/tailscaled"),
},
Phases: []phase{
{
WantCmds: []string{
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --config=/etc/tailscaled",
},
}, {
Notify: runningNotify,
},
},
},
}
for _, test := range tests {

View File

@@ -11,7 +11,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
@@ -23,8 +22,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
@@ -51,8 +48,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
google.golang.org/protobuf/encoding/prototext from github.com/prometheus/common/expfmt+
google.golang.org/protobuf/encoding/protowire from google.golang.org/protobuf/encoding/protodelim+
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
@@ -71,16 +69,15 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/proto from github.com/prometheus/client_golang/prometheus+
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/prometheus/client_model/go+
google.golang.org/protobuf/reflect/protoregistry from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/runtime/protoiface from google.golang.org/protobuf/internal/impl+
google.golang.org/protobuf/runtime/protoimpl from github.com/prometheus/client_model/go+
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/util from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
tailscale.com from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/cmd/derper+
@@ -105,7 +102,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
tailscale.com/net/stun from tailscale.com/net/stunserver
tailscale.com/net/stunserver from tailscale.com/cmd/derper
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -141,6 +139,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale
@@ -152,6 +151,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/syspolicy from tailscale.com/ipn
tailscale.com/util/vizerror from tailscale.com/tsweb+
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
@@ -193,7 +193,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/time/rate from tailscale.com/cmd/derper+
bufio from compress/flate+
bytes from bufio+
cmp from slices
cmp from slices+
compress/flate from compress/gzip+
compress/gzip from internal/profile+
container/list from crypto/tls+
@@ -231,7 +231,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
encoding/pem from crypto/tls+
errors from bufio+
expvar from tailscale.com/cmd/derper+
flag from tailscale.com/cmd/derper
flag from tailscale.com/cmd/derper+
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
@@ -262,6 +262,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/url from crypto/x509+
os from crypto/rand+
os/exec from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
os/signal from tailscale.com/cmd/derper
W os/user from tailscale.com/util/winutil
path from golang.org/x/crypto/acme/autocert+
path/filepath from crypto/x509+
@@ -271,7 +272,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
runtime/debug from golang.org/x/crypto/acme+
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
runtime/trace from net/http/pprof+
slices from tailscale.com/ipn/ipnstate+
sort from compress/flate+
strconv from compress/flate+
@@ -279,6 +280,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
testing from tailscale.com/util/syspolicy
text/tabwriter from runtime/pprof
time from compress/gzip+
unicode from bytes+

View File

@@ -17,11 +17,12 @@ import (
"math"
"net"
"net/http"
"net/netip"
"os"
"os/signal"
"path/filepath"
"regexp"
"strings"
"syscall"
"time"
"go4.org/mem"
@@ -30,7 +31,7 @@ import (
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/metrics"
"tailscale.com/net/stun"
"tailscale.com/net/stunserver"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/util/cmpx"
@@ -59,25 +60,11 @@ var (
)
var (
stats = new(metrics.Set)
stunDisposition = &metrics.LabelMap{Label: "disposition"}
stunAddrFamily = &metrics.LabelMap{Label: "family"}
tlsRequestVersion = &metrics.LabelMap{Label: "version"}
tlsActiveVersion = &metrics.LabelMap{Label: "version"}
stunReadError = stunDisposition.Get("read_error")
stunNotSTUN = stunDisposition.Get("not_stun")
stunWriteError = stunDisposition.Get("write_error")
stunSuccess = stunDisposition.Get("success")
stunIPv4 = stunAddrFamily.Get("ipv4")
stunIPv6 = stunAddrFamily.Get("ipv6")
)
func init() {
stats.Set("counter_requests", stunDisposition)
stats.Set("counter_addrfamily", stunAddrFamily)
expvar.Publish("stun", stats)
expvar.Publish("derper_tls_request_version", tlsRequestVersion)
expvar.Publish("gauge_derper_tls_active_version", tlsActiveVersion)
}
@@ -135,6 +122,9 @@ func writeNewConfig() config {
func main() {
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
if *dev {
*addr = ":3340" // above the keys DERP
log.Printf("Running in dev mode.")
@@ -146,6 +136,11 @@ func main() {
log.Fatalf("invalid server address: %v", err)
}
if *runSTUN {
ss := stunserver.New(ctx)
go ss.ListenAndServe(net.JoinHostPort(listenHost, fmt.Sprint(*stunPort)))
}
cfg := loadConfig()
serveTLS := tsweb.IsProd443(*addr) || *certMode == "manual"
@@ -221,10 +216,6 @@ func main() {
}))
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
if *runSTUN {
go serveSTUN(listenHost, *stunPort)
}
quietLogger := log.New(logFilter{}, "", 0)
httpsrv := &http.Server{
Addr: *addr,
@@ -241,6 +232,10 @@ func main() {
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
}
go func() {
<-ctx.Done()
httpsrv.Shutdown(ctx)
}()
if serveTLS {
log.Printf("derper: serving on %s with TLS", *addr)
@@ -351,59 +346,6 @@ func probeHandler(w http.ResponseWriter, r *http.Request) {
}
}
func serveSTUN(host string, port int) {
pc, err := net.ListenPacket("udp", net.JoinHostPort(host, fmt.Sprint(port)))
if err != nil {
log.Fatalf("failed to open STUN listener: %v", err)
}
log.Printf("running STUN server on %v", pc.LocalAddr())
serverSTUNListener(context.Background(), pc.(*net.UDPConn))
}
func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
var buf [64 << 10]byte
var (
n int
ua *net.UDPAddr
err error
)
for {
n, ua, err = pc.ReadFromUDP(buf[:])
if err != nil {
if ctx.Err() != nil {
return
}
log.Printf("STUN ReadFrom: %v", err)
time.Sleep(time.Second)
stunReadError.Add(1)
continue
}
pkt := buf[:n]
if !stun.Is(pkt) {
stunNotSTUN.Add(1)
continue
}
txid, err := stun.ParseBindingRequest(pkt)
if err != nil {
stunNotSTUN.Add(1)
continue
}
if ua.IP.To4() != nil {
stunIPv4.Add(1)
} else {
stunIPv6.Add(1)
}
addr, _ := netip.AddrFromSlice(ua.IP)
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port)))
_, err = pc.WriteTo(res, ua)
if err != nil {
stunWriteError.Add(1)
} else {
stunSuccess.Add(1)
}
}
}
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
func prodAutocertHostPolicy(_ context.Context, host string) error {

View File

@@ -5,13 +5,11 @@ package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"tailscale.com/net/stun"
"tailscale.com/tstest/deptest"
)
@@ -39,38 +37,6 @@ func TestProdAutocertHostPolicy(t *testing.T) {
}
}
func BenchmarkServerSTUN(b *testing.B) {
b.ReportAllocs()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer pc.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go serverSTUNListener(ctx, pc.(*net.UDPConn))
addr := pc.LocalAddr().(*net.UDPAddr)
var resBuf [1500]byte
cc, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")})
if err != nil {
b.Fatal(err)
}
tx := stun.NewTxID()
req := stun.Request(tx)
for i := 0; i < b.N; i++ {
if _, err := cc.WriteToUDP(req, addr); err != nil {
b.Fatal(err)
}
_, _, err := cc.ReadFromUDP(resBuf[:])
if err != nil {
b.Fatal(err)
}
}
}
func TestNoContent(t *testing.T) {
testCases := []struct {
name string

View File

@@ -158,7 +158,7 @@ func main() {
if !ok && (!oiok || !osok) {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key or TS_OAUTH_ID and TS_OAUTH_SECRET to your Tailscale OAuth ID and Secret")
}
if ok && (oiok || osok) {
if apiKey != "" && (oauthId != "" || oauthSecret != "") {
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
}
var client *http.Client

View File

@@ -31,10 +31,12 @@ var (
//go:embed hello.tmpl.html
var embeddedTemplate string
var localClient tailscale.LocalClient
func main() {
flag.Parse()
if *testIP != "" {
res, err := tailscale.WhoIs(context.Background(), *testIP)
res, err := localClient.WhoIs(context.Background(), *testIP)
if err != nil {
log.Fatal(err)
}
@@ -76,7 +78,7 @@ func main() {
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
switch hi.ServerName {
case "hello.ts.net":
return tailscale.GetCertificate(hi)
return localClient.GetCertificate(hi)
case "hello.ipn.dev":
c, err := tls.LoadX509KeyPair(
"/etc/hello/hello.ipn.dev.crt",
@@ -170,7 +172,7 @@ func root(w http.ResponseWriter, r *http.Request) {
return
}
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
who, err := localClient.WhoIs(r.Context(), r.RemoteAddr)
var data tmplData
if err != nil {
if devMode() {

View File

@@ -0,0 +1,260 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
"context"
"fmt"
"net/netip"
"slices"
"sync"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
xslices "golang.org/x/exp/slices"
corev1 "k8s.io/api/core/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tstime"
"tailscale.com/util/clientmetric"
"tailscale.com/util/set"
)
const (
reasonConnectorCreationFailed = "ConnectorCreationFailed"
reasonConnectorCreated = "ConnectorCreated"
reasonConnectorCleanupFailed = "ConnectorCleanupFailed"
reasonConnectorCleanupInProgress = "ConnectorCleanupInProgress"
reasonConnectorInvalid = "ConnectorInvalid"
messageConnectorCreationFailed = "Failed creating Connector: %v"
messageConnectorInvalid = "Connector is invalid: %v"
shortRequeue = time.Second * 5
)
type ConnectorReconciler struct {
client.Client
recorder record.EventRecorder
ssr *tailscaleSTSReconciler
logger *zap.SugaredLogger
tsnamespace string
clock tstime.Clock
mu sync.Mutex // protects following
subnetRouters set.Slice[types.UID] // for subnet routers gauge
exitNodes set.Slice[types.UID] // for exit nodes gauge
}
var (
// gaugeConnectorResources tracks the overall number of Connectors currently managed by this operator instance.
gaugeConnectorResources = clientmetric.NewGauge("k8s_connector_resources")
// gaugeConnectorSubnetRouterResources tracks the number of Connectors managed by this operator instance that are subnet routers.
gaugeConnectorSubnetRouterResources = clientmetric.NewGauge("k8s_connector_subnetrouter_resources")
// gaugeConnectorExitNodeResources tracks the number of Connectors currently managed by this operator instance that are exit nodes.
gaugeConnectorExitNodeResources = clientmetric.NewGauge("k8s_connector_exitnode_resources")
)
func (a *ConnectorReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
logger := a.logger.With("Connector", req.Name)
logger.Debugf("starting reconcile")
defer logger.Debugf("reconcile finished")
cn := new(tsapi.Connector)
err = a.Get(ctx, req.NamespacedName, cn)
if apierrors.IsNotFound(err) {
logger.Debugf("Connector not found, assuming it was deleted")
return reconcile.Result{}, nil
} else if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com Connector: %w", err)
}
if !cn.DeletionTimestamp.IsZero() {
logger.Debugf("Connector is being deleted or should not be exposed, cleaning up resources")
ix := xslices.Index(cn.Finalizers, FinalizerName)
if ix < 0 {
logger.Debugf("no finalizer, nothing to do")
return reconcile.Result{}, nil
}
if done, err := a.maybeCleanupConnector(ctx, logger, cn); err != nil {
return reconcile.Result{}, err
} else if !done {
logger.Debugf("Connector resource cleanup not yet finished, will retry...")
return reconcile.Result{RequeueAfter: shortRequeue}, nil
}
cn.Finalizers = append(cn.Finalizers[:ix], cn.Finalizers[ix+1:]...)
if err := a.Update(ctx, cn); err != nil {
return reconcile.Result{}, err
}
logger.Infof("Connector resources cleaned up")
return reconcile.Result{}, nil
}
oldCnStatus := cn.Status.DeepCopy()
setStatus := func(cn *tsapi.Connector, conditionType tsapi.ConnectorConditionType, status metav1.ConditionStatus, reason, message string) (reconcile.Result, error) {
tsoperator.SetConnectorCondition(cn, tsapi.ConnectorReady, status, reason, message, cn.Generation, a.clock, logger)
if !apiequality.Semantic.DeepEqual(oldCnStatus, cn.Status) {
// An error encountered here should get returned by the Reconcile function.
if updateErr := a.Client.Status().Update(ctx, cn); updateErr != nil {
err = errors.Wrap(err, updateErr.Error())
}
}
return res, err
}
if !slices.Contains(cn.Finalizers, FinalizerName) {
// This log line is printed exactly once during initial provisioning,
// because once the finalizer is in place this block gets skipped. So,
// this is a nice place to tell the operator that the high level,
// multi-reconcile operation is underway.
logger.Infof("ensuring Connector is set up")
cn.Finalizers = append(cn.Finalizers, FinalizerName)
if err := a.Update(ctx, cn); err != nil {
logger.Errorf("error adding finalizer: %w", err)
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionFalse, reasonConnectorCreationFailed, reasonConnectorCreationFailed)
}
}
if err := a.validate(cn); err != nil {
logger.Errorf("error validating Connector spec: %w", err)
message := fmt.Sprintf(messageConnectorInvalid, err)
a.recorder.Eventf(cn, corev1.EventTypeWarning, reasonConnectorInvalid, message)
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionFalse, reasonConnectorInvalid, message)
}
if err = a.maybeProvisionConnector(ctx, logger, cn); err != nil {
logger.Errorf("error creating Connector resources: %w", err)
message := fmt.Sprintf(messageConnectorCreationFailed, err)
a.recorder.Eventf(cn, corev1.EventTypeWarning, reasonConnectorCreationFailed, message)
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionFalse, reasonConnectorCreationFailed, message)
}
logger.Info("Connector resources synced")
cn.Status.IsExitNode = cn.Spec.ExitNode
if cn.Spec.SubnetRouter != nil {
cn.Status.SubnetRoutes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
cn.Status.SubnetRoutes = ""
return setStatus(cn, tsapi.ConnectorReady, metav1.ConditionTrue, reasonConnectorCreated, reasonConnectorCreated)
}
// maybeProvisionConnector ensures that any new resources required for this
// Connector instance are deployed to the cluster.
func (a *ConnectorReconciler) maybeProvisionConnector(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) error {
hostname := cn.Name + "-connector"
if cn.Spec.Hostname != "" {
hostname = string(cn.Spec.Hostname)
}
crl := childResourceLabels(cn.Name, a.tsnamespace, "connector")
sts := &tailscaleSTSConfig{
ParentResourceName: cn.Name,
ParentResourceUID: string(cn.UID),
Hostname: hostname,
ChildResourceLabels: crl,
Tags: cn.Spec.Tags.Stringify(),
Connector: &connector{
isExitNode: cn.Spec.ExitNode,
},
}
if cn.Spec.SubnetRouter != nil && len(cn.Spec.SubnetRouter.AdvertiseRoutes) > 0 {
sts.Connector.routes = cn.Spec.SubnetRouter.AdvertiseRoutes.Stringify()
}
a.mu.Lock()
if sts.Connector.isExitNode {
a.exitNodes.Add(cn.UID)
} else {
a.exitNodes.Remove(cn.UID)
}
if sts.Connector.routes != "" {
a.subnetRouters.Add(cn.GetUID())
} else {
a.subnetRouters.Remove(cn.GetUID())
}
a.mu.Unlock()
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
_, err := a.ssr.Provision(ctx, logger, sts)
return err
}
func (a *ConnectorReconciler) maybeCleanupConnector(ctx context.Context, logger *zap.SugaredLogger, cn *tsapi.Connector) (bool, error) {
if done, err := a.ssr.Cleanup(ctx, logger, childResourceLabels(cn.Name, a.tsnamespace, "connector")); err != nil {
return false, fmt.Errorf("failed to cleanup Connector resources: %w", err)
} else if !done {
logger.Debugf("Connector cleanup not done yet, waiting for next reconcile")
return false, nil
}
// Unlike most log entries in the reconcile loop, this will get printed
// exactly once at the very end of cleanup, because the final step of
// cleanup removes the tailscale finalizer, which will make all future
// reconciles exit early.
logger.Infof("cleaned up Connector resources")
a.mu.Lock()
a.subnetRouters.Remove(cn.UID)
a.exitNodes.Remove(cn.UID)
a.mu.Unlock()
gaugeConnectorExitNodeResources.Set(int64(a.exitNodes.Len()))
gaugeConnectorSubnetRouterResources.Set(int64(a.subnetRouters.Len()))
var connectors set.Slice[types.UID]
connectors.AddSlice(a.exitNodes.Slice())
connectors.AddSlice(a.subnetRouters.Slice())
gaugeConnectorResources.Set(int64(connectors.Len()))
return true, nil
}
func (a *ConnectorReconciler) validate(cn *tsapi.Connector) error {
// Connector fields are already validated at apply time with CEL validation
// on custom resource fields. The checks here are a backup in case the
// CEL validation breaks without us noticing.
if !(cn.Spec.SubnetRouter != nil || cn.Spec.ExitNode) {
return errors.New("invalid spec: a Connector must expose subnet routes or act as an exit node (or both)")
}
if cn.Spec.SubnetRouter == nil {
return nil
}
return validateSubnetRouter(cn.Spec.SubnetRouter)
}
func validateSubnetRouter(sb *tsapi.SubnetRouter) error {
if len(sb.AdvertiseRoutes) < 1 {
return errors.New("invalid subnet router spec: no routes defined")
}
var err error
for _, route := range sb.AdvertiseRoutes {
pfx, e := netip.ParsePrefix(string(route))
if e != nil {
err = errors.Wrap(err, fmt.Sprintf("route %s is invalid: %v", route, err))
continue
}
if pfx.Masked() != pfx {
err = errors.Wrap(err, fmt.Sprintf("route %s has non-address bits set; expected %s", pfx, pfx.Masked()))
}
}
return err
}

View File

@@ -0,0 +1,185 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
"context"
"testing"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tstest"
)
func TestConnector(t *testing.T) {
// Create a Connector that defines a Tailscale node that advertises
// 10.40.0.0/14 route and acts as an exit node.
cn := &tsapi.Connector{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
UID: types.UID("1234-UID"),
},
TypeMeta: metav1.TypeMeta{
Kind: tsapi.ConnectorKind,
APIVersion: "tailscale.io/v1alpha1",
},
Spec: tsapi.ConnectorSpec{
SubnetRouter: &tsapi.SubnetRouter{
AdvertiseRoutes: []tsapi.Route{"10.40.0.0/14"},
},
ExitNode: true,
},
}
fc := fake.NewClientBuilder().
WithScheme(tsapi.GlobalScheme).
WithObjects(cn).
WithStatusSubresource(cn).
Build()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
cl := tstest.NewClock(tstest.ClockOpts{})
cr := &ConnectorReconciler{
Client: fc,
ssr: &tailscaleSTSReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
},
clock: cl,
logger: zl.Sugar(),
}
expectReconciled(t, cr, "", "test")
fullName, shortName := findGenName(t, fc, "", "test", "connector")
opts := configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
hostname: "test-connector",
shouldUseDeclarativeConfig: true,
isExitNode: true,
subnetRoutes: "10.40.0.0/14",
confFileHash: "9321660203effb80983eaecc7b5ac5a8c53934926f46e895b9fe295dcfc5a904",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(opts))
// Add another route to be advertised.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.40.0.0/14", "10.44.0.0/20"}
})
opts.subnetRoutes = "10.40.0.0/14,10.44.0.0/20"
opts.confFileHash = "fb6c4daf67425f983985750cd8d6f2beae77e614fcb34176604571f5623d6862"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(opts))
// Remove a route.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter.AdvertiseRoutes = []tsapi.Route{"10.44.0.0/20"}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(opts))
// Remove the subnet router.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter = nil
})
opts.subnetRoutes = ""
opts.confFileHash = "7c421a99128eb80e79a285a82702f19f8f720615542a15bd794858a6275d8079"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(opts))
// Re-add the subnet router.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.SubnetRouter = &tsapi.SubnetRouter{
AdvertiseRoutes: []tsapi.Route{"10.44.0.0/20"},
}
})
opts.subnetRoutes = "10.44.0.0/20"
opts.confFileHash = "bacba177bcfe3849065cf6fee53d658a9bb4144197ac5b861727d69ea99742bb"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(opts))
// Delete the Connector.
if err = fc.Delete(context.Background(), cn); err != nil {
t.Fatalf("error deleting Connector: %v", err)
}
expectRequeue(t, cr, "", "test")
expectReconciled(t, cr, "", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
// Create a Connector that advertises a route and is not an exit node.
cn = &tsapi.Connector{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
UID: types.UID("1234-UID"),
},
TypeMeta: metav1.TypeMeta{
Kind: tsapi.ConnectorKind,
APIVersion: "tailscale.io/v1alpha1",
},
Spec: tsapi.ConnectorSpec{
SubnetRouter: &tsapi.SubnetRouter{
AdvertiseRoutes: []tsapi.Route{"10.40.0.0/14"},
},
},
}
opts.subnetRoutes = "10.44.0.0/14"
opts.isExitNode = false
mustCreate(t, fc, cn)
expectReconciled(t, cr, "", "test")
fullName, shortName = findGenName(t, fc, "", "test", "connector")
opts = configOpts{
stsName: shortName,
secretName: fullName,
parentType: "connector",
shouldUseDeclarativeConfig: true,
subnetRoutes: "10.40.0.0/14",
hostname: "test-connector",
confFileHash: "57d922331890c9b1c8c6ae664394cb254334c551d9cd9db14537b5d9da9fb17e",
}
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedSTS(opts))
// Add an exit node.
mustUpdate[tsapi.Connector](t, fc, "", "test", func(conn *tsapi.Connector) {
conn.Spec.ExitNode = true
})
opts.isExitNode = true
opts.confFileHash = "1499b591fd97a50f0330db6ec09979792c49890cf31f5da5bb6a3f50dba1e77a"
expectReconciled(t, cr, "", "test")
expectEqual(t, fc, expectedSTS(opts))
// Delete the Connector.
if err = fc.Delete(context.Background(), cn); err != nil {
t.Fatalf("error deleting Connector: %v", err)
}
expectRequeue(t, cr, "", "test")
expectReconciled(t, cr, "", "test")
expectMissing[appsv1.StatefulSet](t, fc, "operator-ns", shortName)
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
}

View File

@@ -49,6 +49,8 @@ spec:
image: {{ .Values.operatorConfig.image.repo }}{{- if .Values.operatorConfig.image.digest -}}{{ printf "@%s" .Values.operatorConfig.image.digest}}{{- else -}}{{ printf "%s" $operatorTag }}{{- end }}
imagePullPolicy: {{ .Values.operatorConfig.image.pullPolicy }}
env:
- name: OPERATOR_INITIAL_TAGS
value: {{ join "," .Values.operatorConfig.defaultTags }}
- name: OPERATOR_HOSTNAME
value: {{ .Values.operatorConfig.hostname }}
- name: OPERATOR_SECRET

View File

@@ -0,0 +1,8 @@
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: tailscale # class name currently can not be changed
annotations: {} # we do not support default IngressClass annotation https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
spec:
controller: tailscale.com/ts-ingress # controller name currently can not be changed
# parameters: {} # currently no parameters are supported

View File

@@ -18,6 +18,12 @@ rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingresses/status"]
verbs: ["*"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["tailscale.com"]
resources: ["connectors", "connectors/status"]
verbs: ["get", "list", "watch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@@ -8,7 +8,20 @@ oauth: {}
# clientId: ""
# clientSecret: ""
# installCRDs determines whether tailscale.com CRDs should be installed as part
# of chart installation. We do not use Helm's CRD installation mechanism as that
# does not allow for upgrading CRDs.
# https://helm.sh/docs/chart_best_practices/custom_resource_definitions/
installCRDs: "true"
operatorConfig:
# ACL tag that operator will be tagged with. Operator must be made owner of
# these tags
# https://tailscale.com/kb/1236/kubernetes-operator/?q=operator#setting-up-the-kubernetes-operator
# Multiple tags are defined as array items and passed to the operator as a comma-separated string
defaultTags:
- "tag:k8s-operator"
image:
repo: tailscale/k8s-operator
# Digest will be prioritized over tag. If neither are set appVersion will be
@@ -16,7 +29,7 @@ operatorConfig:
tag: ""
digest: ""
pullPolicy: Always
logging: "info"
logging: "info" # info, debug, dev
hostname: "tailscale-operator"
nodeSelector:
kubernetes.io/os: linux
@@ -47,7 +60,9 @@ proxyConfig:
# ACL tag that operator will tag proxies with. Operator must be made owner of
# these tags
# https://tailscale.com/kb/1236/kubernetes-operator/?q=operator#setting-up-the-kubernetes-operator
defaultTags: tag:k8s
# Multiple tags can be passed as a comma-separated string i.e 'tag:k8s-proxies,tag:prod'.
# Note that if you pass multiple tags to this field via `--set` flag to helm upgrade/install commands you must escape the comma (for example, "tag:k8s-proxies\,tag:prod"). See https://github.com/helm/helm/issues/1556
defaultTags: "tag:k8s"
firewallMode: auto
# apiServerProxyConfig allows to configure whether the operator should expose

View File

@@ -0,0 +1,125 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.13.0
name: connectors.tailscale.com
spec:
group: tailscale.com
names:
kind: Connector
listKind: ConnectorList
plural: connectors
shortNames:
- cn
singular: connector
scope: Cluster
versions:
- additionalPrinterColumns:
- description: CIDR ranges exposed to tailnet by a subnet router defined via this Connector instance.
jsonPath: .status.subnetRoutes
name: SubnetRoutes
type: string
- description: Whether this Connector instance defines an exit node.
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
type: object
required:
- spec
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ConnectorSpec describes the desired Tailscale component.
type: object
properties:
exitNode:
description: ExitNode defines whether the Connector node should act as a Tailscale exit node. Defaults to false. https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
description: Hostname is the tailnet hostname that should be assigned to the Connector node. If unset, hostname defaults to <connector name>-connector. Hostname can contain lower case letters, numbers and dashes, it must not start or end with a dash and must be between 2 and 63 characters long.
type: string
pattern: ^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
subnetRouter:
description: SubnetRouter defines subnet routes that the Connector node should expose to tailnet. If unset, none are exposed. https://tailscale.com/kb/1019/subnets/
type: object
required:
- advertiseRoutes
properties:
advertiseRoutes:
description: AdvertiseRoutes refer to CIDRs that the subnet router should make available. Route values must be strings that represent a valid IPv4 or IPv6 CIDR range. Values can be Tailscale 4via6 subnet routes. https://tailscale.com/kb/1201/4via6-subnets/
type: array
minItems: 1
items:
type: string
format: cidr
tags:
description: Tags that the Tailscale node will be tagged with. Defaults to [tag:k8s]. To autoapprove the subnet routes or exit node defined by a Connector, you can configure Tailscale ACLs to give these tags the necessary permissions. See https://tailscale.com/kb/1018/acls/#auto-approvers-for-routes-and-exit-nodes. If you specify custom tags here, you must also make the operator an owner of these tags. See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator. Tags cannot be changed once a Connector node has been created. Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$.
type: array
items:
type: string
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
x-kubernetes-validations:
- rule: has(self.subnetRouter) || self.exitNode == true
message: A Connector needs to be either an exit node or a subnet router, or both.
status:
description: ConnectorStatus describes the status of the Connector. This is set and managed by the Tailscale operator.
type: object
properties:
conditions:
description: List of status conditions to indicate the status of the Connector. Known condition types are `ConnectorReady`.
type: array
items:
description: ConnectorCondition contains condition information for a Connector.
type: object
required:
- status
- type
properties:
lastTransitionTime:
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
type: string
format: date-time
message:
description: Message is a human readable description of the details of the last transition, complementing reason.
type: string
observedGeneration:
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
type: integer
format: int64
reason:
description: Reason is a brief machine readable explanation for the condition's last transition.
type: string
status:
description: Status of the condition, one of ('True', 'False', 'Unknown').
type: string
type:
description: Type of the condition, known values are (`SubnetRouterReady`).
type: string
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean
subnetRoutes:
description: SubnetRoutes are the routes currently exposed to tailnet via this Connector instance.
type: string
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,19 @@
# Before applying ensure that the operator owns tag:prod.
# https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator.
# To set up autoapproval set tag:prod as approver for 10.40.0.0/14 route and exit node.
# Otherwise approve it manually in Machines panel once the
# ts-prod Tailscale node has been created.
# See https://tailscale.com/kb/1018/acls/#auto-approvers-for-routes-and-exit-nodes
apiVersion: tailscale.com/v1alpha1
kind: Connector
metadata:
name: prod
spec:
tags:
- "tag:prod"
hostname: ts-prod
subnetRouter:
advertiseRoutes:
- "10.40.0.0/14"
- "192.168.0.0/14"
exitNode: true

View File

@@ -27,6 +27,132 @@ metadata:
name: proxies
namespace: tailscale
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.13.0
name: connectors.tailscale.com
spec:
group: tailscale.com
names:
kind: Connector
listKind: ConnectorList
plural: connectors
shortNames:
- cn
singular: connector
scope: Cluster
versions:
- additionalPrinterColumns:
- description: CIDR ranges exposed to tailnet by a subnet router defined via this Connector instance.
jsonPath: .status.subnetRoutes
name: SubnetRoutes
type: string
- description: Whether this Connector instance defines an exit node.
jsonPath: .status.isExitNode
name: IsExitNode
type: string
- description: Status of the deployed Connector resources.
jsonPath: .status.conditions[?(@.type == "ConnectorReady")].reason
name: Status
type: string
name: v1alpha1
schema:
openAPIV3Schema:
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: ConnectorSpec describes the desired Tailscale component.
properties:
exitNode:
description: ExitNode defines whether the Connector node should act as a Tailscale exit node. Defaults to false. https://tailscale.com/kb/1103/exit-nodes
type: boolean
hostname:
description: Hostname is the tailnet hostname that should be assigned to the Connector node. If unset, hostname defaults to <connector name>-connector. Hostname can contain lower case letters, numbers and dashes, it must not start or end with a dash and must be between 2 and 63 characters long.
pattern: ^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$
type: string
subnetRouter:
description: SubnetRouter defines subnet routes that the Connector node should expose to tailnet. If unset, none are exposed. https://tailscale.com/kb/1019/subnets/
properties:
advertiseRoutes:
description: AdvertiseRoutes refer to CIDRs that the subnet router should make available. Route values must be strings that represent a valid IPv4 or IPv6 CIDR range. Values can be Tailscale 4via6 subnet routes. https://tailscale.com/kb/1201/4via6-subnets/
items:
format: cidr
type: string
minItems: 1
type: array
required:
- advertiseRoutes
type: object
tags:
description: Tags that the Tailscale node will be tagged with. Defaults to [tag:k8s]. To autoapprove the subnet routes or exit node defined by a Connector, you can configure Tailscale ACLs to give these tags the necessary permissions. See https://tailscale.com/kb/1018/acls/#auto-approvers-for-routes-and-exit-nodes. If you specify custom tags here, you must also make the operator an owner of these tags. See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator. Tags cannot be changed once a Connector node has been created. Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$.
items:
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
type: string
type: array
type: object
x-kubernetes-validations:
- message: A Connector needs to be either an exit node or a subnet router, or both.
rule: has(self.subnetRouter) || self.exitNode == true
status:
description: ConnectorStatus describes the status of the Connector. This is set and managed by the Tailscale operator.
properties:
conditions:
description: List of status conditions to indicate the status of the Connector. Known condition types are `ConnectorReady`.
items:
description: ConnectorCondition contains condition information for a Connector.
properties:
lastTransitionTime:
description: LastTransitionTime is the timestamp corresponding to the last status change of this condition.
format: date-time
type: string
message:
description: Message is a human readable description of the details of the last transition, complementing reason.
type: string
observedGeneration:
description: If set, this represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.condition[x].observedGeneration is 9, the condition is out of date with respect to the current state of the Connector.
format: int64
type: integer
reason:
description: Reason is a brief machine readable explanation for the condition's last transition.
type: string
status:
description: Status of the condition, one of ('True', 'False', 'Unknown').
type: string
type:
description: Type of the condition, known values are (`SubnetRouterReady`).
type: string
required:
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
isExitNode:
description: IsExitNode is set to true if the Connector acts as an exit node.
type: boolean
subnetRoutes:
description: SubnetRoutes are the routes currently exposed to tailnet via this Connector instance.
type: string
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
@@ -47,6 +173,24 @@ rules:
- ingresses/status
verbs:
- '*'
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- tailscale.com
resources:
- connectors
- connectors/status
verbs:
- get
- list
- watch
- update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
@@ -140,6 +284,8 @@ spec:
spec:
containers:
- env:
- name: OPERATOR_INITIAL_TAGS
value: tag:k8s-operator
- name: OPERATOR_HOSTNAME
value: tailscale-operator
- name: OPERATOR_SECRET
@@ -176,3 +322,11 @@ spec:
- name: oauth
secret:
secretName: operator-oauth
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
annotations: {}
name: tailscale
spec:
controller: tailscale.com/ts-ingress

View File

@@ -18,15 +18,49 @@ import (
"gopkg.in/yaml.v3"
)
const (
operatorDeploymentFilesPath = "cmd/k8s-operator/deploy"
crdPath = operatorDeploymentFilesPath + "/crds/tailscale.com_connectors.yaml"
helmTemplatesPath = operatorDeploymentFilesPath + "/chart/templates"
crdTemplatePath = helmTemplatesPath + "/connectors.yaml"
helmConditionalStart = "{{ if .Values.installCRDs -}}\n"
helmConditionalEnd = "{{- end -}}"
)
func main() {
if len(os.Args) < 2 {
log.Fatalf("usage ./generate [staticmanifests|helmcrd]")
}
repoRoot := "../../"
cmd := exec.Command("./tool/helm", "template", "operator", "./cmd/k8s-operator/deploy/chart",
switch os.Args[1] {
case "helmcrd": // insert CRD to Helm templates behind a installCRDs=true conditional check
log.Print("Adding Connector CRD to Helm templates")
if err := generate("./"); err != nil {
log.Fatalf("error adding Connector CRD to Helm templates: %v", err)
}
return
case "staticmanifests": // generate static manifests from Helm templates (including the CRD)
default:
log.Fatalf("unknown option %s, known options are 'staticmanifests', 'helmcrd'", os.Args[1])
}
log.Printf("Inserting CRD into the Helm templates")
if err := generate(repoRoot); err != nil {
log.Fatalf("error adding Connector CRD to Helm templates: %v", err)
}
defer func() {
if err := cleanup(repoRoot); err != nil {
log.Fatalf("error cleaning up generated resources")
}
}()
log.Print("Templating Helm chart contents")
helmTmplCmd := exec.Command("./tool/helm", "template", "operator", "./cmd/k8s-operator/deploy/chart",
"--namespace=tailscale")
cmd.Dir = repoRoot
helmTmplCmd.Dir = repoRoot
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
helmTmplCmd.Stdout = &out
helmTmplCmd.Stderr = os.Stderr
if err := helmTmplCmd.Run(); err != nil {
log.Fatalf("error templating helm manifests: %v", err)
}
@@ -54,7 +88,6 @@ func main() {
if err != nil {
log.Fatalf("failed read from input data: %v", err)
}
bytes, err := yaml.Marshal(document)
if err != nil {
log.Fatalf("failed to marshal YAML document: %v", err)
@@ -72,3 +105,35 @@ func main() {
log.Fatalf("error writing new file: %v", err)
}
}
func generate(baseDir string) error {
log.Print("Placing Connector CRD into Helm templates..")
chartBytes, err := os.ReadFile(filepath.Join(baseDir, crdPath))
if err != nil {
return fmt.Errorf("error reading CRD contents: %w", err)
}
// Place a new temporary Helm template file with the templated CRD
// contents into Helm templates.
file, err := os.Create(filepath.Join(baseDir, crdTemplatePath))
if err != nil {
return fmt.Errorf("error creating CRD template file: %w", err)
}
if _, err := file.Write([]byte(helmConditionalStart)); err != nil {
return fmt.Errorf("error writing helm if statement start: %w", err)
}
if _, err := file.Write(chartBytes); err != nil {
return fmt.Errorf("error writing chart bytes: %w", err)
}
if _, err := file.Write([]byte(helmConditionalEnd)); err != nil {
return fmt.Errorf("error writing helm if-statement end: %w", err)
}
return nil
}
func cleanup(baseDir string) error {
log.Print("Cleaning up CRD from Helm templates")
if err := os.Remove(filepath.Join(baseDir, crdTemplatePath)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error cleaning up CRD template: %w", err)
}
return nil
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9 && !windows
package main
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func Test_generate(t *testing.T) {
base, err := os.Getwd()
base = filepath.Join(base, "../../../")
if err != nil {
t.Fatalf("error getting current working directory: %v", err)
}
defer cleanup(base)
if err := generate(base); err != nil {
t.Fatalf("CRD template generation: %v", err)
}
tempDir := t.TempDir()
helmCLIPath := filepath.Join(base, "tool/helm")
helmChartTemplatesPath := filepath.Join(base, "cmd/k8s-operator/deploy/chart")
helmPackageCmd := exec.Command(helmCLIPath, "package", helmChartTemplatesPath, "--destination", tempDir, "--version", "0.0.1")
helmPackageCmd.Stderr = os.Stderr
helmPackageCmd.Stdout = os.Stdout
if err := helmPackageCmd.Run(); err != nil {
t.Fatalf("error packaging Helm chart: %v", err)
}
helmPackagePath := filepath.Join(tempDir, "tailscale-operator-0.0.1.tgz")
helmLintCmd := exec.Command(helmCLIPath, "lint", helmPackagePath)
helmLintCmd.Stderr = os.Stderr
helmLintCmd.Stdout = os.Stdout
if err := helmLintCmd.Run(); err != nil {
t.Fatalf("Helm chart linter failed: %v", err)
}
// Test that default Helm install contains the CRD
installContentsWithCRD := bytes.NewBuffer([]byte{})
helmTemplateWithCRDCmd := exec.Command(helmCLIPath, "template", helmPackagePath)
helmTemplateWithCRDCmd.Stderr = os.Stderr
helmTemplateWithCRDCmd.Stdout = installContentsWithCRD
if err := helmTemplateWithCRDCmd.Run(); err != nil {
t.Fatalf("templating Helm chart with CRDs failed: %v", err)
}
if !strings.Contains(installContentsWithCRD.String(), "name: connectors.tailscale.com") {
t.Errorf("CRD not found in default chart install")
}
// Test that CRD can be excluded from Helm chart install
installContentsWithoutCRD := bytes.NewBuffer([]byte{})
helmTemplateWithoutCRDCmd := exec.Command(helmCLIPath, "template", helmPackagePath, "--set", "installCRDs=false")
helmTemplateWithoutCRDCmd.Stderr = os.Stderr
helmTemplateWithoutCRDCmd.Stdout = installContentsWithoutCRD
if err := helmTemplateWithoutCRDCmd.Run(); err != nil {
t.Fatalf("templating Helm chart without CRDs failed: %v", err)
}
if strings.Contains(installContentsWithoutCRD.String(), "name: connectors.tailscale.com") {
t.Errorf("CRD found in chart install that should not contain a CRD")
}
}

View File

@@ -12,10 +12,12 @@ import (
"strings"
"sync"
"github.com/pkg/errors"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -26,6 +28,12 @@ import (
"tailscale.com/util/set"
)
const (
tailscaleIngressClassName = "tailscale" // ingressClass.metadata.name for tailscale IngressClass resource
tailscaleIngressControllerName = "tailscale.com/ts-ingress" // ingressClass.spec.controllerName for tailscale IngressClass resource
ingressClassDefaultAnnotation = "ingressclass.kubernetes.io/is-default-class" // we do not support this https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
)
type IngressReconciler struct {
client.Client
@@ -109,6 +117,10 @@ func (a *IngressReconciler) maybeCleanup(ctx context.Context, logger *zap.Sugare
// This function adds a finalizer to ing, ensuring that we can handle orderly
// deprovisioning later.
func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.SugaredLogger, ing *networkingv1.Ingress) error {
if err := a.validateIngressClass(ctx); err != nil {
logger.Warnf("error validating tailscale IngressClass: %v. In future this might be a terminal error.", err)
}
if !slices.Contains(ing.Finalizers, FinalizerName) {
// This log line is printed exactly once during initial provisioning,
// because once the finalizer is in place this block gets skipped. So,
@@ -205,10 +217,25 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
continue
}
for _, p := range rule.HTTP.Paths {
// Send a warning if folks use Exact path type - to make
// it easier for us to support Exact path type matching
// in the future if needed.
// https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types
if *p.PathType == networkingv1.PathTypeExact {
msg := "Exact path type strict matching is currently not supported and requests will be routed as for Prefix path type. This behaviour might change in the future."
logger.Warnf(fmt.Sprintf("Unsupported Path type exact for path %s. %s", p.Path, msg))
a.recorder.Eventf(ing, corev1.EventTypeWarning, "UnsupportedPathTypeExact", msg)
}
addIngressBackend(&p.Backend, p.Path)
}
}
if len(web.Handlers) == 0 {
logger.Warn("Ingress contains no valid backends")
a.recorder.Eventf(ing, corev1.EventTypeWarning, "NoValidBackends", "no valid backends")
return nil
}
crl := childResourceLabels(ing.Name, ing.Namespace, "ingress")
var tags []string
if tstr, ok := ing.Annotations[AnnotationTags]; ok {
@@ -267,5 +294,28 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
func (a *IngressReconciler) shouldExpose(ing *networkingv1.Ingress) bool {
return ing != nil &&
ing.Spec.IngressClassName != nil &&
*ing.Spec.IngressClassName == "tailscale"
*ing.Spec.IngressClassName == tailscaleIngressClassName
}
// validateIngressClass attempts to validate that 'tailscale' IngressClass
// included in Tailscale installation manifests exists and has not been modified
// to attempt to enable features that we do not support.
func (a *IngressReconciler) validateIngressClass(ctx context.Context) error {
ic := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: tailscaleIngressClassName,
},
}
if err := a.Get(ctx, client.ObjectKeyFromObject(ic), ic); apierrors.IsNotFound(err) {
return errors.New("Tailscale IngressClass not found in cluster. Latest installation manifests include a tailscale IngressClass - please update")
} else if err != nil {
return fmt.Errorf("error retrieving 'tailscale' IngressClass: %w", err)
}
if ic.Spec.Controller != tailscaleIngressControllerName {
return fmt.Errorf("Tailscale Ingress class controller name %s does not match tailscale Ingress controller name %s. Ensure that you are using 'tailscale' IngressClass from latest Tailscale installation manifests", ic.Spec.Controller, tailscaleIngressControllerName)
}
if ic.GetAnnotations()[ingressClassDefaultAnnotation] != "" {
return fmt.Errorf("%s annotation is set on 'tailscale' IngressClass, but Tailscale Ingress controller does not support default Ingress class. Ensure that you are using 'tailscale' IngressClass from latest Tailscale installation manifests", ingressClassDefaultAnnotation)
}
return nil
}

View File

@@ -37,12 +37,18 @@ import (
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/store/kubestore"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/tsnet"
"tailscale.com/tstime"
"tailscale.com/types/logger"
"tailscale.com/version"
)
//go:generate go run tailscale.com/cmd/k8s-operator/generate
// Generate static manifests for deploying Tailscale operator on Kubernetes from the operator's Helm chart.
//go:generate go run tailscale.com/cmd/k8s-operator/generate staticmanifests
// Generate Connector CustomResourceDefinition yaml from its Go types.
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen crd schemapatch:manifests=./deploy/crds output:dir=./deploy/crds paths=../../k8s-operator/apis/...
func main() {
// Required to use our client API. We're fine with the instability since the
@@ -84,6 +90,8 @@ func main() {
defer s.Close()
restConfig := config.GetConfigOrDie()
maybeLaunchAPIServerProxy(zlog, restConfig, s, mode)
// TODO (irbekrm): gather the reconciler options into an opts struct
// rather than passing a million of them in one by one.
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags, tsFirewallMode)
}
@@ -206,20 +214,26 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
nsFilter := cache.ByObject{
Field: client.InNamespace(tsNamespace).AsSelector(),
}
mgr, err := manager.New(restConfig, manager.Options{
mgrOpts := manager.Options{
// TODO (irbekrm): stricter filtering what we watch/cache/call
// reconcilers on. c/r by default starts a watch on any
// resources that we GET via the controller manager's client.
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
&corev1.Secret{}: nsFilter,
&appsv1.StatefulSet{}: nsFilter,
},
},
})
Scheme: tsapi.GlobalScheme,
}
mgr, err := manager.New(restConfig, mgrOpts)
if err != nil {
startlog.Fatalf("could not create manager: %v", err)
}
svcFilter := handler.EnqueueRequestsFromMapFunc(serviceHandler)
svcChildFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("svc"))
eventRecorder := mgr.GetEventRecorderFor("tailscale-operator")
ssr := &tailscaleSTSReconciler{
Client: mgr.GetClient(),
@@ -264,6 +278,21 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
startlog.Fatalf("could not create controller: %v", err)
}
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("connector"))
err = builder.ControllerManagedBy(mgr).
For(&tsapi.Connector{}).
Watches(&appsv1.StatefulSet{}, connectorFilter).
Watches(&corev1.Secret{}, connectorFilter).
Complete(&ConnectorReconciler{
ssr: ssr,
recorder: eventRecorder,
Client: mgr.GetClient(),
logger: zlog.Named("connector-reconciler"),
clock: tstime.DefaultClock{},
})
if err != nil {
startlog.Fatal("could not create connector reconciler: %v", err)
}
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
startlog.Fatalf("could not start manager: %v", err)

View File

@@ -6,24 +6,15 @@
package main
import (
"context"
"fmt"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
"tailscale.com/types/ptr"
)
@@ -66,16 +57,19 @@ func TestLoadBalancerClass(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
opts := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(o))
expectEqual(t, fc, expectedSecret(t, opts))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(opts))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -159,6 +153,7 @@ func TestLoadBalancerClass(t *testing.T) {
}
expectEqual(t, fc, want)
}
func TestTailnetTargetFQDNAnnotation(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
@@ -203,16 +198,18 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
tailnetTargetFQDN: tailnetTargetFQDN,
hostname: "default-test",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -235,14 +232,8 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
o = stsOpts{
name: shortName,
secretName: fullName,
tailnetTargetFQDN: tailnetTargetFQDN,
hostname: "default-test",
}
expectEqual(t, fc, expectedSTS(o))
// Change the tailscale-target-fqdn annotation which should update the
@@ -272,6 +263,7 @@ func TestTailnetTargetFQDNAnnotation(t *testing.T) {
expectMissing[corev1.Service](t, fc, "operator-ns", shortName)
expectMissing[corev1.Secret](t, fc, "operator-ns", fullName)
}
func TestTailnetTargetIPAnnotation(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
@@ -316,16 +308,18 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
tailnetTargetIP: tailnetTargetIP,
hostname: "default-test",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -348,14 +342,8 @@ func TestTailnetTargetIPAnnotation(t *testing.T) {
},
}
expectEqual(t, fc, want)
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
o = stsOpts{
name: shortName,
secretName: fullName,
tailnetTargetIP: tailnetTargetIP,
hostname: "default-test",
}
expectEqual(t, fc, expectedSTS(o))
// Change the tailscale-target-ip annotation which should update the
@@ -427,15 +415,18 @@ func TestAnnotations(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -532,15 +523,18 @@ func TestAnnotationIntoLB(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
// Normally the Tailscale proxy pod would come up here and write its info
@@ -586,11 +580,6 @@ func TestAnnotationIntoLB(t *testing.T) {
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
expectEqual(t, fc, expectedHeadlessService(shortName))
o = stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
}
expectEqual(t, fc, expectedSTS(o))
// ... but the service should have a LoadBalancer status.
@@ -665,15 +654,18 @@ func TestLBIntoAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
// Normally the Tailscale proxy pod would come up here and write its info
@@ -737,11 +729,6 @@ func TestLBIntoAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedHeadlessService(shortName))
o = stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
}
expectEqual(t, fc, expectedSTS(o))
want = &corev1.Service{
@@ -808,15 +795,18 @@ func TestCustomHostname(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "reindeer-flotilla",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "reindeer-flotilla",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSecret(t, o))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(o))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -919,12 +909,15 @@ func TestCustomPriorityClassName(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
o := stsOpts{
name: shortName,
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "tailscale-critical",
priorityClassName: "custom-priority-class-name",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(o))
@@ -969,14 +962,16 @@ func TestDefaultLoadBalancer(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(o))
}
@@ -1021,335 +1016,20 @@ func TestProxyFirewallMode(t *testing.T) {
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
o := stsOpts{
name: shortName,
secretName: fullName,
hostname: "default-test",
firewallMode: "nftables",
fullName, shortName := findGenName(t, fc, "default", "test", "svc")
o := configOpts{
stsName: shortName,
secretName: fullName,
namespace: "default",
parentType: "svc",
hostname: "default-test",
firewallMode: "nftables",
clusterTargetIP: "10.20.30.40",
}
expectEqual(t, fc, expectedSTS(o))
}
func expectedSecret(name string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
StringData: map[string]string{
"authkey": "secret-authkey",
},
}
}
func expectedHeadlessService(name string) *corev1.Service {
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
GenerateName: "ts-test-",
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "1234-UID",
},
ClusterIP: "None",
},
}
}
func expectedSTS(opts stsOpts) *appsv1.StatefulSet {
containerEnv := []corev1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
{Name: "TS_HOSTNAME", Value: opts.hostname},
}
annots := map[string]string{
"tailscale.com/operator-last-set-hostname": opts.hostname,
}
if opts.tailnetTargetIP != "" {
annots["tailscale.com/operator-last-set-ts-tailnet-target-ip"] = opts.tailnetTargetIP
containerEnv = append(containerEnv, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_IP",
Value: opts.tailnetTargetIP,
})
} else if opts.tailnetTargetFQDN != "" {
annots["tailscale.com/operator-last-set-ts-tailnet-target-fqdn"] = opts.tailnetTargetFQDN
containerEnv = append(containerEnv, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_FQDN",
Value: opts.tailnetTargetFQDN,
})
} else {
containerEnv = append(containerEnv, corev1.EnvVar{
Name: "TS_DEST_IP",
Value: "10.20.30.40",
})
annots["tailscale.com/operator-last-set-cluster-ip"] = "10.20.30.40"
}
if opts.firewallMode != "" {
containerEnv = append(containerEnv, corev1.EnvVar{
Name: "TS_DEBUG_FIREWALL_MODE",
Value: opts.firewallMode,
})
}
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: opts.name,
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
Spec: appsv1.StatefulSetSpec{
Replicas: ptr.To[int32](1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "1234-UID"},
},
ServiceName: opts.name,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: annots,
DeletionGracePeriodSeconds: ptr.To[int64](10),
Labels: map[string]string{"app": "1234-UID"},
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",
PriorityClassName: opts.priorityClassName,
InitContainers: []corev1.Container{
{
Name: "sysctler",
Image: "tailscale/tailscale",
Command: []string{"/bin/sh"},
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
},
},
Containers: []corev1.Container{
{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: containerEnv,
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
ImagePullPolicy: "Always",
},
},
},
},
},
}
}
func findGenName(t *testing.T, client client.Client, ns, name string) (full, noSuffix string) {
t.Helper()
labels := map[string]string{
LabelManaged: "true",
LabelParentName: name,
LabelParentNamespace: ns,
LabelParentType: "svc",
}
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
if err != nil {
t.Fatalf("finding secret for %q: %v", name, err)
}
if s == nil {
t.Fatalf("no secret found for %q", name)
}
return s.GetName(), strings.TrimSuffix(s.GetName(), "-0")
}
func mustCreate(t *testing.T, client client.Client, obj client.Object) {
t.Helper()
if err := client.Create(context.Background(), obj); err != nil {
t.Fatalf("creating %q: %v", obj.GetName(), err)
}
}
func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Status().Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
t.Helper()
got := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: want.GetName(),
Namespace: want.GetNamespace(),
}, got); err != nil {
t.Fatalf("getting %q: %v", want.GetName(), err)
}
// The resource version changes eagerly whenever the operator does even a
// no-op update. Asserting a specific value leads to overly brittle tests,
// so just remove it from both got and want.
got.SetResourceVersion("")
want.SetResourceVersion("")
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("unexpected object (-got +want):\n%s", diff)
}
}
func expectMissing[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); !apierrors.IsNotFound(err) {
t.Fatalf("object %s/%s unexpectedly present, wanted missing", ns, name)
}
}
func expectReconciled(t *testing.T, sr *ServiceReconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: ns,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected immediate requeue")
}
if res.RequeueAfter != 0 {
t.Fatalf("unexpected timed requeue (%v)", res.RequeueAfter)
}
}
func expectRequeue(t *testing.T, sr *ServiceReconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: ns,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected immediate requeue")
}
if res.RequeueAfter == 0 {
t.Fatalf("expected timed requeue, got success")
}
}
type stsOpts struct {
name string
secretName string
hostname string
priorityClassName string
firewallMode string
tailnetTargetIP string
tailnetTargetFQDN string
}
type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
deleted []string
}
func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
c.Lock()
defer c.Unlock()
c.keyRequests = append(c.keyRequests, caps)
k := &tailscale.Key{
ID: "key",
Created: time.Now(),
Capabilities: caps,
}
return "secret-authkey", k, nil
}
func (c *fakeTSClient) DeleteDevice(ctx context.Context, deviceID string) error {
c.Lock()
defer c.Unlock()
c.deleted = append(c.deleted, deviceID)
return nil
}
func (c *fakeTSClient) KeyRequests() []tailscale.KeyCapabilities {
c.Lock()
defer c.Unlock()
return c.keyRequests
}
func (c *fakeTSClient) Deleted() []string {
c.Lock()
defer c.Unlock()
return c.deleted
}
func Test_isMagicDNSName(t *testing.T) {
tests := []struct {
in string

View File

@@ -6,7 +6,6 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
@@ -24,22 +23,11 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
"tailscale.com/util/clientmetric"
"tailscale.com/util/ctxkey"
"tailscale.com/util/set"
)
type whoIsKey struct{}
// whoIsFromRequest returns the WhoIsResponse previously stashed by a call to
// addWhoIsToRequest.
func whoIsFromRequest(r *http.Request) *apitype.WhoIsResponse {
return r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
}
// addWhoIsToRequest stashes who in r's context, retrievable by a call to
// whoIsFromRequest.
func addWhoIsToRequest(r *http.Request, who *apitype.WhoIsResponse) *http.Request {
return r.WithContext(context.WithValue(r.Context(), whoIsKey{}, who))
}
var whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
@@ -127,7 +115,7 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
counterNumRequestsProxied.Add(1)
h.rp.ServeHTTP(w, addWhoIsToRequest(r, who))
h.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
}
// runAPIServerProxy runs an HTTP server that authenticates requests using the
@@ -240,7 +228,7 @@ type impersonateRule struct {
// in the context by the apiserverProxy.
func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
log = log.With("remote", r.RemoteAddr)
who := whoIsFromRequest(r)
who := whoIsKey.Value(r.Context())
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
if len(rules) == 0 && err == nil {
// Try the old capability name for backwards compatibility.

View File

@@ -95,7 +95,7 @@ func TestImpersonationHeaders(t *testing.T) {
for _, tc := range tests {
r := must.Get(http.NewRequest("GET", "https://op.ts.net/api/foo", nil))
r = addWhoIsToRequest(r, &apitype.WhoIsResponse{
r = r.WithContext(whoIsKey.WithValue(r.Context(), &apitype.WhoIsResponse{
Node: &tailcfg.Node{
Name: "node.ts.net",
Tags: tc.tags,
@@ -104,7 +104,7 @@ func TestImpersonationHeaders(t *testing.T) {
LoginName: tc.emailish,
},
CapMap: tc.capMap,
})
}))
addImpersonationHeaders(r, zl.Sugar())
if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" {

View File

@@ -7,6 +7,7 @@ package main
import (
"context"
"crypto/sha256"
_ "embed"
"encoding/json"
"errors"
@@ -26,6 +27,7 @@ import (
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
"tailscale.com/types/opt"
@@ -54,11 +56,17 @@ const (
AnnotationFunnel = "tailscale.com/funnel"
// Annotations set by the operator on pods to trigger restarts when the
// hostname, IP or FQDN changes.
// hostname, IP, FQDN or tailscaled config changes.
podAnnotationLastSetClusterIP = "tailscale.com/operator-last-set-cluster-ip"
podAnnotationLastSetHostname = "tailscale.com/operator-last-set-hostname"
podAnnotationLastSetTailnetTargetIP = "tailscale.com/operator-last-set-ts-tailnet-target-ip"
podAnnotationLastSetTailnetTargetFQDN = "tailscale.com/operator-last-set-ts-tailnet-target-fqdn"
// podAnnotationLastSetConfigFileHash is sha256 hash of the current tailscaled configuration contents.
podAnnotationLastSetConfigFileHash = "tailscale.com/operator-last-set-config-file-hash"
// tailscaledConfigKey is the name of the key in proxy Secret Data that
// holds the tailscaled config contents.
tailscaledConfigKey = "tailscaled"
)
type tailscaleSTSConfig struct {
@@ -66,18 +74,26 @@ type tailscaleSTSConfig struct {
ParentResourceUID string
ChildResourceLabels map[string]string
ServeConfig *ipn.ServeConfig
// Tailscale target in cluster we are setting up ingress for
ClusterTargetIP string
ServeConfig *ipn.ServeConfig
ClusterTargetIP string // ingress target
// Tailscale IP of a Tailscale service we are setting up egress for
TailnetTargetIP string
TailnetTargetIP string // egress target IP
// Tailscale FQDN of a Tailscale service we are setting up egress for
TailnetTargetFQDN string
TailnetTargetFQDN string // egress target FQDN
Hostname string
Tags []string // if empty, use defaultTags
// Connector specifies a configuration of a Connector instance if that's
// what this StatefulSet should be created for.
Connector *connector
}
type connector struct {
// routes is a list of subnet routes that this Connector should expose.
routes string
// isExitNode defines whether this Connector should act as an exit node.
isExitNode bool
}
type tailscaleSTSReconciler struct {
@@ -107,16 +123,17 @@ func (a *tailscaleSTSReconciler) IsHTTPSEnabledOnTailnet() bool {
// up to date.
func (a *tailscaleSTSReconciler) Provision(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig) (*corev1.Service, error) {
// Do full reconcile.
// TODO (don't create Service for the Connector)
hsvc, err := a.reconcileHeadlessService(ctx, logger, sts)
if err != nil {
return nil, fmt.Errorf("failed to reconcile headless service: %w", err)
}
secretName, err := a.createOrGetSecret(ctx, logger, sts, hsvc)
secretName, tsConfigHash, err := a.createOrGetSecret(ctx, logger, sts, hsvc)
if err != nil {
return nil, fmt.Errorf("failed to create or get API key secret: %w", err)
}
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName)
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName, tsConfigHash)
if err != nil {
return nil, fmt.Errorf("failed to reconcile statefulset: %w", err)
}
@@ -197,18 +214,17 @@ const maxStatefulSetNameLength = 63 - 10 - 1
// generation will NOT result in a StatefulSet name longer than 52 chars.
// This is done because of https://github.com/kubernetes/kubernetes/issues/64023.
func statefulSetNameBase(parent string) string {
base := fmt.Sprintf("ts-%s-", parent)
// Calculate what length name GenerateName returns for this base.
generator := names.SimpleNameGenerator
generatedName := generator.GenerateName(base)
if excess := len(generatedName) - maxStatefulSetNameLength; excess > 0 {
base = base[:len(base)-excess-1] // take extra char off to make space for hyphen
base = base + "-" // re-instate hyphen
for {
generatedName := generator.GenerateName(base)
excess := len(generatedName) - maxStatefulSetNameLength
if excess <= 0 {
return base
}
base = base[:len(base)-1-excess] // cut off the excess chars
base = base + "-" // re-instate the dash
}
return base
}
func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig) (*corev1.Service, error) {
@@ -230,7 +246,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
return createOrUpdate(ctx, a.Client, a.operatorNamespace, hsvc, func(svc *corev1.Service) { svc.Spec = hsvc.Spec })
}
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service) (string, error) {
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service) (string, string, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
// Hardcode a -0 suffix so that in future, if we support
@@ -246,22 +262,25 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
logger.Debugf("secret %s/%s already exists", secret.GetNamespace(), secret.GetName())
orig = secret.DeepCopy()
} else if !apierrors.IsNotFound(err) {
return "", err
return "", "", err
}
var (
authKey, hash string
)
if orig == nil {
// Secret doesn't exist yet, create one. Initially it contains
// only the Tailscale authkey, but once Tailscale starts it'll
// also store the daemon state.
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, stsC.ChildResourceLabels)
if err != nil {
return "", err
return "", "", err
}
if sts != nil {
// StatefulSet exists, so we have already created the secret.
// If the secret is missing, they should delete the StatefulSet.
logger.Errorf("Tailscale proxy secret doesn't exist, but the corresponding StatefulSet %s/%s already does. Something is wrong, please delete the StatefulSet.", sts.GetNamespace(), sts.GetName())
return "", nil
return "", "", nil
}
// Create API Key secret which is going to be used by the statefulset
// to authenticate with Tailscale.
@@ -270,30 +289,42 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
if len(tags) == 0 {
tags = a.defaultTags
}
authKey, err := a.newAuthKey(ctx, tags)
authKey, err = a.newAuthKey(ctx, tags)
if err != nil {
return "", err
return "", "", err
}
}
if !shouldDoTailscaledDeclarativeConfig(stsC) && authKey != "" {
mak.Set(&secret.StringData, "authkey", authKey)
}
if shouldDoTailscaledDeclarativeConfig(stsC) {
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
if err != nil {
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
}
hash = h
mak.Set(&secret.StringData, tailscaledConfigKey, string(confFileBytes))
}
if stsC.ServeConfig != nil {
j, err := json.Marshal(stsC.ServeConfig)
if err != nil {
return "", err
return "", "", err
}
mak.Set(&secret.StringData, "serve-config", string(j))
}
if orig != nil {
logger.Debugf("patching existing state Secret with values %s", secret.Data[tailscaledConfigKey])
if err := a.Patch(ctx, secret, client.MergeFrom(orig)); err != nil {
return "", err
return "", "", err
}
} else {
logger.Debugf("creating new state Secret with authkey %s", secret.Data[tailscaledConfigKey])
if err := a.Create(ctx, secret); err != nil {
return "", err
return "", "", err
}
}
return secret.Name, nil
return secret.Name, hash, nil
}
// DeviceInfo returns the device ID and hostname for the Tailscale device
@@ -321,7 +352,6 @@ func (a *tailscaleSTSReconciler) DeviceInfo(ctx context.Context, childLabels map
return "", "", nil, err
}
}
return id, hostname, ips, nil
}
@@ -349,7 +379,7 @@ var proxyYaml []byte
//go:embed deploy/manifests/userspace-proxy.yaml
var userspaceProxyYaml []byte
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, authKeySecret string) (*appsv1.StatefulSet, error) {
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string) (*appsv1.StatefulSet, error) {
var ss appsv1.StatefulSet
if sts.ServeConfig != nil {
if err := yaml.Unmarshal(userspaceProxyYaml, &ss); err != nil {
@@ -369,30 +399,93 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
}
container := &ss.Spec.Template.Spec.Containers[0]
container.Image = a.proxyImage
ss.ObjectMeta = metav1.ObjectMeta{
Name: headlessSvc.Name,
Namespace: a.operatorNamespace,
Labels: sts.ChildResourceLabels,
}
ss.Spec.ServiceName = headlessSvc.Name
ss.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": sts.ParentResourceUID,
},
}
mak.Set(&ss.Spec.Template.Labels, "app", sts.ParentResourceUID)
for key, val := range sts.ChildResourceLabels {
ss.Spec.Template.Labels[key] = val // sync StatefulSet labels to Pod to make it easier for users to select the Pod
}
// Generic containerboot configuration options.
container.Env = append(container.Env,
corev1.EnvVar{
Name: "TS_KUBE_SECRET",
Value: authKeySecret,
Value: proxySecret,
},
corev1.EnvVar{
)
if !shouldDoTailscaledDeclarativeConfig(sts) {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_HOSTNAME",
Value: sts.Hostname,
})
// containerboot currently doesn't have a way to re-read the hostname/ip as
// it is passed via an environment variable. So we need to restart the
// container when the value changes. We do this by adding an annotation to
// the pod template that contains the last value we set.
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetHostname, sts.Hostname)
}
// Configure containeboot to run tailscaled with a configfile read from the state Secret.
if shouldDoTailscaledDeclarativeConfig(sts) {
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: proxySecret,
Items: []corev1.KeyToPath{{
Key: tailscaledConfigKey,
Path: tailscaledConfigKey,
}},
},
},
})
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
})
container.Env = append(container.Env, corev1.EnvVar{
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
Value: "/etc/tsconfig/tailscaled",
})
}
if a.tsFirewallMode != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_DEBUG_FIREWALL_MODE",
Value: a.tsFirewallMode,
})
}
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
// Ingress/egress proxy configuration options.
if sts.ClusterTargetIP != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_DEST_IP",
Value: sts.ClusterTargetIP,
})
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetClusterIP, sts.ClusterTargetIP)
} else if sts.TailnetTargetIP != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_IP",
Value: sts.TailnetTargetIP,
})
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetTailnetTargetIP, sts.TailnetTargetIP)
} else if sts.TailnetTargetFQDN != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_FQDN",
Value: sts.TailnetTargetFQDN,
})
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetTailnetTargetFQDN, sts.TailnetTargetFQDN)
} else if sts.ServeConfig != nil {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_SERVE_CONFIG",
@@ -407,7 +500,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
Name: "serve-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: authKeySecret,
SecretName: proxySecret,
Items: []corev1.KeyToPath{{
Key: "serve-config",
Path: "serve-config",
@@ -416,49 +509,47 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
},
})
}
if a.tsFirewallMode != "" {
container.Env = append(container.Env, corev1.EnvVar{
Name: "TS_DEBUG_FIREWALL_MODE",
Value: a.tsFirewallMode,
},
)
}
ss.ObjectMeta = metav1.ObjectMeta{
Name: headlessSvc.Name,
Namespace: a.operatorNamespace,
Labels: sts.ChildResourceLabels,
}
ss.Spec.ServiceName = headlessSvc.Name
ss.Spec.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": sts.ParentResourceUID,
},
}
// containerboot currently doesn't have a way to re-read the hostname/ip as
// it is passed via an environment variable. So we need to restart the
// container when the value changes. We do this by adding an annotation to
// the pod template that contains the last value we set.
ss.Spec.Template.Annotations = map[string]string{
podAnnotationLastSetHostname: sts.Hostname,
}
if sts.ClusterTargetIP != "" {
ss.Spec.Template.Annotations[podAnnotationLastSetClusterIP] = sts.ClusterTargetIP
}
if sts.TailnetTargetIP != "" {
ss.Spec.Template.Annotations[podAnnotationLastSetTailnetTargetIP] = sts.TailnetTargetIP
}
if sts.TailnetTargetFQDN != "" {
ss.Spec.Template.Annotations[podAnnotationLastSetTailnetTargetFQDN] = sts.TailnetTargetFQDN
}
ss.Spec.Template.Labels = map[string]string{
"app": sts.ParentResourceUID,
}
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
}
// tailscaledConfig takes a proxy config, a newly generated auth key if
// generated and a Secret with the previous proxy state and auth key and
// produces returns tailscaled configuration and a hash of that configuration.
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret) ([]byte, string, error) {
conf := ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Locked: "false",
Hostname: &stsC.Hostname,
}
if stsC.Connector != nil {
routes, err := netutil.CalcAdvertiseRoutes(stsC.Connector.routes, stsC.Connector.isExitNode)
if err != nil {
return nil, "", fmt.Errorf("error calculating routes: %w", err)
}
conf.AdvertiseRoutes = routes
}
if newAuthkey != "" {
conf.AuthKey = &newAuthkey
} else if oldSecret != nil && len(oldSecret.Data[tailscaledConfigKey]) > 0 { // write to StringData, read from Data as StringData is write-only
origConf := &ipn.ConfigVAlpha{}
if err := json.Unmarshal([]byte(oldSecret.Data[tailscaledConfigKey]), origConf); err != nil {
return nil, "", fmt.Errorf("error unmarshaling previous tailscaled config: %w", err)
}
conf.AuthKey = origConf.AuthKey
}
confFileBytes, err := json.Marshal(conf)
if err != nil {
return nil, "", fmt.Errorf("error marshaling tailscaled config : %w", err)
}
hash, err := hashBytes(confFileBytes)
if err != nil {
return nil, "", fmt.Errorf("error calculating config hash: %w", err)
}
return confFileBytes, hash, nil
}
// ptrObject is a type constraint for pointer types that implement
// client.Object.
type ptrObject[T any] interface {
@@ -466,6 +557,24 @@ type ptrObject[T any] interface {
*T
}
// hashBytes produces a hash for the provided bytes that is the same across
// different invocations of this code. We do not use the
// tailscale.com/deephash.Hash here because that produces a different hash for
// the same value in different tailscale builds. The hash we are producing here
// is used to determine if the container running the Connector Tailscale node
// needs to be restarted. The container does not need restarting when the only
// thing that changed is operator version (the hash is also exposed to users via
// an annotation and might be confusing if it changes without the config having
// changed).
func hashBytes(b []byte) (string, error) {
h := sha256.New()
_, err := h.Write(b)
if err != nil {
return "", fmt.Errorf("error calculating hash: %w", err)
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// createOrUpdate adds obj to the k8s cluster, unless the object already exists,
// in which case update is called to make changes to it. If update is nil, the
// existing object is returned unmodified.
@@ -569,3 +678,10 @@ func nameForService(svc *corev1.Service) (string, error) {
func isValidFirewallMode(m string) bool {
return m == "auto" || m == "nftables" || m == "iptables"
}
// shouldDoTailscaledDeclarativeConfig determines whether the proxy instance
// should be configured to run tailscaled only with a all config opts passed to
// tailscaled.
func shouldDoTailscaledDeclarativeConfig(stsC *tailscaleSTSConfig) bool {
return stsC.Connector != nil
}

View File

@@ -6,6 +6,9 @@
package main
import (
"fmt"
"regexp"
"strings"
"testing"
)
@@ -19,32 +22,20 @@ import (
// https://github.com/kubernetes/kubernetes/blob/v1.28.4/staging/src/k8s.io/apiserver/pkg/storage/names/generate.go#L45.
// https://github.com/kubernetes/kubernetes/pull/116430
func Test_statefulSetNameBase(t *testing.T) {
tests := []struct {
name string
in string
out string
}{
{
name: "43 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb-",
},
{
name: "44 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xbo",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb-",
},
{
name: "42 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9x",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9x-",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := statefulSetNameBase(tt.in); got != tt.out {
t.Errorf("stsNamePrefix(%s) = %q, want %s", tt.in, got, tt.out)
}
})
// Service name lengths can be 1 - 63 chars, be paranoid and test them all.
var b strings.Builder
for b.Len() < 63 {
if _, err := b.WriteString("a"); err != nil {
t.Fatalf("error writing to string builder: %v", err)
}
baseLength := b.Len()
if baseLength > 43 {
baseLength = 43 // currently 43 is the max base length
}
wantsNameR := regexp.MustCompile(`^ts-a{` + fmt.Sprint(baseLength) + `}-$`) // to match a string like ts-aaaa-
gotName := statefulSetNameBase(b.String())
if !wantsNameR.MatchString(gotName) {
t.Fatalf("expected string %s to match regex %s ", gotName, wantsNameR.String()) // fatal rather than error as this test is called 63 times
}
}
}

View File

@@ -0,0 +1,417 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package main
import (
"context"
"encoding/json"
"net/netip"
"strings"
"sync"
"testing"
"time"
"github.com/google/go-cmp/cmp"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/types/ptr"
"tailscale.com/util/mak"
)
// confgOpts contains configuration options for creating cluster resources for
// Tailscale proxies.
type configOpts struct {
stsName string
secretName string
hostname string
namespace string
parentType string
priorityClassName string
firewallMode string
tailnetTargetIP string
tailnetTargetFQDN string
clusterTargetIP string
subnetRoutes string
isExitNode bool
shouldUseDeclarativeConfig bool // tailscaled in proxy should be configured using config file
confFileHash string
}
func expectedSTS(opts configOpts) *appsv1.StatefulSet {
tsContainer := corev1.Container{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []corev1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
},
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{"NET_ADMIN"},
},
},
ImagePullPolicy: "Always",
}
annots := make(map[string]string)
var volumes []corev1.Volume
if opts.shouldUseDeclarativeConfig {
volumes = []corev1.Volume{
{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: opts.secretName,
Items: []corev1.KeyToPath{
{
Key: "tailscaled",
Path: "tailscaled",
},
},
},
},
},
}
tsContainer.VolumeMounts = []corev1.VolumeMount{{
Name: "tailscaledconfig",
ReadOnly: true,
MountPath: "/etc/tsconfig",
}}
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
Value: "/etc/tsconfig/tailscaled",
})
annots["tailscale.com/operator-last-set-config-file-hash"] = opts.confFileHash
} else {
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{Name: "TS_HOSTNAME", Value: opts.hostname})
annots["tailscale.com/operator-last-set-hostname"] = opts.hostname
}
if opts.firewallMode != "" {
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "TS_DEBUG_FIREWALL_MODE",
Value: opts.firewallMode,
})
}
if opts.tailnetTargetIP != "" {
annots["tailscale.com/operator-last-set-ts-tailnet-target-ip"] = opts.tailnetTargetIP
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_IP",
Value: opts.tailnetTargetIP,
})
} else if opts.tailnetTargetFQDN != "" {
annots["tailscale.com/operator-last-set-ts-tailnet-target-fqdn"] = opts.tailnetTargetFQDN
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "TS_TAILNET_TARGET_FQDN",
Value: opts.tailnetTargetFQDN,
})
} else if opts.clusterTargetIP != "" {
tsContainer.Env = append(tsContainer.Env, corev1.EnvVar{
Name: "TS_DEST_IP",
Value: opts.clusterTargetIP,
})
annots["tailscale.com/operator-last-set-cluster-ip"] = opts.clusterTargetIP
}
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
APIVersion: "apps/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: opts.stsName,
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": opts.namespace,
"tailscale.com/parent-resource-type": opts.parentType,
},
},
Spec: appsv1.StatefulSetSpec{
Replicas: ptr.To[int32](1),
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "1234-UID"},
},
ServiceName: opts.stsName,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: annots,
DeletionGracePeriodSeconds: ptr.To[int64](10),
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": opts.namespace,
"tailscale.com/parent-resource-type": opts.parentType,
"app": "1234-UID",
},
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",
PriorityClassName: opts.priorityClassName,
InitContainers: []corev1.Container{
{
Name: "sysctler",
Image: "tailscale/tailscale",
Command: []string{"/bin/sh"},
Args: []string{"-c", "sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1"},
SecurityContext: &corev1.SecurityContext{
Privileged: ptr.To(true),
},
},
},
Containers: []corev1.Container{tsContainer},
Volumes: volumes,
},
},
},
}
}
func expectedHeadlessService(name string) *corev1.Service {
return &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
GenerateName: "ts-test-",
Namespace: "operator-ns",
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": "default",
"tailscale.com/parent-resource-type": "svc",
},
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "1234-UID",
},
ClusterIP: "None",
},
}
}
func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
t.Helper()
labels := map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-type": opts.parentType,
}
s := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: opts.secretName,
Namespace: "operator-ns",
},
}
if !opts.shouldUseDeclarativeConfig {
mak.Set(&s.StringData, "authkey", "secret-authkey")
labels["tailscale.com/parent-resource-ns"] = opts.namespace
} else {
conf := &ipn.ConfigVAlpha{
Version: "alpha0",
AcceptDNS: "false",
Hostname: &opts.hostname,
Locked: "false",
AuthKey: ptr.To("secret-authkey"),
}
var routes []netip.Prefix
if opts.subnetRoutes != "" || opts.isExitNode {
r := opts.subnetRoutes
if opts.isExitNode {
r = "0.0.0.0/0,::/0," + r
}
for _, rr := range strings.Split(r, ",") {
prefix, err := netip.ParsePrefix(rr)
if err != nil {
t.Fatal(err)
}
routes = append(routes, prefix)
}
}
conf.AdvertiseRoutes = routes
b, err := json.Marshal(conf)
if err != nil {
t.Fatalf("error marshalling tailscaled config")
}
mak.Set(&s.StringData, "tailscaled", string(b))
labels["tailscale.com/parent-resource-ns"] = "" // Connector is cluster scoped
}
s.Labels = labels
return s
}
func findGenName(t *testing.T, client client.Client, ns, name, typ string) (full, noSuffix string) {
t.Helper()
labels := map[string]string{
LabelManaged: "true",
LabelParentName: name,
LabelParentNamespace: ns,
LabelParentType: typ,
}
s, err := getSingleObject[corev1.Secret](context.Background(), client, "operator-ns", labels)
if err != nil {
t.Fatalf("finding secret for %q: %v", name, err)
}
if s == nil {
t.Fatalf("no secret found for %q %s %+#v", name, ns, labels)
}
return s.GetName(), strings.TrimSuffix(s.GetName(), "-0")
}
func mustCreate(t *testing.T, client client.Client, obj client.Object) {
t.Helper()
if err := client.Create(context.Background(), obj); err != nil {
t.Fatalf("creating %q: %v", obj.GetName(), err)
}
}
func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Status().Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
t.Helper()
got := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: want.GetName(),
Namespace: want.GetNamespace(),
}, got); err != nil {
t.Fatalf("getting %q: %v", want.GetName(), err)
}
// The resource version changes eagerly whenever the operator does even a
// no-op update. Asserting a specific value leads to overly brittle tests,
// so just remove it from both got and want.
got.SetResourceVersion("")
want.SetResourceVersion("")
if diff := cmp.Diff(got, want); diff != "" {
t.Fatalf("unexpected object (-got +want):\n%s", diff)
}
}
func expectMissing[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); !apierrors.IsNotFound(err) {
t.Fatalf("object %s/%s unexpectedly present, wanted missing", ns, name)
}
}
func expectReconciled(t *testing.T, sr reconcile.Reconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: ns,
Name: name,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.Requeue {
t.Fatalf("unexpected immediate requeue")
}
if res.RequeueAfter != 0 {
t.Fatalf("unexpected timed requeue (%v)", res.RequeueAfter)
}
}
func expectRequeue(t *testing.T, sr reconcile.Reconciler, ns, name string) {
t.Helper()
req := reconcile.Request{
NamespacedName: types.NamespacedName{
Name: name,
Namespace: ns,
},
}
res, err := sr.Reconcile(context.Background(), req)
if err != nil {
t.Fatalf("Reconcile: unexpected error: %v", err)
}
if res.RequeueAfter == 0 {
t.Fatalf("expected timed requeue, got success")
}
}
type fakeTSClient struct {
sync.Mutex
keyRequests []tailscale.KeyCapabilities
deleted []string
}
func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabilities) (string, *tailscale.Key, error) {
c.Lock()
defer c.Unlock()
c.keyRequests = append(c.keyRequests, caps)
k := &tailscale.Key{
ID: "key",
Created: time.Now(),
Capabilities: caps,
}
return "secret-authkey", k, nil
}
func (c *fakeTSClient) DeleteDevice(ctx context.Context, deviceID string) error {
c.Lock()
defer c.Unlock()
c.deleted = append(c.deleted, deviceID)
return nil
}
func (c *fakeTSClient) KeyRequests() []tailscale.KeyCapabilities {
c.Lock()
defer c.Unlock()
return c.keyRequests
}
func (c *fakeTSClient) Deleted() []string {
c.Lock()
defer c.Unlock()
return c.deleted
}

View File

@@ -25,6 +25,7 @@
package main
import (
"cmp"
"encoding/base64"
"encoding/json"
"flag"
@@ -45,7 +46,6 @@ import (
"github.com/go-json-experiment/json/jsontext"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/cmpx"
"tailscale.com/util/must"
)
@@ -155,7 +155,7 @@ func printMessage(msg message) {
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) int {
nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes
ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes
return cmpx.Compare(ny, nx)
return cmp.Compare(ny, nx)
})
var sum netlogtype.Counts
for _, cc := range traffic {

188
cmd/stund/depaware.txt Normal file
View File

@@ -0,0 +1,188 @@
tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depaware)
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/google/uuid from tailscale.com/tsweb
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
💣 go4.org/mem from tailscale.com/metrics+
go4.org/netipx from tailscale.com/net/tsaddr
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
google.golang.org/protobuf/encoding/prototext from github.com/prometheus/common/expfmt+
google.golang.org/protobuf/encoding/protowire from google.golang.org/protobuf/encoding/protodelim+
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/proto from github.com/prometheus/client_golang/prometheus+
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/prometheus/client_model/go+
google.golang.org/protobuf/reflect/protoregistry from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/runtime/protoiface from google.golang.org/protobuf/internal/impl+
google.golang.org/protobuf/runtime/protoimpl from github.com/prometheus/client_model/go+
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
tailscale.com from tailscale.com/version
tailscale.com/envknob from tailscale.com/tsweb+
tailscale.com/metrics from tailscale.com/net/stunserver+
tailscale.com/net/netaddr from tailscale.com/net/tsaddr
tailscale.com/net/stun from tailscale.com/net/stunserver
tailscale.com/net/stunserver from tailscale.com/cmd/stund
tailscale.com/net/tsaddr from tailscale.com/tsweb
tailscale.com/tailcfg from tailscale.com/version
tailscale.com/tsweb from tailscale.com/cmd/stund
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/ipproto from tailscale.com/tailcfg
tailscale.com/types/key from tailscale.com/tailcfg
tailscale.com/types/lazy from tailscale.com/version+
tailscale.com/types/logger from tailscale.com/tsweb
tailscale.com/types/opt from tailscale.com/envknob+
tailscale.com/types/ptr from tailscale.com/tailcfg
tailscale.com/types/structs from tailscale.com/tailcfg+
tailscale.com/types/tkatype from tailscale.com/tailcfg+
tailscale.com/types/views from tailscale.com/net/tsaddr+
tailscale.com/util/cmpx from tailscale.com/tailcfg+
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/lineread from tailscale.com/version/distro
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/slicesx from tailscale.com/tailcfg
tailscale.com/util/vizerror from tailscale.com/tailcfg+
tailscale.com/version from tailscale.com/envknob+
tailscale.com/version/distro from tailscale.com/envknob
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
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 golang.org/x/crypto/nacl/box+
golang.org/x/crypto/hkdf from crypto/tls
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/net/dns/dnsmessage from net
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
D golang.org/x/net/route from net
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/prometheus/procfs+
W golang.org/x/sys/windows from github.com/prometheus/client_golang/prometheus
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
cmp from slices
compress/flate from compress/gzip
compress/gzip from google.golang.org/protobuf/internal/impl+
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdh+
crypto/aes from crypto/ecdsa+
crypto/cipher from crypto/aes+
crypto/des from crypto/tls+
crypto/dsa from crypto/x509
crypto/ecdh from crypto/ecdsa+
crypto/ecdsa from crypto/tls+
crypto/ed25519 from crypto/tls+
crypto/elliptic from crypto/ecdsa+
crypto/hmac from crypto/tls+
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/sha512 from crypto/ecdsa+
crypto/subtle from crypto/aes+
crypto/tls from net/http+
crypto/x509 from crypto/tls
crypto/x509/pkix from crypto/x509
database/sql/driver from github.com/google/uuid
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
encoding/json from expvar+
encoding/pem from crypto/tls+
errors from bufio+
expvar from github.com/prometheus/client_golang/prometheus+
flag from tailscale.com/cmd/stund
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
io from bufio+
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
log from expvar+
log/internal from log
maps from tailscale.com/tailcfg+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from math/big+
mime from github.com/prometheus/common/expfmt+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
net from crypto/tls+
net/http from expvar+
net/http/httptrace from net/http
net/http/internal from net/http
net/http/pprof from tailscale.com/tsweb+
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/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/debug from github.com/prometheus/client_golang/prometheus+
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
slices from tailscale.com/metrics+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
time from compress/gzip+
unicode from bytes+
unicode/utf16 from crypto/x509+
unicode/utf8 from bufio+

48
cmd/stund/stund.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The stund binary is a standalone STUN server.
package main
import (
"context"
"flag"
"io"
"log"
"net/http"
"os/signal"
"syscall"
"tailscale.com/net/stunserver"
"tailscale.com/tsweb"
)
var (
stunAddr = flag.String("stun", ":3478", "UDP address on which to start the STUN server")
httpAddr = flag.String("http", ":3479", "address on which to start the debug http server")
)
func main() {
flag.Parse()
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
log.Printf("HTTP server listening on %s", *httpAddr)
go http.ListenAndServe(*httpAddr, mux())
s := stunserver.New(ctx)
if err := s.ListenAndServe(*stunAddr); err != nil {
log.Fatal(err)
}
}
func mux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "<h1>stund</h1><a href=/debug>/debug</a>")
})
debug := tsweb.Debugger(mux)
debug.KV("stun_addr", *stunAddr)
return mux
}

View File

@@ -558,7 +558,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
AllowSingleHosts: true,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
@@ -575,7 +574,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
@@ -594,7 +592,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NetfilterMode: preftype.NetfilterOn,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
@@ -684,7 +681,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NoSNAT: true,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
@@ -701,7 +697,6 @@ func TestPrefsFromUpArgs(t *testing.T) {
NoSNAT: true,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
@@ -720,7 +715,24 @@ func TestPrefsFromUpArgs(t *testing.T) {
},
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
},
},
},
{
name: "via_route_good_16_bit",
goos: "linux",
args: upArgsT{
advertiseRoutes: "fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112",
netfilterMode: "off",
},
want: &ipn.Prefs{
WantRunning: true,
NoSNAT: true,
AdvertiseRoutes: []netip.Prefix{
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
},
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
},
},
},
@@ -740,7 +752,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
advertiseRoutes: "fd7a:115c:a1e0:b1a:1234:5678::/112",
netfilterMode: "off",
},
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xff or less",
wantErr: "route fd7a:115c:a1e0:b1a:1234:5678::/112 contains invalid site ID 12345678; must be 0xffff or less",
},
}
for _, tt := range tests {

View File

@@ -258,7 +258,7 @@ var debugCmd = &ffcli.Command{
{
Name: "portmap",
Exec: debugPortmap,
ShortHelp: "run portmap debugging debugging",
ShortHelp: "run portmap debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
@@ -274,6 +274,16 @@ var debugCmd = &ffcli.Command{
Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes",
},
{
Name: "dial-types",
Exec: runDebugDialTypes,
ShortHelp: "prints debug information about connecting to a given host or IP",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("dial-types")
fs.StringVar(&debugDialTypesArgs.network, "network", "tcp", `network type to dial ("tcp", "udp", etc.)`)
return fs
})(),
},
},
}
@@ -683,8 +693,8 @@ func runVia(ctx context.Context, args []string) error {
if err != nil {
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
}
if siteID > 0xff {
return fmt.Errorf("site-id values over 255 are currently reserved")
if siteID > 0xffff {
return fmt.Errorf("site-id values over 65535 are currently reserved")
}
ipp, err := netip.ParsePrefix(args[1])
if err != nil {
@@ -1015,3 +1025,61 @@ func debugControlKnobs(ctx context.Context, args []string) error {
e.Encode(v)
return nil
}
var debugDialTypesArgs struct {
network string
}
func runDebugDialTypes(ctx context.Context, args []string) error {
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
if len(args) != 2 || args[0] == "" || args[1] == "" {
return errors.New("usage: dial-types <hostname-or-IP> <port>")
}
port, err := strconv.ParseUint(args[1], 10, 16)
if err != nil {
return fmt.Errorf("invalid port %q: %w", args[1], err)
}
hostOrIP := args[0]
ip, _, err := tailscaleIPFromArg(ctx, hostOrIP)
if err != nil {
return err
}
if ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
qparams := make(url.Values)
qparams.Set("ip", ip)
qparams.Set("port", strconv.FormatUint(port, 10))
qparams.Set("network", debugDialTypesArgs.network)
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/debug-dial-types?"+qparams.Encode(), nil)
if err != nil {
return err
}
resp, err := localClient.DoLocalRequest(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Printf("%s", body)
return nil
}

View File

@@ -4,6 +4,7 @@
package cli
import (
"cmp"
"context"
"errors"
"flag"
@@ -17,12 +18,13 @@ import (
xmaps "golang.org/x/exp/maps"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
)
var exitNodeCmd = &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes",
LongHelp: "Show machines on your tailnet configured as exit nodes",
Subcommands: []*ffcli.Command{
{
Name: "list",
@@ -228,7 +230,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
// by location.Priority, in order of highest priority.
func sortPeersByPriority(peers []*ipnstate.PeerStatus) {
slices.SortStableFunc(peers, func(a, b *ipnstate.PeerStatus) int {
return cmpx.Compare(b.Location.Priority, a.Location.Priority)
return cmp.Compare(b.Location.Priority, a.Location.Priority)
})
}

View File

@@ -85,7 +85,7 @@ func runNetcheck(ctx context.Context, args []string) error {
}
for {
t0 := time.Now()
report, err := c.GetReport(ctx, dm)
report, err := c.GetReport(ctx, dm, nil)
d := time.Since(t0)
if netcheckArgs.verbose {
c.Logf("GetReport took %v; err=%v", d.Round(time.Millisecond), err)

View File

@@ -93,14 +93,6 @@ var infoMap = map[serveMode]commandInfo{
},
}
func buildShortUsage(subcmd string) string {
return strings.Join([]string{
subcmd + " [flags] <target> [off]",
subcmd + " status [--json]",
subcmd + " reset",
}, "\n ")
}
// errHelpFunc is standard error text that prompts users to
// run `$subcmd --help` for information on how to use serve.
var errHelpFunc = func(m serveMode) error {
@@ -280,7 +272,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
// if foreground mode, create a WatchIPNBus session
// and use the nested config for all following operations
// TODO(marwan-at-work): nested-config validations should happen here or previous to this point.
watcher, err = e.lc.WatchIPNBus(ctx, ipn.NotifyInitialState)
watcher, err = e.lc.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyNoPrivateKeys)
if err != nil {
return err
}

View File

@@ -27,7 +27,6 @@ func TestServeDevConfigMutations(t *testing.T) {
command []string // serve args; nil means no command to run (only reset)
want *ipn.ServeConfig // non-nil means we want a save of this value
wantErr func(error) (badErrMsg string) // nil means no error is wanted
before func(t *testing.T)
}
// group is a group of steps that share the same
@@ -1224,14 +1223,6 @@ func TestMessageForPort(t *testing.T) {
}
}
func unindent(s string) string {
lines := strings.Split(s, "\n")
for i, line := range lines {
lines[i] = strings.TrimSpace(line)
}
return strings.Join(lines, "\n")
}
func TestIsLegacyInvocation(t *testing.T) {
tests := []struct {
subcmd serveMode

View File

@@ -12,11 +12,13 @@ import (
"os/exec"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/web"
"tailscale.com/clientupdate"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/net/tsaddr"
"tailscale.com/safesocket"
"tailscale.com/types/opt"
"tailscale.com/types/views"
"tailscale.com/version"
)
@@ -116,7 +118,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
ForceDaemon: setArgs.forceDaemon,
AutoUpdate: ipn.AutoUpdatePrefs{
Check: setArgs.updateCheck,
Apply: setArgs.updateApply,
Apply: opt.NewBool(setArgs.updateApply),
},
AppConnector: ipn.AppConnectorPrefs{
Advertise: setArgs.advertiseConnector,
@@ -172,7 +174,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
// does not use clientupdate.
if version.IsMacSysExt() {
apply := "0"
if maskedPrefs.AutoUpdate.Apply {
if maskedPrefs.AutoUpdate.Apply.EqualBool(true) {
apply = "1"
}
out, err := exec.Command("defaults", "write", "io.tailscale.ipn.macsys", "SUAutomaticallyUpdate", apply).CombinedOutput()
@@ -192,7 +194,15 @@ func runSet(ctx context.Context, args []string) (retErr error) {
}
_, err = localClient.EditPrefs(ctx, maskedPrefs)
return err
if err != nil {
return err
}
if setArgs.runWebClient && len(st.TailscaleIPs) > 0 {
printf("\nWeb interface now running at %s:%d", st.TailscaleIPs[0], web.ListenPort)
}
return nil
}
// calcAdvertiseRoutesForSet returns the new value for Prefs.AdvertiseRoutes based on the

View File

@@ -1044,18 +1044,6 @@ func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netip.Addr) {
return
}
func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
for _, ps := range st.Peer {
if ps.PrimaryRoutes == nil {
continue
}
if ps.PrimaryRoutes.Len() > 0 {
return true
}
}
return false
}
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.

View File

@@ -18,7 +18,7 @@ import (
var whoisCmd = &ffcli.Command{
Name: "whois",
ShortUsage: "whois [--json] [ip|ip:port]",
ShortUsage: "whois [--json] ip[:port]",
ShortHelp: "Show the machine and user associated with a Tailscale IP (v4 or v6)",
LongHelp: strings.TrimSpace(`
'tailscale whois' shows the machine and user associated with a Tailscale IP (v4 or v6).

View File

@@ -26,7 +26,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress/flate from nhooyr.io/websocket
💣 github.com/mattn/go-colorable from tailscale.com/cmd/tailscale/cli
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
@@ -38,7 +37,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
github.com/peterbourgon/ff/v3/internal from github.com/peterbourgon/ff/v3
github.com/pkg/errors from github.com/gorilla/csrf
github.com/skip2/go-qrcode from tailscale.com/cmd/tailscale/cli
github.com/skip2/go-qrcode/bitset from github.com/skip2/go-qrcode+
github.com/skip2/go-qrcode/reedsolomon from github.com/skip2/go-qrcode
@@ -63,12 +61,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 go4.org/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/util from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
sigs.k8s.io/yaml from tailscale.com/cmd/tailscale/cli
sigs.k8s.io/yaml/goyaml.v2 from sigs.k8s.io/yaml
software.sslmate.com/src/go-pkcs12 from tailscale.com/cmd/tailscale/cli
software.sslmate.com/src/go-pkcs12/internal/rc2 from software.sslmate.com/src/go-pkcs12
tailscale.com from tailscale.com/version
@@ -143,6 +142,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/ctxkey from tailscale.com/types/logger
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web
@@ -157,6 +157,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/syspolicy from tailscale.com/ipn
tailscale.com/util/testenv from tailscale.com/cmd/tailscale/cli
tailscale.com/util/truncate from tailscale.com/cmd/tailscale/cli
tailscale.com/util/vizerror from tailscale.com/types/ipproto+
@@ -210,7 +211,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
archive/tar from tailscale.com/clientupdate
bufio from compress/flate+
bytes from bufio+
cmp from slices
cmp from slices+
compress/flate from compress/gzip+
compress/gzip from net/http+
compress/zlib from image/png+
@@ -266,7 +267,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
image/png from github.com/skip2/go-qrcode
io from bufio+
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
log/internal from log
maps from tailscale.com/types/views+
@@ -296,6 +297,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
regexp from github.com/tailscale/goupnp/httpu+
regexp/syntax from regexp
runtime/debug from tailscale.com/util/singleflight+
runtime/trace from testing
slices from tailscale.com/cmd/tailscale/cli+
sort from compress/flate+
strconv from compress/flate+
@@ -303,6 +305,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
testing from tailscale.com/util/syspolicy
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
text/template from html/template
text/template/parse from html/template+

View File

@@ -6,11 +6,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
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
@@ -30,7 +30,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
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/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
@@ -41,6 +43,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
L github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/internal/presigned-url from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/ssm from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm
@@ -55,6 +58,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
L github.com/aws/smithy-go/auth from github.com/aws/aws-sdk-go-v2/internal/auth+
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/context from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
@@ -67,6 +71,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/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+
@@ -108,7 +113,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress from github.com/klauspost/compress/zstd
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd+
@@ -130,7 +134,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
github.com/pkg/errors from github.com/gorilla/csrf
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf
@@ -141,8 +144,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
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+
@@ -191,13 +193,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
💣 gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/buffer+
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/link/channel from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
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+
gvisor.dev/gvisor/pkg/tcpip/network/internal/multicast from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
@@ -220,6 +222,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 inet.af/wf from tailscale.com/wf
nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/util from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
tailscale.com from tailscale.com/version
tailscale.com/appc from tailscale.com/ipn/ipnlocal
@@ -283,7 +286,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/netknob from tailscale.com/net/netns+
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
@@ -345,9 +348,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
tailscale.com/util/ctxkey from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/execqueue from tailscale.com/control/controlclient+
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth+
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
@@ -403,11 +408,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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+
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
golang.org/x/crypto/hkdf from crypto/tls+
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/golang-x-crypto/ssh+
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+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
@@ -442,7 +446,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
archive/tar from tailscale.com/clientupdate
bufio from compress/flate+
bytes from bufio+
cmp from slices
cmp from slices+
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
W compress/zlib from debug/pe
@@ -491,7 +495,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
hash from crypto+
hash/adler32 from compress/zlib+
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnlocal+
html/template from github.com/gorilla/csrf

View File

@@ -27,7 +27,6 @@ func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
} else {
logf("%s Taildrop: using %v", dg, path)
lb.SetDirectFileRoot(path)
lb.SetDirectFileDoFinalRename(true)
}
}

View File

@@ -13,6 +13,7 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"errors"
"expvar"
"flag"
"fmt"
"log"
@@ -324,7 +325,7 @@ func ipnServerOpts() (o serverOptions) {
var logPol *logpolicy.Policy
var debugMux *http.ServeMux
func run() error {
func run() (err error) {
var logf logger.Logf = log.Printf
sys := new(tsd.System)
@@ -332,7 +333,6 @@ func run() error {
// Parse config, if specified, to fail early if it's invalid.
var conf *conffile.Config
if args.confFile != "" {
var err error
conf, err = conffile.Load(args.confFile)
if err != nil {
return fmt.Errorf("error reading config file: %w", err)
@@ -340,13 +340,17 @@ func run() error {
sys.InitialConfig = conf
}
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
var netMon *netmon.Monitor
isWinSvc := isWindowsService()
if !isWinSvc {
netMon, err = netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}
sys.Set(netMon)
}
sys.Set(netMon)
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
pol.SetVerbosityLevel(args.verbose)
@@ -362,7 +366,7 @@ func run() error {
log.Printf("Error reading environment config: %v", err)
}
if isWindowsService() {
if isWinSvc {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
if err := runWindowsService(pol); err != nil {
@@ -508,7 +512,13 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
// Note: don't just return ns.DialContextTCP or we'll
// return an interface containing a nil pointer.
tcpConn, err := ns.DialContextTCP(ctx, dst)
if err != nil {
return nil, err
}
return tcpConn, nil
}
}
if socksListener != nil || httpProxyListener != nil {
@@ -720,7 +730,7 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
return netstack.Create(logf,
ret, err := netstack.Create(logf,
sys.Tun.Get(),
sys.Engine.Get(),
sys.MagicSock.Get(),
@@ -728,6 +738,14 @@ func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
sys.DNSManager.Get(),
sys.ProxyMapper(),
)
if err != nil {
return nil, err
}
// Only register debug info if we have a debug mux
if debugMux != nil {
expvar.Publish("netstack", ret.ExpVar())
}
return ret, nil
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -12,22 +12,6 @@ import (
"testing"
)
// defaultTestArgs contains the default values for all flags in the testing
// package. It is used to reset the flag values in testwrapper tests to allow
// parsing the flags again.
var defaultTestArgs map[string]string
// initDefaultTestArgs initializes defaultTestArgs.
func initDefaultTestArgs() {
if defaultTestArgs != nil {
return
}
defaultTestArgs = make(map[string]string)
flag.CommandLine.VisitAll(func(f *flag.Flag) {
defaultTestArgs[f.Name] = f.DefValue
})
}
// registerTestFlags registers all flags from the testing package with the
// provided flag set. It does so by calling testing.Init() and then iterating
// over all flags registered on flag.CommandLine.

View File

@@ -83,26 +83,6 @@ func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
return json.Marshal(metadata)
}
func cleanDist() error {
log.Printf("Cleaning %s...\n", *distDir)
files, err := os.ReadDir(*distDir)
if err != nil {
if os.IsNotExist(err) {
return os.MkdirAll(*distDir, 0755)
}
return err
}
for _, file := range files {
if file.Name() != "placeholder" {
if err := os.Remove(filepath.Join(*distDir, file.Name())); err != nil {
return err
}
}
}
return nil
}
func precompressDist(fastCompression bool) error {
log.Printf("Pre-compressing files in %s/...\n", *distDir)
return precompress.PrecompressDir(*distDir, precompress.Options{

View File

@@ -7,7 +7,6 @@ import (
"bufio"
"bytes"
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io"
@@ -302,32 +301,6 @@ func TestConnMemoryOverhead(t *testing.T) {
}
}
// mkConns creates synthetic Noise Conns wrapping the given net.Conns.
// This function is for testing just the Conn transport logic without
// having to muck about with Noise handshakes.
func mkConns(s1, s2 net.Conn) (*Conn, *Conn) {
var k1, k2 [chp.KeySize]byte
if _, err := rand.Read(k1[:]); err != nil {
panic(err)
}
if _, err := rand.Read(k2[:]); err != nil {
panic(err)
}
ret1 := &Conn{
conn: s1,
tx: txState{cipher: newCHP(k1)},
rx: rxState{cipher: newCHP(k2)},
}
ret2 := &Conn{
conn: s2,
tx: txState{cipher: newCHP(k2)},
rx: rxState{cipher: newCHP(k1)},
}
return ret1, ret2
}
type readSink struct {
r io.Reader

View File

@@ -32,7 +32,6 @@ import (
"encoding/binary"
"hash"
"io"
"math"
"golang.org/x/crypto/blake2s"
"golang.org/x/crypto/chacha20poly1305"
@@ -105,10 +104,6 @@ var minNonce = uint32(0)
* UTILITY FUNCTIONS *
* ---------------------------------------------------------------- */
func getPublicKey(kp *keypair) [32]byte {
return kp.public_key
}
func isEmptyKey(k [32]byte) bool {
return subtle.ConstantTimeCompare(k[:], emptyKey[:]) == 1
}
@@ -162,12 +157,6 @@ func generateKeypair() keypair {
return generateKeypair()
}
func generatePublicKey(private_key [32]byte) [32]byte {
var public_key [32]byte
curve25519.ScalarBaseMult(&public_key, &private_key)
return public_key
}
func encrypt(k [32]byte, n uint32, ad []byte, plaintext []byte) []byte {
var nonce [12]byte
var ciphertext []byte
@@ -246,12 +235,6 @@ func decryptWithAd(cs *cipherstate, ad []byte, ciphertext []byte) (*cipherstate,
return cs, plaintext, valid
}
func reKey(cs *cipherstate) *cipherstate {
e := encrypt(cs.k, math.MaxUint32, []byte{}, emptyKey[:])
copy(cs.k[:], e)
return cs
}
/* SymmetricState */
func initializeSymmetric(protocolName []byte) symmetricstate {
@@ -273,19 +256,6 @@ func mixHash(ss *symmetricstate, data []byte) *symmetricstate {
return ss
}
func mixKeyAndHash(ss *symmetricstate, ikm [32]byte) *symmetricstate {
var tempH [32]byte
var tempK [32]byte
ss.ck, tempH, tempK = getHkdf(ss.ck, ikm[:])
ss = mixHash(ss, tempH[:])
ss.cs = initializeKey(tempK)
return ss
}
func getHandshakeHash(ss *symmetricstate) [32]byte {
return ss.h
}
func encryptAndHash(ss *symmetricstate, plaintext []byte) (*symmetricstate, []byte) {
var ciphertext []byte
if hasKey(&ss.cs) {
@@ -471,5 +441,3 @@ func RecvMessage(session *noisesession, message *messagebuffer) (*noisesession,
session.mc = session.mc + 1
return session, plaintext, valid
}
func main() {}

View File

@@ -22,6 +22,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/util/execqueue"
)
type LoginGoal struct {
@@ -118,7 +119,7 @@ type Auto struct {
closed bool
updateCh chan struct{} // readable when we should inform the server of a change
observer Observer // called to update Client status; always non-nil
observerQueue execQueue
observerQueue execqueue.ExecQueue
unregisterHealthWatch func()
@@ -252,14 +253,6 @@ func (c *Auto) updateControl() {
}
}
// cancelAuthCtx cancels the existing auth goroutine's context
// & creates a new one, causing it to restart.
func (c *Auto) cancelAuthCtx() {
c.mu.Lock()
defer c.mu.Unlock()
c.cancelAuthCtxLocked()
}
// cancelAuthCtxLocked is like cancelAuthCtx, but assumes the caller holds c.mu.
func (c *Auto) cancelAuthCtxLocked() {
if c.authCancel != nil {
@@ -271,14 +264,6 @@ func (c *Auto) cancelAuthCtxLocked() {
}
}
// cancelMapCtx cancels the context for the existing mapPoll and liteUpdates
// goroutines and creates a new one, causing them to restart.
func (c *Auto) cancelMapCtx() {
c.mu.Lock()
defer c.mu.Unlock()
c.cancelMapCtxLocked()
}
// cancelMapCtxLocked is like cancelMapCtx, but assumes the caller holds c.mu.
func (c *Auto) cancelMapCtxLocked() {
if c.mapCancel != nil {
@@ -691,7 +676,7 @@ func (c *Auto) Shutdown() {
direct := c.direct
if !closed {
c.closed = true
c.observerQueue.shutdown()
c.observerQueue.Shutdown()
c.cancelAuthCtxLocked()
c.cancelMapCtxLocked()
for _, w := range c.unpauseWaiters {
@@ -712,7 +697,7 @@ func (c *Auto) Shutdown() {
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
c.observerQueue.wait(ctx)
c.observerQueue.Wait(ctx)
c.logf("Client.Shutdown done.")
}
}
@@ -753,95 +738,3 @@ func (c *Auto) DoNoiseRequest(req *http.Request) (*http.Response, error) {
func (c *Auto) GetSingleUseNoiseRoundTripper(ctx context.Context) (http.RoundTripper, *tailcfg.EarlyNoise, error) {
return c.direct.GetSingleUseNoiseRoundTripper(ctx)
}
type execQueue struct {
mu sync.Mutex
closed bool
inFlight bool // whether a goroutine is running q.run
doneWaiter chan struct{} // non-nil if waiter is waiting, then closed
queue []func()
}
func (q *execQueue) Add(f func()) {
q.mu.Lock()
defer q.mu.Unlock()
if q.closed {
return
}
if q.inFlight {
q.queue = append(q.queue, f)
} else {
q.inFlight = true
go q.run(f)
}
}
// RunSync waits for the queue to be drained and then synchronously runs f.
// It returns an error if the queue is closed before f is run or ctx expires.
func (q *execQueue) RunSync(ctx context.Context, f func()) error {
for {
if err := q.wait(ctx); err != nil {
return err
}
q.mu.Lock()
if q.inFlight {
q.mu.Unlock()
continue
}
defer q.mu.Unlock()
if q.closed {
return errors.New("closed")
}
f()
return nil
}
}
func (q *execQueue) run(f func()) {
f()
q.mu.Lock()
for len(q.queue) > 0 && !q.closed {
f := q.queue[0]
q.queue[0] = nil
q.queue = q.queue[1:]
q.mu.Unlock()
f()
q.mu.Lock()
}
q.inFlight = false
q.queue = nil
if q.doneWaiter != nil {
close(q.doneWaiter)
q.doneWaiter = nil
}
q.mu.Unlock()
}
func (q *execQueue) shutdown() {
q.mu.Lock()
defer q.mu.Unlock()
q.closed = true
}
// wait waits for the queue to be empty.
func (q *execQueue) wait(ctx context.Context) error {
q.mu.Lock()
waitCh := q.doneWaiter
if q.inFlight && waitCh == nil {
waitCh = make(chan struct{})
q.doneWaiter = waitCh
}
q.mu.Unlock()
if waitCh == nil {
return nil
}
select {
case <-waitCh:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -61,23 +61,24 @@ import (
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
controlKnobs *controlknobs.Knobs // always non-nil
serverURL string // URL of the tailcontrol server
clock tstime.Clock
logf logger.Logf
netMon *netmon.Monitor // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
skipIPForwardingCheck bool
pinger Pinger
popBrowser func(url string) // or nil
c2nHandler http.Handler // or nil
onClientVersion func(*tailcfg.ClientVersion) // or nil
onControlTime func(time.Time) // or nil
httpc *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
controlKnobs *controlknobs.Knobs // always non-nil
serverURL string // URL of the tailcontrol server
clock tstime.Clock
logf logger.Logf
netMon *netmon.Monitor // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
skipIPForwardingCheck bool
pinger Pinger
popBrowser func(url string) // or nil
c2nHandler http.Handler // or nil
onClientVersion func(*tailcfg.ClientVersion) // or nil
onControlTime func(time.Time) // or nil
onTailnetDefaultAutoUpdate func(bool) // or nil
dialPlan ControlDialPlanner // can be nil
@@ -110,24 +111,25 @@ type Observer interface {
}
type Options struct {
Persist persist.Persist // initial persistent data
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
Clock tstime.Clock
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
NetMon *netmon.Monitor // optional network monitor
PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
OnControlTime func(time.Time) // optional func to notify callers of new time from control
Dialer *tsdial.Dialer // non-nil
C2NHandler http.Handler // or nil
ControlKnobs *controlknobs.Knobs // or nil to ignore
Persist persist.Persist // initial persistent data
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
Clock tstime.Clock
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
NetMon *netmon.Monitor // optional network monitor
PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
OnControlTime func(time.Time) // optional func to notify callers of new time from control
OnTailnetDefaultAutoUpdate func(bool) // optional func to inform GUI of default auto-update setting for the tailnet
Dialer *tsdial.Dialer // non-nil
C2NHandler http.Handler // or nil
ControlKnobs *controlknobs.Knobs // or nil to ignore
// Observer is called when there's a change in status to report
// from the control client.
@@ -263,26 +265,27 @@ func NewDirect(opts Options) (*Direct, error) {
}
c := &Direct{
httpc: httpc,
controlKnobs: opts.ControlKnobs,
getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL,
clock: opts.Clock,
logf: opts.Logf,
persist: opts.Persist.View(),
authKey: opts.AuthKey,
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
netMon: opts.NetMon,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
onClientVersion: opts.OnClientVersion,
onControlTime: opts.OnControlTime,
c2nHandler: opts.C2NHandler,
dialer: opts.Dialer,
dnsCache: dnsCache,
dialPlan: opts.DialPlan,
httpc: httpc,
controlKnobs: opts.ControlKnobs,
getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL,
clock: opts.Clock,
logf: opts.Logf,
persist: opts.Persist.View(),
authKey: opts.AuthKey,
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
netMon: opts.NetMon,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
onClientVersion: opts.OnClientVersion,
onTailnetDefaultAutoUpdate: opts.OnTailnetDefaultAutoUpdate,
onControlTime: opts.OnControlTime,
c2nHandler: opts.C2NHandler,
dialer: opts.Dialer,
dnsCache: dnsCache,
dialPlan: opts.DialPlan,
}
if opts.Hostinfo == nil {
c.SetHostinfo(hostinfo.New())
@@ -1041,7 +1044,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
var resp tailcfg.MapResponse
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
vlogf("netmap: decode error: %v")
vlogf("netmap: decode error: %v", err)
return err
}
watchdogTimer.Stop()
@@ -1091,6 +1094,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
metricMapResponseKeepAlives.Add(1)
continue
}
if au, ok := resp.DefaultAutoUpdate.Get(); ok {
if c.onTailnetDefaultAutoUpdate != nil {
c.onTailnetDefaultAutoUpdate(au)
}
}
metricMapResponseMap.Add(1)
if gotNonKeepAliveMessage {

View File

@@ -8,7 +8,6 @@ import (
"encoding/json"
"fmt"
"net"
"net/netip"
"reflect"
"slices"
"sort"
@@ -86,9 +85,9 @@ type mapSession struct {
lastDomainAuditLogID string
lastHealth []string
lastPopBrowserURL string
stickyDebug tailcfg.Debug // accumulated opt.Bool values
lastTKAInfo *tailcfg.TKAInfo
lastNetmapSummary string // from NetworkMap.VeryConcise
lastMaxExpiry time.Duration
}
// newMapSession returns a mostly unconfigured new mapSession.
@@ -321,6 +320,9 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo
}
if resp.MaxKeyDuration > 0 {
ms.lastMaxExpiry = resp.MaxKeyDuration
}
}
var (
@@ -765,6 +767,7 @@ 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 != "" {
@@ -790,43 +793,3 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
}
return nm
}
func nodesSorted(v []*tailcfg.Node) bool {
for i, n := range v {
if i > 0 && n.ID <= v[i-1].ID {
return false
}
}
return true
}
func sortNodes(v []*tailcfg.Node) {
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
}
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
if v1 == nil {
return nil
}
v2 := make([]*tailcfg.Node, len(v1))
for i, n := range v1 {
v2[i] = n.Clone()
}
return v2
}
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
switch {
default:
return in
case debugSelfIPv6Only():
for _, a := range in {
if a.Addr().Is6() {
ret = append(ret, a)
}
}
return ret
}
}

View File

@@ -64,6 +64,15 @@ type Knobs struct {
// LinuxForceNfTables is whether the node should use nftables for Linux
// netfiltering, unless overridden by the user.
LinuxForceNfTables atomic.Bool
// SeamlessKeyRenewal is whether to enable the alpha functionality of
// renewing node keys without breaking connections.
// http://go/seamless-key-renewal
SeamlessKeyRenewal atomic.Bool
// ProbeUDPLifetime is whether the node should probe UDP path lifetime on
// the tail end of an active direct connection in magicsock.
ProbeUDPLifetime atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@@ -89,6 +98,8 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
silentDisco = has(tailcfg.NodeAttrSilentDisco)
forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables)
forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables)
seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal)
probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
@@ -109,6 +120,8 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
k.SilentDisco.Store(silentDisco)
k.LinuxForceIPTables.Store(forceIPTables)
k.LinuxForceNfTables.Store(forceNfTables)
k.SeamlessKeyRenewal.Store(seamlessKeyRenewal)
k.ProbeUDPLifetime.Store(probeUDPLifetime)
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
@@ -130,5 +143,7 @@ func (k *Knobs) AsDebugJSON() map[string]any {
"SilentDisco": k.SilentDisco.Load(),
"LinuxForceIPTables": k.LinuxForceIPTables.Load(),
"LinuxForceNfTables": k.LinuxForceNfTables.Load(),
"SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(),
"ProbeUDPLifetime": k.ProbeUDPLifetime.Load(),
}
}

View File

@@ -159,16 +159,16 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
}
type clientInfo struct {
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// MeshKey optionally specifies a pre-shared key used by
// trusted clients. It's required to subscribe to the
// connection list & forward packets. It's empty for regular
// users.
MeshKey string `json:"meshKey,omitempty"`
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// CanAckPings is whether the client declares it's able to ack
// pings.
CanAckPings bool

View File

@@ -712,7 +712,6 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
connectedAt: s.clock.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
@@ -753,12 +752,6 @@ func (s *Server) debugLogf(format string, v ...any) {
}
}
// for testing
var (
timeSleep = time.Sleep
timeNow = time.Now
)
// run serves the client until there's an error.
// If the client hangs up or the server is closed, run returns nil, otherwise run returns an error.
func (c *sclient) run(ctx context.Context) error {
@@ -1323,7 +1316,6 @@ type sclient struct {
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
@@ -1360,16 +1352,13 @@ type sclient struct {
// peerConnState represents whether a peer is connected to the server
// or not.
type peerConnState struct {
ipPort netip.AddrPort // if present, the peer's IP:port
peer key.NodePublic
present bool
ipPort netip.AddrPort // if present, the peer's IP:port
}
// pkt is a request to write a data frame to an sclient.
type pkt struct {
// src is the who's the sender of the packet.
src key.NodePublic
// enqueuedAt is when a packet was put onto a queue before it was sent,
// and is used for reporting metrics on the duration of packets in the queue.
enqueuedAt time.Time
@@ -1377,6 +1366,9 @@ type pkt struct {
// bs is the data packet bytes.
// The memory is owned by pkt.
bs []byte
// src is the who's the sender of the packet.
src key.NodePublic
}
// peerGoneMsg is a request to write a peerGone frame to an sclient
@@ -1579,6 +1571,17 @@ func (c *sclient) sendMeshUpdates() error {
c.s.mu.Lock()
defer c.s.mu.Unlock()
// allow all happened-before mesh update request goroutines to complete, if
// we don't finish the task we'll queue another below.
drainUpdates:
for {
select {
case <-c.meshUpdate:
default:
break drainUpdates
}
}
writes := 0
for _, pcs := range c.peerStateChange {
if c.bw.Available() <= frameHeaderLen+keyLen {

View File

@@ -11,7 +11,7 @@
// senderDiscoPub [32]byte // nacl public key
// nonce [24]byte
//
// The recipient then decrypts the bytes following (the nacl secretbox)
// The recipient then decrypts the bytes following (the nacl box)
// and then the inner payload structure is:
//
// messageType byte (the MessageType constants below)
@@ -35,7 +35,7 @@ const Magic = "TS💬" // 6 bytes: 0x54 53 f0 9f 92 ac
const keyLen = 32
// NonceLen is the length of the nonces used by nacl secretboxes.
// NonceLen is the length of the nonces used by nacl box.
const NonceLen = 24
type MessageType byte
@@ -70,7 +70,7 @@ func Source(p []byte) (src []byte, ok bool) {
}
// Parse parses the encrypted part of the message from inside the
// nacl secretbox.
// nacl box.
func Parse(p []byte) (Message, error) {
if len(p) < 2 {
return nil, errShort

View File

@@ -1,9 +1,9 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package webhooks provides example consumer code for Tailscale
// Command webhooks provides example consumer code for Tailscale
// webhooks.
package webhooks
package main
import (
"crypto/hmac"

View File

@@ -0,0 +1,222 @@
<?xml version="1.0" encoding="utf-8"?>
<policyDefinitionResources revision="1.0" schemaVersion="1.0"
xmlns="http://www.microsoft.com/GroupPolicy/PolicyDefinitions">
<displayName>Tailscale</displayName>
<description>A set of policies that enforces particular settings in the Tailscale Windows client.</description>
<resources>
<stringTable>
<string id="TAILSCALE_PRODUCT">Tailscale</string>
<string id="SINCE_V1_22">Tailscale version 1.22.0 and later</string>
<string id="SINCE_V1_26">Tailscale version 1.26.0 and later</string>
<string id="SINCE_V1_50">Tailscale version 1.50.0 and later</string>
<string id="SINCE_V1_52">Tailscale version 1.52.0 and later</string>
<string id="SINCE_V1_56">Tailscale version 1.56.0 and later</string>
<string id="PARTIAL_FULL_SINCE_V1_56">Tailscale version 1.56.0 and later (full support), some earlier versions (partial support)</string>
<string id="SINCE_V1_58">Tailscale version 1.58.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="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.
See https://tailscale.com/kb/1315/mdm-keys#set-a-custom-control-server-url for more details.
If you configure this policy, set it to the URL of your coordination server, beginning with https:// and ending with no trailing slash. If blank or "https://controlplane.tailscale.com", the default coordination server will be required.
If you disable this policy, the Tailscale SaaS coordination server will be used by default, but a non-standard Tailscale coordination server can be configured using the CLI.]]></string>
<string id="LogTarget">Require using a specific Tailscale log server</string>
<string id="LogTarget_Help"><![CDATA[This policy can be used to require the use of a non-standard log server.
Please note that using a non-standard log server will limit Tailscale Support's ability to diagnose problems.
If you configure this policy, set it to the URL of your log server, beginning with https:// and ending with no trailing slash. If blank or "https://log.tailscale.io", the default log server will be used.
If you disable this policy, the Tailscale standard log server will be used by default, but a non-standard Tailscale log server can be configured using the TS_LOG_TARGET environment variable.]]></string>
<string id="Tailnet">Specify which Tailnet should be used for Login</string>
<string id="Tailnet_Help"><![CDATA[This policy can be used to suggest or require a specific tailnet when opening the login page.
See https://tailscale.com/kb/1315/mdm-keys#set-a-suggested-or-required-tailnet for more details.
To suggest a tailnet at login time, set this to the name of the tailnet, as shown in the top-left of the admin panel, such as "example.com". That tailnet's SSO button will be shown prominently, along with the option to select a different tailnet.
To require logging in to a particular tailnet, add the "required:" prefix, such as "required:example.com". The result is similar to the suggested tailnet but there will be no option to choose a different tailnet.
If you configure this policy, set it to the name of the tailnet, possibly with the "required:" prefix, as described above.
If you disable this policy, the standard login page will be used.]]></string>
<string id="ExitNodeID">Require using a specific Exit Node</string>
<string id="ExitNodeID_Help"><![CDATA[This policy can be used to require always using the specified Exit Node whenever the Tailscale client is connected.
See https://tailscale.com/kb/1315/mdm-keys#force-an-exit-node-to-always-be-used and https://tailscale.com/kb/1103/exit-nodes for more details.
If you enable this policy, set it to the ID of an exit node. The ID is visible on the Machines page of the admin console, or can be queried using the Tailscale API. If the specified exit node is unavailable, this device will have no Internet access unless Tailscale is disconnected.
If you disable this policy or supply an empty exit node ID, then usage of exit nodes will be disallowed.
If you do not configure this policy, no exit node will be used by default but an exit node (if one is available and permitted by ACLs) can be chosen by the user if desired.]]></string>
<string id="AllowIncomingConnections">Allow incoming connections</string>
<string id="AllowIncomingConnections_Help"><![CDATA[This policy can be used to require that the Allow Incoming Connections setting is configured a certain way.
See https://tailscale.com/kb/1315/mdm-keys#set-whether-to-allow-incoming-connections and https://tailscale.com/kb/1072/client-preferences#allow-incoming-connections for more details.
If you enable this policy, then Allow Incoming Connections is always enabled and the menu option is hidden.
If you disable this policy, then Allow Incoming Connections is always disabled and the menu option is hidden.
If you do not configure this policy, then Allow Incoming Connections depends on what is selected in the Preferences submenu.]]></string>
<string id="UnattendedMode">Run Tailscale in Unattended Mode</string>
<string id="UnattendedMode_Help"><![CDATA[This policy can be used to require that the Run Unattended setting is configured a certain way.
See https://tailscale.com/kb/1315/mdm-keys#set-unattended-mode and https://tailscale.com/kb/1088/run-unattended for more details.
If you enable this policy, then Run Unattended is always enabled and the menu option is hidden.
If you disable this policy, then Run Unattended is always disabled and the menu option is hidden.
If you do not configure this policy, then Run Unattended depends on what is selected in the Preferences submenu.]]></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.
See https://tailscale.com/kb/1315/mdm-keys#toggle-local-network-access-when-an-exit-node-is-in-use and https://tailscale.com/kb/1103/exit-nodes#step-4-use-the-exit-node for more details.
If you enable this policy, then Allow Local Network Access is always enabled and the menu option is hidden.
If you disable this policy, then Allow Local Network Access is always disabled and the menu option is hidden.
If you do not configure this policy, then Allow Local Network Access depends on what is selected in the Exit Node submenu.]]></string>
<string id="UseTailscaleDNSSettings">Use Tailscale DNS Settings</string>
<string id="UseTailscaleDNSSettings_Help"><![CDATA[This policy can be used to require that Use Tailscale DNS is configured a certain way.
See https://tailscale.com/kb/1315/mdm-keys#set-whether-the-device-uses-tailscale-dns-settings for more details.
If you enable this policy, then Use Tailscale DNS is always enabled and the menu option is hidden.
If you disable this policy, then Use Tailscale DNS is always disabled and the menu option is hidden.
If you do not configure this policy, then Use Tailscale DNS depends on what is selected in the Preferences submenu.]]></string>
<string id="UseTailscaleSubnets">Use Tailscale Subnets</string>
<string id="UseTailscaleSubnets_Help"><![CDATA[This policy can be used to require that Use Tailscale Subnets is configured a certain way.
See https://tailscale.com/kb/1315/mdm-keys#set-whether-the-device-accepts-tailscale-subnets or https://tailscale.com/kb/1019/subnets for more details.
If you enable this policy, then Use Tailscale Subnets is always enabled and the menu option is hidden.
If you disable this policy, then Use Tailscale Subnets is always disabled and the menu option is hidden.
If you do not configure this policy, then Use Tailscale Subnets depends on what is selected in the Preferences submenu.]]></string>
<string id="InstallUpdates">Automatically install updates</string>
<string id="InstallUpdates_Help"><![CDATA[This policy can be used to require that Automatically Install Updates is configured a certain way.
See https://tailscale.com/kb/1067/update#auto-updates for more details.
If you enable this policy, then Automatically Install Updates is always enabled and the menu option is hidden.
If you disable this policy, then Automatically Install Updates is always disabled and the menu option is hidden.
If you do not configure this policy, then Automatically Install Updates depends on what is selected in the Preferences submenu.]]></string>
<string id="AdvertiseExitNode">Run Tailscale as an Exit Node</string>
<string id="AdvertiseExitNode_Help"><![CDATA[This policy can be used to require that Run Exit Node is configured a certain way.
See https://tailscale.com/kb/1103/exit-nodes for more details.
If you enable this policy, then Run Exit Node is always enabled and the menu option is hidden.
If you disable this policy, then Run Exit Node is always disabled and the menu option is hidden.
If you do not configure this policy, then Run Exit Node depends on what is selected in the Exit Node submenu.]]></string>
<string id="AdminPanel">Show the "Admin Panel" menu item</string>
<string id="AdminPanel_Help"><![CDATA[This policy can be used to show or hide the Admin Console item in the Tailscale Menu.
If you enable or don't configure this policy, the Admin Console item will be shown in the Tailscale menu when available.
If you disable this policy, the Admin Console item will always be hidden from the Tailscale menu.]]></string>
<string id="NetworkDevices">Show the "Network Devices" submenu</string>
<string id="NetworkDevices_Help"><![CDATA[This policy can be used to show or hide the Network Devices submenu in the Tailscale Menu.
If you enable or don't configure this policy, the Network Devices submenu will be shown in the Tailscale menu.
If you disable this policy, the Network Devices submenu will be hidden from the Tailscale menu. This does not affect other devices' visibility in the CLI.]]></string>
<string id="TestMenu">Show the "Debug" submenu</string>
<string id="TestMenu_Help"><![CDATA[This policy can be used to show or hide the Debug submenu of the Tailscale menu.
See https://tailscale.com/kb/1315/mdm-keys#hide-the-debug-menu for more details.
If you enable or don't configure this policy, the Debug submenu will be shown in the Tailscale menu when opened while holding Ctrl.
If you disable this policy, the Debug submenu will be hidden from the Tailscale menu.]]></string>
<string id="UpdateMenu">Show the "Update Available" menu item</string>
<string id="UpdateMenu_Help"><![CDATA[This policy can be used to show or hide the Update Available item in the Tailscale Menu.
See https://tailscale.com/kb/1315/mdm-keys#hide-the-update-menu for more details.
If you enable or don't configure this policy, the Update Available item will be shown in the Tailscale menu when there is an update.
If you disable this policy, the Update Available item will be hidden from the Tailscale menu.]]></string>
<string id="RunExitNode">Show the "Run Exit Node" menu item</string>
<string id="RunExitNode_Help"><![CDATA[This policy can be used to show or hide the Run Exit Node item in the Exit Node submenu.
See https://tailscale.com/kb/1315/mdm-keys#hide-the-run-as-exit-node-menu-item for more details.
This does not affect using the CLI to enable or disable advertising an exit node. If you wish to enable or disable this feature, see the Run Exit Node policy in the Settings category.
If you enable or don't configure this policy, the Run Exit Node item will be shown in the Exit Node submenu.
If you disable this policy, the Run Exit Node item will be hidden from the Exit Node submenu.]]></string>
<string id="PreferencesMenu">Show the "Preferences" submenu</string>
<string id="PreferencesMenu_Help"><![CDATA[This policy can be used to show or hide the Preferences submenu of the Tailscale menu.
See https://tailscale.com/kb/1315/mdm-keys#hide-the-preferences-menu for more details.
This does not affect using the CLI to modify that menu's preferences. If you wish to control those, look at the policies in the Settings category.
If you enable or don't configure this policy, the Preferences submenu will be shown in the Tailscale menu.
If you disable this policy, the Preferences submenu will be hidden from the Tailscale menu.]]></string>
<string id="ExitNodesPicker">Show the "Exit Node" submenu</string>
<string id="ExitNodesPicker_Help"><![CDATA[This policy can be used to show or hide the Exit Node submenu of the Tailscale menu.
See https://tailscale.com/kb/1315/mdm-keys#hide-the-exit-node-picker for more details.
This does not affect using the CLI to select or stop using an exit node. If you wish to control exit node usage, look at the "Require using a specific Exit Node" policy in the Settings category.
If you enable or don't configure this policy, the Exit Node submenu will be shown in the Tailscale menu.
If you disable this policy, the Exit Node submenu will be hidden from the Tailscale menu.]]></string>
<string id="KeyExpirationNoticeTime">Specify a custom key expiration notification time</string>
<string id="KeyExpirationNoticeTime_Help"><![CDATA[This policy can be used to configure how soon the notification appears before key expiry.
See https://tailscale.com/kb/1315/mdm-keys#set-the-key-expiration-notice-period for more details.
Time intervals must be specified as a Go Duration: for example, 24h, 5h25m30s. Time units larger than hours are unsupported.
If you enable this policy and supply a valid time interval, the key expiry notification will begin to display when the current key has less than that amount of time remaining.
If you disable or don't configure this policy, the default time period will be used (as of Tailscale 1.56, this is 24 hours).]]></string>
<string id="LogSCMInteractions">Log extra details about service events</string>
<string id="LogSCMInteractions_Help"><![CDATA[This policy can be used to enable additional logging related to Service Control Manager for debugging purposes.
This should only be enabled if recommended by Tailscale Support.
If you enable this policy, additional logging will be enabled for SCM events.
If you disable or don't configure this policy, the normal amount of logging occurs.]]></string>
<string id="FlushDNSOnSessionUnlock">Flush the DNS cache on session unlock</string>
<string id="FlushDNSOnSessionUnlock_Help"><![CDATA[This policy can be used to enable additional DNS cache flushing for debugging purposes.
This should only be enabled if recommended by Tailscale Support.
If you enable this policy, the DNS cache will be flushed on session unlock in addition to when the DNS cache would normally be flushed.
If you disable or don't configure this policy, the DNS cache is managed normally.]]></string>
<string id="PostureChecking">Collect data for posture checking</string>
<string id="PostureChecking_Help"><![CDATA[This policy can be used to require that the Posture Checking setting is configured a certain way.
See https://tailscale.com/kb/1315/mdm-keys#enable-gathering-device-posture-data and https://tailscale.com/kb/1326/device-identity for more details.
If you enable this policy, then data collection is always enabled.
If you disable this policy, then data collection is always disabled.
If you do not configure this policy, then data collection depends on if it has been enabled from the CLI (as of Tailscale 1.56), it may be present in the GUI in later versions.]]></string>
</stringTable>
<presentationTable>
<presentation id="LoginURL">
<textBox refId="LoginURLPrompt">
<label>Coordination server</label>
</textBox>
</presentation>
<presentation id="LogTarget">
<textBox refId="LogTargetPrompt">
<label>Log server</label>
</textBox>
</presentation>
<presentation id="Tailnet">
<textBox refId="TailnetPrompt">
<label>Tailnet</label>
</textBox>
</presentation>
<presentation id="ExitNodeID">
<textBox refId="ExitNodeIDPrompt">
<label>Exit Node</label>
</textBox>
</presentation>
</presentationTable>
</resources>
</policyDefinitionResources>

View File

@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8"?>
<policyDefinitions revision="1.0" schemaVersion="1.0"
xmlns="http://www.microsoft.com/GroupPolicy/PolicyDefinitions">
<policyNamespaces>
<target prefix="tailscale" namespace="Tailscale.Policies" />
</policyNamespaces>
<resources minRequiredRevision="1.0" />
<supportedOn>
<products>
<product name="TAILSCALE_PRODUCT" displayName="$(string.TAILSCALE_PRODUCT)">
<majorVersion name="TAILSCALE_V1" displayName="$(string.TAILSCALE_PRODUCT)" versionIndex="1" />
</product>
</products>
<definitions>
<definition name="SINCE_V1_22"
displayName="$(string.SINCE_V1_22)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_26"
displayName="$(string.SINCE_V1_26)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_50"
displayName="$(string.SINCE_V1_50)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_52"
displayName="$(string.SINCE_V1_52)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="PARTIAL_FULL_SINCE_V1_56"
displayName="$(string.PARTIAL_FULL_SINCE_V1_56)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_56"
displayName="$(string.SINCE_V1_56)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
<definition name="SINCE_V1_58"
displayName="$(string.SINCE_V1_58)">
<and><reference ref="TAILSCALE_PRODUCT"/></and>
</definition>
</definitions>
</supportedOn>
<categories>
<category name="Top_Category" displayName="$(string.Tailscale_Category)" />
<category name="UI_Category" displayName="$(string.UI_Category)">
<parentCategory ref="Top_Category" />
</category>
<category name="Settings_Category" displayName="$(string.Settings_Category)">
<parentCategory ref="Top_Category" />
</category>
</categories>
<policies>
<policy name="LoginURL" class="Machine" displayName="$(string.LoginURL)" explainText="$(string.LoginURL_Help)" presentation="$(presentation.LoginURL)" key="Software\Policies\Tailscale">
<parentCategory ref="Top_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<elements>
<text id="LoginURLPrompt" valueName="LoginURL" required="true" />
</elements>
</policy>
<policy name="LogTarget" class="Machine" displayName="$(string.LogTarget)" explainText="$(string.LogTarget_Help)" presentation="$(presentation.LogTarget)" key="Software\Policies\Tailscale">
<parentCategory ref="Top_Category" />
<supportedOn ref="SINCE_V1_58" />
<elements>
<text id="LogTargetPrompt" valueName="LogTarget" required="true" />
</elements>
</policy>
<policy name="Tailnet" class="Machine" displayName="$(string.Tailnet)" explainText="$(string.Tailnet_Help)" presentation="$(presentation.Tailnet)" key="Software\Policies\Tailscale">
<parentCategory ref="Top_Category" />
<supportedOn ref="SINCE_V1_52" />
<elements>
<text id="TailnetPrompt" valueName="Tailnet" required="true" />
</elements>
</policy>
<policy name="ExitNodeID" class="Machine" displayName="$(string.ExitNodeID)" explainText="$(string.ExitNodeID_Help)" presentation="$(presentation.ExitNodeID)" key="Software\Policies\Tailscale">
<parentCategory ref="Settings_Category" />
<supportedOn ref="SINCE_V1_56" />
<elements>
<text id="ExitNodeIDPrompt" valueName="ExitNodeID" required="true" />
</elements>
</policy>
<policy name="AllowIncomingConnections" class="Machine" displayName="$(string.AllowIncomingConnections)" explainText="$(string.AllowIncomingConnections_Help)" key="Software\Policies\Tailscale" valueName="AllowIncomingConnections">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="UnattendedMode" class="Machine" displayName="$(string.UnattendedMode)" explainText="$(string.UnattendedMode_Help)" key="Software\Policies\Tailscale" valueName="UnattendedMode">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</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" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="UseTailscaleDNSSettings" class="Machine" displayName="$(string.UseTailscaleDNSSettings)" explainText="$(string.UseTailscaleDNSSettings_Help)" key="Software\Policies\Tailscale" valueName="UseTailscaleDNSSettings">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="UseTailscaleSubnets" class="Machine" displayName="$(string.UseTailscaleSubnets)" explainText="$(string.UseTailscaleSubnets_Help)" key="Software\Policies\Tailscale" valueName="UseTailscaleSubnets">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="InstallUpdates" class="Machine" displayName="$(string.InstallUpdates)" explainText="$(string.InstallUpdates_Help)" key="Software\Policies\Tailscale" valueName="InstallUpdates">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="AdvertiseExitNode" class="Machine" displayName="$(string.AdvertiseExitNode)" explainText="$(string.AdvertiseExitNode_Help)" key="Software\Policies\Tailscale" valueName="AdvertiseExitNode">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="PostureChecking" class="Machine" displayName="$(string.PostureChecking)" explainText="$(string.PostureChecking_Help)" key="Software\Policies\Tailscale" valueName="PostureChecking">
<parentCategory ref="Settings_Category" />
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
<enabledValue>
<string>always</string>
</enabledValue>
<disabledValue>
<string>never</string>
</disabledValue>
</policy>
<policy name="LogSCMInteractions" class="Machine" displayName="$(string.LogSCMInteractions)" explainText="$(string.LogSCMInteractions_Help)" key="Software\Policies\Tailscale" valueName="LogSCMInteractions">
<parentCategory ref="Top_Category" />
<supportedOn ref="SINCE_V1_26" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="FlushDNSOnSessionUnlock" class="Machine" displayName="$(string.FlushDNSOnSessionUnlock)" explainText="$(string.FlushDNSOnSessionUnlock_Help)" key="Software\Policies\Tailscale" valueName="FlushDNSOnSessionUnlock">
<parentCategory ref="Top_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<decimal value="1" />
</enabledValue>
<disabledValue>
<decimal value="0" />
</disabledValue>
</policy>
<policy name="AdminPanel" class="Machine" displayName="$(string.AdminPanel)" explainText="$(string.AdminPanel_Help)" key="Software\Policies\Tailscale" valueName="AdminPanel">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="NetworkDevices" class="Machine" displayName="$(string.NetworkDevices)" explainText="$(string.NetworkDevices_Help)" key="Software\Policies\Tailscale" valueName="NetworkDevices">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="TestMenu" class="Machine" displayName="$(string.TestMenu)" explainText="$(string.TestMenu_Help)" key="Software\Policies\Tailscale" valueName="TestMenu">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="UpdateMenu" class="Machine" displayName="$(string.UpdateMenu)" explainText="$(string.UpdateMenu_Help)" key="Software\Policies\Tailscale" valueName="UpdateMenu">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="RunExitNode" class="Machine" displayName="$(string.RunExitNode)" explainText="$(string.RunExitNode_Help)" key="Software\Policies\Tailscale" valueName="RunExitNode">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="PreferencesMenu" class="Machine" displayName="$(string.PreferencesMenu)" explainText="$(string.PreferencesMenu_Help)" key="Software\Policies\Tailscale" valueName="PreferencesMenu">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
<policy name="ExitNodesPicker" class="Machine" displayName="$(string.ExitNodesPicker)" explainText="$(string.ExitNodesPicker_Help)" key="Software\Policies\Tailscale" valueName="ExitNodesPicker">
<parentCategory ref="UI_Category" />
<supportedOn ref="SINCE_V1_22" />
<enabledValue>
<string>show</string>
</enabledValue>
<disabledValue>
<string>hide</string>
</disabledValue>
</policy>
</policies>
</policyDefinitions>

View File

@@ -26,6 +26,7 @@ func (Check) Run(_ context.Context, logf logger.Logf) error {
return permissionsImpl(logf)
}
//lint:ignore U1000 used in non-windows implementations.
func formatUserID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if uu, err := user.LookupId(idStr); err != nil {
@@ -35,6 +36,7 @@ func formatUserID[T constraints.Integer](id T) string {
}
}
//lint:ignore U1000 used in non-windows implementations.
func formatGroupID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if g, err := user.LookupGroupId(idStr); err != nil {
@@ -44,6 +46,7 @@ func formatGroupID[T constraints.Integer](id T) string {
}
}
//lint:ignore U1000 used in non-windows implementations.
func formatGroups[T constraints.Integer](groups []T) string {
var buf strings.Builder
for i, group := range groups {

View File

@@ -120,4 +120,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-bG/ydsJf2UncOcDo8/BXdvQJO3Mk0tl8JGje1b6kto4=
# nix-direnv cache busting line: sha256-b/iffKOn7nMiWvM0AIGGzZaJ15NTaBlJff+aja3NQio=

187
go.mod
View File

@@ -1,125 +1,130 @@
module tailscale.com
go 1.21
go 1.21.1
toolchain go1.21.5
require (
filippo.io/mkcert v1.4.4
github.com/akutz/memconn v0.1.0
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
github.com/andybalholm/brotli v1.0.5
github.com/andybalholm/brotli v1.1.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aws/aws-sdk-go-v2 v1.21.0
github.com/aws/aws-sdk-go-v2/config v1.18.42
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/service/ssm v1.38.0
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7
github.com/coreos/go-iptables v0.7.0
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/coreos/go-systemd/v22 v22.5.0
github.com/creack/pty v1.1.18
github.com/creack/pty v1.1.21
github.com/dave/courtney v0.4.0
github.com/dave/jennifer v1.7.0
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.19.4
github.com/frankban/quicktest v1.14.5
github.com/evanw/esbuild v0.19.11
github.com/frankban/quicktest v1.14.6
github.com/fxamacker/cbor/v2 v2.5.0
github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533
github.com/go-logr/zapr v1.2.4
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0
github.com/go-logr/zapr v1.3.0
github.com/go-ole/go-ole v1.3.0
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golangci/golangci-lint v1.52.2
github.com/google/go-cmp v0.5.9
github.com/google/go-containerregistry v0.16.1
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.18.0
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c
github.com/google/uuid v1.3.1
github.com/google/uuid v1.5.0
github.com/goreleaser/nfpm/v2 v2.33.1
github.com/hdevalence/ed25519consensus v0.1.0
github.com/hdevalence/ed25519consensus v0.2.0
github.com/iancoleman/strcase v0.3.0
github.com/illarion/gonotify v1.0.1
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86
github.com/jsimonetti/rtnetlink v1.3.5
github.com/jsimonetti/rtnetlink v1.4.0
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.17.0
github.com/klauspost/compress v1.17.4
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.19
github.com/mattn/go-isatty v0.0.20
github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/sdnotify v1.0.0
github.com/miekg/dns v1.1.56
github.com/miekg/dns v1.1.58
github.com/mitchellh/go-ps v1.0.0
github.com/peterbourgon/ff/v3 v3.4.0
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
github.com/prometheus/client_golang v1.17.0
github.com/prometheus/common v0.44.0
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/common v0.46.0
github.com/safchain/ethtool v0.3.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780
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-20220601142259-c0b937af2e89
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272
github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.11.0
github.com/u-root/u-root v0.12.0
github.com/vishvananda/netlink v1.2.1-beta.2
github.com/vishvananda/netns v0.0.4
go.uber.org/zap v1.26.0
go4.org/mem v0.0.0-20220726221520-4f986261bf13
go4.org/netipx v0.0.0-20230824141953-6213f710f925
golang.org/x/crypto v0.15.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/mod v0.14.0
golang.org/x/net v0.18.0
golang.org/x/oauth2 v0.12.0
golang.org/x/sync v0.5.0
golang.org/x/sys v0.15.0
golang.org/x/term v0.14.0
golang.org/x/time v0.3.0
golang.org/x/tools v0.15.0
golang.org/x/net v0.20.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.16.0
golang.org/x/term v0.16.0
golang.org/x/time v0.5.0
golang.org/x/tools v0.17.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-20230928000133-4fe30062272c
gvisor.dev/gvisor v0.0.0-20240119233241-c9c1d4f9b186
honnef.co/go/tools v0.4.6
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9
inet.af/wf v0.0.0-20221017222439-36129f591884
k8s.io/api v0.28.2
k8s.io/apimachinery v0.28.2
k8s.io/apiserver v0.28.2
k8s.io/client-go v0.28.2
nhooyr.io/websocket v1.8.7
k8s.io/api v0.29.1
k8s.io/apimachinery v0.29.1
k8s.io/apiserver v0.29.1
k8s.io/client-go v0.29.1
nhooyr.io/websocket v1.8.10
sigs.k8s.io/controller-runtime v0.16.2
sigs.k8s.io/yaml v1.3.0
software.sslmate.com/src/go-pkcs12 v0.2.1
sigs.k8s.io/controller-tools v0.13.0
sigs.k8s.io/yaml v1.4.0
software.sslmate.com/src/go-pkcs12 v0.4.0
)
require (
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 // indirect
github.com/dave/brenda v1.1.0 // indirect
github.com/gobuffalo/flect v1.0.2 // indirect
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
)
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
dario.cat/mergo v1.0.0 // indirect
filippo.io/edwards25519 v1.0.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Abirdcfly/dupword v0.0.11 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/Antonboom/errname v0.1.9 // indirect
@@ -132,27 +137,26 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/OpenPeeDeeP/depguard v1.1.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.5.1 // 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.13.40 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 // 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.9.11 // 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.9.35 // 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.14.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect
github.com/aws/smithy-go 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/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
@@ -165,35 +169,35 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.15.1 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.10.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/docker/cli v24.0.6+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
github.com/docker/docker v24.0.7+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/docker/cli v25.0.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker v25.0.0+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/esimonov/ifshort v1.0.4 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.7.0 // indirect
github.com/evanphx/json-patch/v5 v5.8.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0
github.com/fsnotify/fsnotify v1.7.0
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/go-critic/go-critic v0.8.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.4.1 // indirect
github.com/go-git/go-git/v5 v5.8.1 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.20.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.11.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/jsonreference v0.20.4 // indirect
github.com/go-openapi/swag v0.22.7 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.1.0 // indirect
@@ -222,7 +226,7 @@ require (
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
github.com/goreleaser/chglog v0.5.0 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/gorilla/csrf v1.7.1
github.com/gorilla/csrf v1.7.2
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.4.2 // indirect
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
@@ -265,7 +269,6 @@ require (
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/mgechev/revive v1.3.1 // indirect
@@ -285,14 +288,14 @@ require (
github.com/nunnatsa/ginkgolinter v0.11.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc5 // indirect
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.4.1 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasilyte/go-ruleguard v0.3.19 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
@@ -317,15 +320,15 @@ require (
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.7.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.16.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
github.com/stretchr/testify v1.8.4
github.com/subosito/gotenv v1.4.2 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55
@@ -335,7 +338,7 @@ require (
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.5 // indirect
@@ -348,27 +351,27 @@ require (
gitlab.com/bosi/decorder v0.2.3 // indirect
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-20230905200255-921286631fa9 // indirect
golang.org/x/image v0.12.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/image v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1
howett.net/plist v1.0.0 // indirect
k8s.io/apiextensions-apiserver v0.28.2 // indirect
k8s.io/component-base v0.28.2 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
k8s.io/apiextensions-apiserver v0.29.1 // indirect
k8s.io/component-base v0.29.1 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 // indirect
k8s.io/utils v0.0.0-20240102154912-e7106e64919e // indirect
mvdan.cc/gofumpt v0.5.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

View File

@@ -1 +1 @@
sha256-bG/ydsJf2UncOcDo8/BXdvQJO3Mk0tl8JGje1b6kto4=
sha256-b/iffKOn7nMiWvM0AIGGzZaJ15NTaBlJff+aja3NQio=

436
go.sum
View File

@@ -42,8 +42,8 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek=
filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU=
@@ -78,14 +78,12 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/OpenPeeDeeP/depguard v1.1.1 h1:TSUznLjvp/4IUP+OQ0t/4jF4QUyxIcVX8YnghZdunyA=
github.com/OpenPeeDeeP/depguard v1.1.1/go.mod h1:JtAMzWkmFEzDPyAd+W0NHl1lvpQKTvT9jnRVsohBKpc=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.1 h1:Awsg7MPc2gD3I7IFac2qE3Gdls0lZW8SzrFZ3k1oz0s=
github.com/ProtonMail/gopenpgp/v2 v2.7.1/go.mod h1:/BU5gfAVwqyd8EfC3Eu7zmuhwYQpKs+cGD8M//iiaxs=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -99,8 +97,8 @@ github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pO
github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE=
github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw=
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
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-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
@@ -111,58 +109,58 @@ github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1
github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s=
github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc=
github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
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.18.42 h1:28jHROB27xZwU0CB88giDSjz7M1Sba3olb5JBGwina8=
github.com/aws/aws-sdk-go-v2/config v1.18.42/go.mod h1:4AZM3nMMxwlG+eZlxvBKqwVbkDLlnN2a4UGTL6HjaZI=
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.13.40 h1:s8yOkDh+5b1jUDhMBtngF6zKWLDs84chUk2Vk0c38Og=
github.com/aws/aws-sdk-go-v2/credentials v1.13.40/go.mod h1:VtEHVAAqDWASwdOqj/1huyT6uHbs5s8FUHfDQdky/Rs=
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.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8=
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.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas=
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.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw=
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.3.43 h1:g+qlObJH4Kn4n21g69DjspU0hKTjWtq7naZ9OLCv0ew=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo=
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 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
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.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o=
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/service/ssm v1.38.0 h1:JON9MBvwUlM8HXylfB2caZuH3VXz9RxO4SMp2+TNc3Q=
github.com/aws/aws-sdk-go-v2/service/ssm v1.38.0/go.mod h1:JjBzoceyKkpQY3v1GPIdg6kHqUFHRJ7SDlwtwoH0Qh8=
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.14.1 h1:YkNzx1RLS0F5qdf9v1Q8Cuv9NXCL2TkosOxhzlUPV64=
github.com/aws/aws-sdk-go-v2/service/sso v1.14.1/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4=
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.17.1 h1:8lKOidPkmSmfUtiTgtdXWgaKItCZ/g75/jEk6Ql6GsA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4=
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.22.0 h1:s4bioTgjSFRwOoyEFzAVCmFmoowBgjTR8gkrF/sQ4wk=
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU=
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.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ=
github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
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/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=
@@ -200,28 +198,31 @@ github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8/go.mod h1:gakxgyXa
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU=
github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
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/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/curioswitch/go-reassign v0.2.0 h1:G9UZyOcpk/d7Gd6mqYgd8XYWFMw/znxwGDUstnC9DIo=
github.com/curioswitch/go-reassign v0.2.0/go.mod h1:x6OpXuWvgfQaMGks2BZybTngWjT84hqJfKoO8Tt/Roc=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/daixiang0/gci v0.10.1 h1:eheNA3ljF6SxnPD/vE4lCBusVHmV3Rs3dkKvFrJ7MR0=
github.com/daixiang0/gci v0.10.1/go.mod h1:xtHP9N7AHdNvtRNfcx9gwTDfw7FRJx4bZUsiEfiNNAI=
github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14 h1:YI1gOOdmMk3xodBao7fehcvoZsEeOyy/cfhlpCSPgM4=
@@ -237,26 +238,26 @@ github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiq
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0oZOHPaOjpmN/BsgNYF+gW/hxZXXc=
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM=
github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8=
github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40=
github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpFowZBX6GoQ=
github.com/docker/cli v25.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v25.0.0+incompatible h1:g9b6wZTblhMgzOT2tspESstfw6ySZ9kdm94BLDKaZac=
github.com/docker/docker v25.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo=
github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU=
github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -271,28 +272,24 @@ github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/evanw/esbuild v0.19.4 h1:Etk+6ZCjtNxZZLEgMKSqpO0/oM0k1WYKJabaPMJ39iQ=
github.com/evanw/esbuild v0.19.4/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
github.com/evanphx/json-patch/v5 v5.8.1 h1:iPEdwg0XayoS+E7Mth9JxwUtOgyVxnDTXHtKhZPlZxA=
github.com/evanphx/json-patch/v5 v5.8.1/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.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y=
github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
@@ -301,45 +298,35 @@ github.com/go-critic/go-critic v0.8.0 h1:4zOcpvDoKvBOl+R1W81IBznr78f8YaE4zKXkfDV
github.com/go-critic/go-critic v0.8.0/go.mod h1:5TjdkPI9cu/yKbYS96BTsslihjKd6zg6vd8O9RZXj2s=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533 h1:1SRqDZauC9fz6vMIDLCUOULPNfOnZ0rmvZo8quraoy4=
github.com/go-json-experiment/json v0.0.0-20230922184908-dc36ffcf8533/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg=
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo=
github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ=
github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU=
github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4=
github.com/go-openapi/swag v0.22.7 h1:JWrc1uc/P9cSomxfnsFSVWoE1FW6bNbrVPmpQYpCcR8=
github.com/go-openapi/swag v0.22.7/go.mod h1:Gl91UqO+btAM0plGGxHqJcQZ1ZTy6jbmridBTsDy8A0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
@@ -363,14 +350,10 @@ github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUN
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA=
github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU=
@@ -448,10 +431,11 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.18.0 h1:ShE7erKNPqRh5ue6Z9DUOlk04WsnFWPO6YGr3OxnfoQ=
github.com/google/go-containerregistry v0.18.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -479,8 +463,8 @@ github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A=
github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -494,13 +478,10 @@ github.com/goreleaser/fileglob v1.3.0 h1:/X6J7U8lbDpQtBvGcwwPS6OpzkNVlVEsFUVRx9+
github.com/goreleaser/fileglob v1.3.0/go.mod h1:Jx6BoXv3mbYkEzwm9THo7xbr5egkAraxkGorbJb4RxU=
github.com/goreleaser/nfpm/v2 v2.33.1 h1:EkdAzZyVhAI9JC1vjmjjbmnNzyH1J6Cu4JCsA7YcQuc=
github.com/goreleaser/nfpm/v2 v2.33.1/go.mod h1:8wwWWvJWmn84xo/Sqiv0aMvEGTHlHZTXTEuVSgQpkIM=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/csrf v1.7.2 h1:oTUjx0vyf2T+wkrx09Trsev1TE+/EbDAeHtSTbtC2eI=
github.com/gorilla/csrf v1.7.2/go.mod h1:F1Fj3KG23WYHE6gozCmBAezKookxbIvUJT+121wTuLk=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
@@ -525,13 +506,15 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
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/hdevalence/ed25519consensus v0.1.0 h1:jtBwzzcHuTmFrQN6xQZn6CQEO/V9f7HsjsjeEZ6auqU=
github.com/hdevalence/ed25519consensus v0.1.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
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=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
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/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -543,8 +526,8 @@ 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/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a h1:S33o3djA1nPRd+d/bf7jbbXytXuK/EoXow7+aa76grQ=
github.com/insomniacslk/dhcp v0.0.0-20230908212754-65c27093e38a/go.mod h1:zmdm3sTSDP3vOOX3CEWRkkRHtKr1DxBx+J1OQFoDQQs=
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=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -564,10 +547,9 @@ github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/ra
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk=
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jsimonetti/rtnetlink v1.3.5 h1:hVlNQNRlLDGZz31gBPicsG7Q53rnlsz1l1Ix/9XlpVA=
github.com/jsimonetti/rtnetlink v1.3.5/go.mod h1:0LFedyiTkebnd43tE4YAkWGIq9jQphow4CcwxaT2Y00=
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=
@@ -593,9 +575,8 @@ github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkHAIKE/contextcheck v1.1.4 h1:B6zAaLhOEEcjvUgIYEqystmnFk1Oemn8bvJhbt0GMb8=
github.com/kkHAIKE/contextcheck v1.1.4/go.mod h1:1+i/gWqokIa+dm31mqGLZhZJ7Uh44DJGZVmr6QRBNJg=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -606,7 +587,6 @@ github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -623,8 +603,6 @@ github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUc
github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0=
github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo=
github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU=
github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY=
github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM=
@@ -639,21 +617,17 @@ github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1r
github.com/maratori/testpackage v1.1.1/go.mod h1:s4gRK/ym6AMrqpOa/kEbQTV4Q4jb7WeLZzVhVVVOQMc=
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 h1:gWg6ZQ4JhDfJPqlo2srm/LN17lpybq15AryXIRcWYLE=
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
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.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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mbilski/exhaustivestruct v1.2.0 h1:wCBmUnSYufAHO6J4AVWY6ff+oxWxsVFrwgOdMUQePUo=
github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
@@ -666,8 +640,8 @@ github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4=
github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I=
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
@@ -699,23 +673,26 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6Fx
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAVRfPlSo=
github.com/nishanths/exhaustive v0.10.0/go.mod h1:IbwrGdVMizvDcIxPYGVdQn5BqWJaOwpCvg4RGb8r/TA=
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c=
github.com/nunnatsa/ginkgolinter v0.11.2 h1:xzQpAsEyZe5F1RMy2Z5kn8UFCGiWfKqJOUd2ZzBXA4M=
github.com/nunnatsa/ginkgolinter v0.11.2/go.mod h1:dJIGXYXbkBswqa/pIzG0QlVTTDSBMxDoCFwhsl4Uras=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI=
github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
@@ -727,8 +704,8 @@ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNc
github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc=
github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
@@ -750,20 +727,20 @@ github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5Fsn
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.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
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=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y=
github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ=
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.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
@@ -838,10 +815,10 @@ github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag07
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -882,18 +859,18 @@ 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-20230713185742-f0b76a10a08e h1:JyeJF/HuSwvxWtsR1c0oKX1lzaSH5Wh4aX+MgiStaGQ=
github.com/tailscale/golang-x-crypto v0.0.0-20230713185742-f0b76a10a08e/go.mod h1:DjoeCULdP6vTJ/xY+nzzR9LaUHprkbZEpNidX0aqEEk=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E=
github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/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=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/dIMvD0t77g0FxFfZIQjghDQxyG2U=
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKfFTvR00d0b0R0bzRKBW9yrj813fhI=
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734/go.mod h1:6v53VHLmLKUaqWMpSGDeRWhltLSCEteMItYoiKLpdJk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2 h1:lR1voET3dwe3CxacGAiva4k08TXtQ6Dlmult4JILlj4=
github.com/tailscale/web-client-prebuilt v0.0.0-20231213172531-a4fa669015b2/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7 h1:xAgOVncJuuxkFZ2oXXDKFTH4HDdFYSZRYdA6oMrCewg=
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@@ -918,16 +895,12 @@ github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+
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/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90/go.mod h1:lYt+LVfZBBwDZ3+PHk4k/c/TnKOkjJXiJO73E32Mmpc=
github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8=
github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
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=
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e h1:BA9O3BmlTmpjbvajAwzWx4Wo2TRVdpPXZEeemGQcajw=
github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
@@ -970,20 +943,16 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8=
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -998,8 +967,8 @@ 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.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
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=
@@ -1010,16 +979,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-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
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-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k=
golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM=
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/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.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
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=
@@ -1094,8 +1063,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
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.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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=
@@ -1106,8 +1075,8 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4=
golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
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=
@@ -1121,8 +1090,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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.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=
@@ -1141,7 +1110,6 @@ 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=
@@ -1183,7 +1151,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1191,8 +1158,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.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=
@@ -1201,8 +1168,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
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=
@@ -1217,14 +1184,13 @@ golang.org/x/text v0.5.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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
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=
@@ -1298,8 +1264,8 @@ golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
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.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
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=
@@ -1402,8 +1368,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -1417,6 +1383,8 @@ gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
@@ -1433,8 +1401,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-20230928000133-4fe30062272c h1:bYb98Ra11fJ8F2xFbZx0zg2VQ28lYqC1JxfaaF53xqY=
gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
gvisor.dev/gvisor v0.0.0-20240119233241-c9c1d4f9b186 h1:VWRSJX9ghfqsRSZGMAILL6QpYRKWnHcYPi24SCubQRs=
gvisor.dev/gvisor v0.0.0-20240119233241-c9c1d4f9b186/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
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=
@@ -1452,24 +1420,24 @@ inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9 h1:zomTWJvjwLbKRgGameQtpK6DN
inet.af/tcpproxy v0.0.0-20231102063150-2862066fc2a9/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=
inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q=
inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw=
k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg=
k8s.io/apiextensions-apiserver v0.28.2 h1:J6/QRWIKV2/HwBhHRVITMLYoypCoPY1ftigDM0Kn+QU=
k8s.io/apiextensions-apiserver v0.28.2/go.mod h1:5tnkxLGa9nefefYzWuAlWZ7RZYuN/765Au8cWLA6SRg=
k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ=
k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU=
k8s.io/apiserver v0.28.2 h1:rBeYkLvF94Nku9XfXyUIirsVzCzJBs6jMn3NWeHieyI=
k8s.io/apiserver v0.28.2/go.mod h1:f7D5e8wH8MWcKD7azq6Csw9UN+CjdtXIVQUyUhrtb+E=
k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY=
k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY=
k8s.io/component-base v0.28.2 h1:Yc1yU+6AQSlpJZyvehm/NkJBII72rzlEsd6MkBQ+G0E=
k8s.io/component-base v0.28.2/go.mod h1:4IuQPQviQCg3du4si8GpMrhAIegxpsgPngPRR/zWpzc=
k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg=
k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833 h1:iFFEmmB7szQhJP42AvRD2+gzdVP7EuIKY1rJgxf0JZY=
k8s.io/kube-openapi v0.0.0-20230928205116-a78145627833/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI=
k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/api v0.29.1 h1:DAjwWX/9YT7NQD4INu49ROJuZAAAP/Ijki48GUPzxqw=
k8s.io/api v0.29.1/go.mod h1:7Kl10vBRUXhnQQI8YR/R327zXC8eJ7887/+Ybta+RoQ=
k8s.io/apiextensions-apiserver v0.29.1 h1:S9xOtyk9M3Sk1tIpQMu9wXHm5O2MX6Y1kIpPMimZBZw=
k8s.io/apiextensions-apiserver v0.29.1/go.mod h1:zZECpujY5yTW58co8V2EQR4BD6A9pktVgHhvc0uLfeU=
k8s.io/apimachinery v0.29.1 h1:KY4/E6km/wLBguvCZv8cKTeOwwOBqFNjwJIdMkMbbRc=
k8s.io/apimachinery v0.29.1/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU=
k8s.io/apiserver v0.29.1 h1:e2wwHUfEmMsa8+cuft8MT56+16EONIEK8A/gpBSco+g=
k8s.io/apiserver v0.29.1/go.mod h1:V0EpkTRrJymyVT3M49we8uh2RvXf7fWC5XLB0P3SwRw=
k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A=
k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks=
k8s.io/component-base v0.29.1 h1:MUimqJPCRnnHsskTTjKD+IC1EHBbRCVyi37IoFBrkYw=
k8s.io/component-base v0.29.1/go.mod h1:fP9GFjxYrLERq1GcWWZAE3bqbNcDKDytn2srWuHTtKc=
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15 h1:m6dl1pkxz3HuE2mP9MUYPCCGyy6IIFlv/vTlLBDxIwA=
k8s.io/kube-openapi v0.0.0-20240117194847-208609032b15/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I=
@@ -1478,18 +1446,20 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphD
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4=
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8 h1:VuJo4Mt0EVPychre4fNlDWDuE5AjXtPJpRUWqZDQhaI=
mvdan.cc/unparam v0.0.0-20230312165513-e84e2d14e3b8/go.mod h1:Oh/d7dEtzsNHGOq1Cdv8aMm3KdKhVvPbRQcM8WFpBR8=
nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g=
nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
sigs.k8s.io/controller-runtime v0.16.2 h1:mwXAVuEk3EQf478PQwQ48zGOXvW27UJc8NHktQVuIPU=
sigs.k8s.io/controller-runtime v0.16.2/go.mod h1:vpMu3LpI5sYWtujJOa2uPK61nB5rbwlN7BAB8aSLvGU=
sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI=
sigs.k8s.io/controller-tools v0.13.0/go.mod h1:5vw3En2NazbejQGCeWKRrE7q4P+CW8/klfVqP8QZkgA=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk=
sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8=
software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=

View File

@@ -1 +1 @@
7592922bcb2af4e0b49bc67d7923fbdabe168dab
ea90ced9ddc95c09aed7d9c59631aa978022c3ba

2
header.txt Normal file
View File

@@ -0,0 +1,2 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

View File

@@ -360,6 +360,8 @@ func SetDERPRegionHealth(region int, problem string) {
selfCheckLocked()
}
// NoteDERPRegionReceivedFrame is called to note that a frame was received from
// the given DERP region at the current time.
func NoteDERPRegionReceivedFrame(region int) {
mu.Lock()
defer mu.Unlock()
@@ -367,6 +369,15 @@ func NoteDERPRegionReceivedFrame(region int) {
selfCheckLocked()
}
// GetDERPRegionReceivedTime returns the last time that a frame was received
// from the given DERP region, or the zero time if no communication with that
// region has occurred.
func GetDERPRegionReceivedTime(region int) time.Time {
mu.Lock()
defer mu.Unlock()
return derpRegionLastFrame[region]
}
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
func SetIPNState(state string, wantRunning bool) {
mu.Lock()

View File

@@ -175,6 +175,7 @@ type PartialFile struct {
// in-progress '*.partial' file's path when the peerapi isn't
// being used; see LocalBackend.SetDirectFileRoot.
PartialPath string `json:",omitempty"`
FinalPath string `json:",omitempty"`
// Done is set in "direct" mode when the partial file has been
// closed and is ready for the caller to rename away the

View File

@@ -9,7 +9,6 @@ import (
"fmt"
"io"
"net"
"net/netip"
"os"
"os/user"
"runtime"
@@ -18,7 +17,6 @@ import (
"inet.af/peercred"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netstat"
"tailscale.com/safesocket"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -48,6 +46,8 @@ type WindowsToken interface {
// IsElevated reports whether the receiver is currently executing as an
// elevated administrative user.
IsElevated() bool
// IsLocalSystem reports whether the receiver is the built-in SYSTEM user.
IsLocalSystem() bool
// UserDir returns the special directory identified by folderID as associated
// with the receiver. folderID must be one of the KNOWNFOLDERID values from
// the x/sys/windows package, serialized as a stringified GUID.
@@ -207,12 +207,3 @@ func isLocalAdmin(uid string) (bool, error) {
}
return groupmember.IsMemberOfGroup(adminGroup, u.Username)
}
func peerPid(entries []netstat.Entry, la, ra netip.AddrPort) int {
for _, e := range entries {
if e.Local == ra && e.Remote == la {
return e.Pid
}
}
return 0
}

View File

@@ -20,7 +20,7 @@ import (
// based on the user who owns the other end of the connection.
// If c is not backed by a named pipe, an error is returned.
func GetConnIdentity(logf logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
ci = &ConnIdentity{conn: c}
ci = &ConnIdentity{conn: c, notWindows: false}
wcc, ok := c.(*safesocket.WindowsClientConn)
if !ok {
return nil, fmt.Errorf("not a WindowsClientConn: %T", c)
@@ -93,6 +93,12 @@ func (t *token) IsElevated() bool {
return t.t.IsElevated()
}
func (t *token) 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 t.IsUID(systemUID)
}
func (t *token) UserDir(folderID string) (string, error) {
guid, err := windows.GUIDFromString(folderID)
if err != nil {

View File

@@ -378,7 +378,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// invoke it here. For this purpose, it is ok to pass it a zero Arguments.
prefs := b.Prefs().AutoUpdate()
return tailcfg.C2NUpdateResponse{
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply,
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
Supported: clientupdate.CanAutoUpdate(),
}
}

View File

@@ -4,6 +4,7 @@
package ipnlocal
import (
"cmp"
"context"
"encoding/base64"
"encoding/json"
@@ -76,6 +77,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/ptr"
@@ -263,9 +265,8 @@ type LocalBackend struct {
// It's also used on several NAS platforms (Synology, TrueNAS, etc)
// but in that case DoFinalRename is also set true, which moves the
// *.partial file to its final name on completion.
directFileRoot string
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
componentLogUntil map[string]componentLogState
directFileRoot string
componentLogUntil map[string]componentLogState
// c2nUpdateStatus is the status of c2n-triggered client update.
c2nUpdateStatus updateStatus
currentUser ipnauth.WindowsToken
@@ -538,17 +539,6 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
b.directFileRoot = dir
}
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
// a received "name.partial" file to "name" when the download is complete.
//
// This only applies when SetDirectFileRoot is non-empty.
// The default is false.
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
b.mu.Lock()
defer b.mu.Unlock()
b.directFileDoFinalRename = v
}
// ReloadConfig reloads the backend's config from disk.
//
// It returns (false, nil) if not running in declarative mode, (true, nil) on
@@ -772,7 +762,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
}
if !prefs.ExitNodeID().IsZero() {
if exitPeer, ok := b.netMap.PeerWithStableID(prefs.ExitNodeID()); ok {
var online = false
online := false
if v := exitPeer.Online(); v != nil {
online = *v
}
@@ -853,7 +843,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
if p.LastSeen() != nil {
lastSeen = *p.LastSeen()
}
var tailscaleIPs = make([]netip.Addr, 0, p.Addresses().Len())
tailscaleIPs := make([]netip.Addr, 0, p.Addresses().Len())
for i := range p.Addresses().LenIter() {
addr := p.Addresses().At(i)
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
@@ -1072,9 +1062,11 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.blockEngineUpdates(false)
}
if st.LoginFinished() && wasBlocked {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
if st.LoginFinished() && (wasBlocked || b.seamlessRenewalEnabled()) {
if wasBlocked {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
}
b.authReconfig()
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
}
@@ -1106,7 +1098,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
b.authURL = st.URL
b.authURLSticky = st.URL
}
if wasBlocked && st.LoginFinished() {
if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() {
// Interactive login finished successfully (URL visited).
// After an interactive login, the user always wants
// WantRunning.
@@ -1271,8 +1263,8 @@ var preferencePolicies = []preferencePolicyInfo{
},
{
key: syspolicy.ApplyUpdates,
get: func(p ipn.PrefsView) bool { return p.AutoUpdate().Apply },
set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply = v },
get: func(p ipn.PrefsView) bool { v, _ := p.AutoUpdate().Apply.Get(); return v },
set: func(p *ipn.Prefs, v bool) { p.AutoUpdate.Apply.Set(v) },
},
{
key: syspolicy.EnableRunExitNode,
@@ -1331,7 +1323,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
nm.Peers = append(nm.Peers, p)
}
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
return cmp.Compare(a.ID(), b.ID())
})
notify = &ipn.Notify{NetMap: nm}
} else if testenv.InTest() {
@@ -1548,7 +1540,7 @@ func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
defer b.mu.Unlock()
ret := xmaps.Values(b.peers)
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
return cmpx.Compare(a.ID(), b.ID())
return cmp.Compare(a.ID(), b.ID())
})
return ret
}
@@ -1767,25 +1759,26 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// new controlclient. SetPrefs() allows you to overwrite ServerURL,
// but it won't take effect until the next Start().
cc, err := b.getNewControlClientFunc()(controlclient.Options{
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv,
ServerURL: serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
HTTPTestClient: httpTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: debugFlags,
NetMon: b.sys.NetMon.Get(),
Pinger: b,
PopBrowserURL: b.tellClientToBrowseToURL,
OnClientVersion: b.onClientVersion,
OnControlTime: b.em.onControlTime,
Dialer: b.Dialer(),
Observer: b,
C2NHandler: http.HandlerFunc(b.handleC2N),
DialPlan: &b.dialPlan, // pointer because it can't be copied
ControlKnobs: b.sys.ControlKnobs(),
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persistv,
ServerURL: serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
HTTPTestClient: httpTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: debugFlags,
NetMon: b.sys.NetMon.Get(),
Pinger: b,
PopBrowserURL: b.tellClientToBrowseToURL,
OnClientVersion: b.onClientVersion,
OnTailnetDefaultAutoUpdate: b.onTailnetDefaultAutoUpdate,
OnControlTime: b.em.onControlTime,
Dialer: b.Dialer(),
Observer: b,
C2NHandler: http.HandlerFunc(b.handleC2N),
DialPlan: &b.dialPlan, // pointer because it can't be copied
ControlKnobs: b.sys.ControlKnobs(),
// Don't warn about broken Linux IP forwarding when
// netstack is being used.
@@ -2453,8 +2446,10 @@ func (b *LocalBackend) popBrowserAuthNow() {
b.logf("popBrowserAuthNow: url=%v", url != "")
b.blockEngineUpdates(true)
b.stopEngineAndWait()
if !b.seamlessRenewalEnabled() {
b.blockEngineUpdates(true)
b.stopEngineAndWait()
}
b.tellClientToBrowseToURL(url)
if b.State() == ipn.Running {
b.enterState(ipn.Starting)
@@ -2500,6 +2495,32 @@ func (b *LocalBackend) onClientVersion(v *tailcfg.ClientVersion) {
b.send(ipn.Notify{ClientVersion: v})
}
func (b *LocalBackend) onTailnetDefaultAutoUpdate(au bool) {
prefs := b.pm.CurrentPrefs()
if !prefs.Valid() {
b.logf("[unexpected]: received tailnet default auto-update callback but current prefs are nil")
return
}
if _, ok := prefs.AutoUpdate().Apply.Get(); ok {
// Apply was already set from a previous default or manually by the
// user. Tailnet default should not affect us, even if it changes.
return
}
b.logf("using tailnet default auto-update setting: %v", au)
prefsClone := prefs.AsStruct()
prefsClone.AutoUpdate.Apply = opt.NewBool(au)
_, err := b.EditPrefs(&ipn.MaskedPrefs{
Prefs: *prefsClone,
AutoUpdateSet: ipn.AutoUpdatePrefsMask{
ApplySet: true,
},
})
if err != nil {
b.logf("failed to apply tailnet-wide default for auto-updates (%v): %v", au, err)
return
}
}
// For testing lazy machine key generation.
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
@@ -2714,6 +2735,16 @@ func (b *LocalBackend) CheckIPNConnectionAllowed(ci *ipnauth.ConnIdentity) error
if !b.pm.CurrentPrefs().ForceDaemon() {
return nil
}
// Always allow Windows SYSTEM user to connect,
// even if Tailscale is currently being used by another user.
if tok, err := ci.WindowsToken(); err == nil {
defer tok.Close()
if tok.IsLocalSystem() {
return nil
}
}
uid := ci.WindowsUserID()
if uid == "" {
return errors.New("empty user uid in connection identity")
@@ -3415,15 +3446,21 @@ func (b *LocalBackend) reconfigAppConnectorLocked(nm *netmap.NetworkMap, prefs i
})
}
var domains []string
var (
domains []string
routes []netip.Prefix
)
for _, attr := range attrs {
if slices.Contains(attr.Connectors, "*") || selfHasTag(attr.Connectors) {
domains = append(domains, attr.Domains...)
routes = append(routes, attr.Routes...)
}
}
slices.Sort(domains)
slices.Compact(domains)
b.appConnector.UpdateDomains(domains)
slices.SortFunc(routes, func(i, j netip.Prefix) int { return i.Addr().Compare(j.Addr()) })
domains = slices.Compact(domains)
routes = slices.Compact(routes)
b.appConnector.UpdateDomainsAndRoutes(domains, routes)
}
// authReconfig pushes a new configuration into wgengine, if engine
@@ -3844,13 +3881,12 @@ func (b *LocalBackend) initPeerAPIListener() {
ps := &peerAPIServer{
b: b,
taildrop: taildrop.ManagerOptions{
Logf: b.logf,
Clock: tstime.DefaultClock{Clock: b.clock},
State: b.store,
Dir: fileRoot,
DirectFileMode: b.directFileRoot != "",
AvoidFinalRename: !b.directFileDoFinalRename,
SendFileNotify: b.sendFileNotify,
Logf: b.logf,
Clock: tstime.DefaultClock{Clock: b.clock},
State: b.store,
Dir: fileRoot,
DirectFileMode: b.directFileRoot != "",
SendFileNotify: b.sendFileNotify,
}.New(),
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
@@ -3982,10 +4018,13 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
singleRouteThreshold = 1
}
netfilterKind := b.capForcedNetfilter
b.mu.Lock()
netfilterKind := b.capForcedNetfilter // protected by b.mu
b.mu.Unlock()
if prefs.NetfilterKind() != "" {
if b.capForcedNetfilter != "" {
b.logf("nodeattr netfilter preference %s overridden by c2n pref %s", b.capForcedNetfilter, prefs.NetfilterKind())
if netfilterKind != "" {
b.logf("nodeattr netfilter preference %s overridden by c2n pref %s", netfilterKind, prefs.NetfilterKind())
}
netfilterKind = prefs.NetfilterKind()
}
@@ -4079,14 +4118,18 @@ func (b *LocalBackend) applyPrefsToHostinfoLocked(hi *tailcfg.Hostinfo, prefs ip
hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
hi.RequestTags = prefs.AdvertiseTags().AsSlice()
hi.ShieldsUp = prefs.ShieldsUp()
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply
hi.AllowsUpdate = envknob.AllowsRemoteUpdate() || prefs.AutoUpdate().Apply.EqualBool(true)
var sshHostKeys []string
if prefs.RunSSH() && envknob.CanSSHD() {
// TODO(bradfitz): this is called with b.mu held. Not ideal.
// If the filesystem gets wedged or something we could block for
// a long time. But probably fine.
sshHostKeys = b.getSSHHostKeyPublicStrings()
var err error
sshHostKeys, err = b.getSSHHostKeyPublicStrings()
if err != nil {
b.logf("warning: unable to get SSH host keys, SSH will appear as disabled for this node: %v", err)
}
}
hi.SSH_HostKeys = sshHostKeys
@@ -4144,6 +4187,9 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
switch newState {
case ipn.NeedsLogin:
systemd.Status("Needs login: %s", authURL)
if b.seamlessRenewalEnabled() {
break
}
b.blockEngineUpdates(true)
fallthrough
case ipn.Stopped:
@@ -4171,7 +4217,6 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State) {
default:
b.logf("[unexpected] unknown newState %#v", newState)
}
}
// hasNodeKey reports whether a non-zero node key is present in the current
@@ -4515,6 +4560,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
}
b.MagicConn().SetSilentDisco(b.ControlKnobs().SilentDisco.Load())
b.MagicConn().SetProbeUDPLifetime(b.ControlKnobs().ProbeUDPLifetime.Load())
b.setDebugLogsByCapabilityLocked(nm)
@@ -4876,7 +4922,7 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
})
}
slices.SortFunc(ret, func(a, b *apitype.FileTarget) int {
return cmpx.Compare(a.Node.Name, b.Node.Name)
return cmp.Compare(a.Node.Name, b.Node.Name)
})
return ret, nil
}
@@ -5296,15 +5342,6 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
return cc.DoNoiseRequest(req)
}
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
// on prefs. It returns false if there are no prefs set.
func (b *LocalBackend) tailscaleSSHEnabled() bool {
b.mu.Lock()
defer b.mu.Unlock()
p := b.pm.CurrentPrefs()
return p.Valid() && p.RunSSH()
}
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
b.mu.Lock()
defer b.mu.Unlock()
@@ -5752,24 +5789,123 @@ func (b *LocalBackend) ObserveDNSResponse(res []byte) {
appConnector.ObserveDNSResponse(res)
}
// ErrDisallowedAutoRoute is returned by AdvertiseRoute when a route that is not allowed is requested.
var ErrDisallowedAutoRoute = errors.New("route is not allowed")
// AdvertiseRoute implements the appc.RouteAdvertiser interface. It sets a new
// route advertisement if one is not already present in the existing routes.
func (b *LocalBackend) AdvertiseRoute(ipp netip.Prefix) error {
currentRoutes := b.Prefs().AdvertiseRoutes()
// TODO(raggi): check if the new route is a subset of an existing route.
if currentRoutes.ContainsFunc(func(r netip.Prefix) bool { return r == ipp }) {
// If the route is disallowed, ErrDisallowedAutoRoute is returned.
func (b *LocalBackend) AdvertiseRoute(ipps ...netip.Prefix) error {
finalRoutes := b.Prefs().AdvertiseRoutes().AsSlice()
newRoutes := false
for _, ipp := range ipps {
if !allowedAutoRoute(ipp) {
continue
}
if slices.Contains(finalRoutes, ipp) {
continue
}
// If the new prefix is already contained by existing routes, skip it.
if coveredRouteRange(finalRoutes, ipp) {
continue
}
finalRoutes = append(finalRoutes, ipp)
newRoutes = true
}
if !newRoutes {
return nil
}
routes := append(currentRoutes.AsSlice(), ipp)
_, err := b.EditPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{
AdvertiseRoutes: routes,
AdvertiseRoutes: finalRoutes,
},
AdvertiseRoutesSet: true,
})
return err
}
// coveredRouteRange checks if a route is already included in a slice of
// prefixes.
func coveredRouteRange(finalRoutes []netip.Prefix, ipp netip.Prefix) bool {
for _, r := range finalRoutes {
if ipp.IsSingleIP() {
if r.Contains(ipp.Addr()) {
return true
}
} else {
if r.Contains(ipp.Addr()) && r.Contains(netipx.PrefixLastIP(ipp)) {
return true
}
}
}
return false
}
// UnadvertiseRoute implements the appc.RouteAdvertiser interface. It removes
// a route advertisement if one is present in the existing routes.
func (b *LocalBackend) UnadvertiseRoute(toRemove ...netip.Prefix) error {
currentRoutes := b.Prefs().AdvertiseRoutes().AsSlice()
finalRoutes := currentRoutes[:0]
for _, ipp := range currentRoutes {
if slices.Contains(toRemove, ipp) {
continue
}
finalRoutes = append(finalRoutes, ipp)
}
_, err := b.EditPrefs(&ipn.MaskedPrefs{
Prefs: ipn.Prefs{
AdvertiseRoutes: finalRoutes,
},
AdvertiseRoutesSet: true,
})
return err
}
// seamlessRenewalEnabled reports whether seamless key renewals are enabled
// (i.e. we saw our self node with the SeamlessKeyRenewal attr in a netmap).
// This enables beta functionality of renewing node keys without breaking
// connections.
func (b *LocalBackend) seamlessRenewalEnabled() bool {
return b.ControlKnobs().SeamlessKeyRenewal.Load()
}
var (
disallowedAddrs = []netip.Addr{
netip.MustParseAddr("::1"),
netip.MustParseAddr("::"),
netip.MustParseAddr("0.0.0.0"),
}
disallowedRanges = []netip.Prefix{
netip.MustParsePrefix("127.0.0.0/8"),
netip.MustParsePrefix("224.0.0.0/4"),
netip.MustParsePrefix("ff00::/8"),
}
)
// allowedAutoRoute determines if the route being added via AdvertiseRoute (the app connector featuge) should be allowed.
func allowedAutoRoute(ipp netip.Prefix) bool {
// Note: blocking the addrs for globals, not solely the prefixes.
for _, addr := range disallowedAddrs {
if ipp.Addr() == addr {
return false
}
}
for _, pfx := range disallowedRanges {
if pfx.Overlaps(ipp) {
return false
}
}
// TODO(raggi): exclude tailscale service IPs and so on as well.
return true
}
// mayDeref dereferences p if non-nil, otherwise it returns the zero value.
func mayDeref[T any](p *T) (v T) {
if p == nil {

View File

@@ -18,6 +18,7 @@ import (
"go4.org/netipx"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/appc/appctest"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
@@ -31,6 +32,7 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
@@ -264,7 +266,6 @@ func TestPeerRoutes(t *testing.T) {
}
})
}
}
func TestPeerAPIBase(t *testing.T) {
@@ -699,7 +700,6 @@ func TestPacketFilterPermitsUnlockedNodes(t *testing.T) {
}
})
}
}
func TestStatusWithoutPeers(t *testing.T) {
@@ -1170,6 +1170,33 @@ func TestRouteAdvertiser(t *testing.T) {
if routes.Len() != 1 || routes.At(0) != testPrefix {
t.Fatalf("got routes %v, want %v", routes, []netip.Prefix{testPrefix})
}
must.Do(ra.UnadvertiseRoute(testPrefix))
routes = b.Prefs().AdvertiseRoutes()
if routes.Len() != 0 {
t.Fatalf("got routes %v, want none", routes)
}
}
func TestRouterAdvertiserIgnoresContainedRoutes(t *testing.T) {
b := newTestBackend(t)
testPrefix := netip.MustParsePrefix("192.0.0.0/24")
ra := appc.RouteAdvertiser(b)
must.Do(ra.AdvertiseRoute(testPrefix))
routes := b.Prefs().AdvertiseRoutes()
if routes.Len() != 1 || routes.At(0) != testPrefix {
t.Fatalf("got routes %v, want %v", routes, []netip.Prefix{testPrefix})
}
must.Do(ra.AdvertiseRoute(netip.MustParsePrefix("192.0.0.8/32")))
// the above /32 is not added as it is contained within the /24
routes = b.Prefs().AdvertiseRoutes()
if routes.Len() != 1 || routes.At(0) != testPrefix {
t.Fatalf("got routes %v, want %v", routes, []netip.Prefix{testPrefix})
}
}
func TestObserveDNSResponse(t *testing.T) {
@@ -1178,14 +1205,52 @@ func TestObserveDNSResponse(t *testing.T) {
// ensure no error when no app connector is configured
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
rc := &routeCollector{}
rc := &appctest.RouteCollector{}
b.appConnector = appc.NewAppConnector(t.Logf, rc)
b.appConnector.UpdateDomains([]string{"example.com"})
b.appConnector.Wait(context.Background())
b.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8"))
b.appConnector.Wait(context.Background())
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.routes, wantRoutes) {
t.Fatalf("got routes %v, want %v", rc.routes, wantRoutes)
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Fatalf("got routes %v, want %v", rc.Routes(), wantRoutes)
}
}
func TestCoveredRouteRange(t *testing.T) {
tests := []struct {
existingRoute netip.Prefix
newRoute netip.Prefix
want bool
}{
{
existingRoute: netip.MustParsePrefix("192.0.0.1/32"),
newRoute: netip.MustParsePrefix("192.0.0.1/32"),
want: true,
},
{
existingRoute: netip.MustParsePrefix("192.0.0.1/32"),
newRoute: netip.MustParsePrefix("192.0.0.2/32"),
want: false,
},
{
existingRoute: netip.MustParsePrefix("192.0.0.0/24"),
newRoute: netip.MustParsePrefix("192.0.0.1/32"),
want: true,
},
{
existingRoute: netip.MustParsePrefix("192.0.0.0/16"),
newRoute: netip.MustParsePrefix("192.0.0.0/24"),
want: true,
},
}
for _, tt := range tests {
got := coveredRouteRange([]netip.Prefix{tt.existingRoute}, tt.newRoute)
if got != tt.want {
t.Errorf("coveredRouteRange(%v, %v) = %v, want %v", tt.existingRoute, tt.newRoute, got, tt.want)
}
}
}
@@ -1224,6 +1289,7 @@ func TestReconfigureAppConnector(t *testing.T) {
}).View()
b.reconfigAppConnectorLocked(b.netMap, b.pm.prefs)
b.appConnector.Wait(context.Background())
want := []string{"example.com"}
if !slices.Equal(b.appConnector.Domains().AsSlice(), want) {
@@ -1323,16 +1389,6 @@ func dnsResponse(domain, address string) []byte {
return must.Get(b.Finish())
}
// routeCollector is a test helper that collects the list of routes advertised
type routeCollector struct {
routes []netip.Prefix
}
func (rc *routeCollector) AdvertiseRoute(pfx netip.Prefix) error {
rc.routes = append(rc.routes, pfx)
return nil
}
type errorSyspolicyHandler struct {
t *testing.T
err error
@@ -1780,13 +1836,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
Apply: opt.NewBool(false),
},
},
wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantAnyChange: true,
@@ -1799,13 +1855,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: false,
Apply: opt.NewBool(false),
},
},
wantAnyChange: true,
@@ -1818,13 +1874,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: false,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantAnyChange: true,
@@ -1837,13 +1893,13 @@ func TestApplySysPolicy(t *testing.T) {
prefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: true,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantPrefs: ipn.Prefs{
AutoUpdate: ipn.AutoUpdatePrefs{
Check: false,
Apply: true,
Apply: opt.NewBool(true),
},
},
wantAnyChange: true,
@@ -1885,7 +1941,6 @@ func TestApplySysPolicy(t *testing.T) {
})
t.Run("set prefs", func(t *testing.T) {
b := newTestBackend(t)
b.SetPrefs(tt.prefs.Clone())
if !b.Prefs().Equals(tt.wantPrefs.View()) {
@@ -2055,3 +2110,56 @@ func TestPreferencePolicyInfo(t *testing.T) {
})
}
}
func TestOnTailnetDefaultAutoUpdate(t *testing.T) {
tests := []struct {
desc string
before, after opt.Bool
tailnetDefault bool
}{
{
before: opt.Bool(""),
tailnetDefault: true,
after: opt.NewBool(true),
},
{
before: opt.Bool(""),
tailnetDefault: false,
after: opt.NewBool(false),
},
{
before: opt.Bool("unset"),
tailnetDefault: true,
after: opt.NewBool(true),
},
{
before: opt.Bool("unset"),
tailnetDefault: false,
after: opt.NewBool(false),
},
{
before: opt.NewBool(false),
tailnetDefault: true,
after: opt.NewBool(false),
},
{
before: opt.NewBool(true),
tailnetDefault: false,
after: opt.NewBool(true),
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("before=%s after=%s", tt.before, tt.after), func(t *testing.T) {
b := newTestBackend(t)
p := ipn.NewPrefs()
p.AutoUpdate.Apply = tt.before
if err := b.pm.setPrefsLocked(p.View()); err != nil {
t.Fatal(err)
}
b.onTailnetDefaultAutoUpdate(tt.tailnetDefault)
if want, got := tt.after, b.pm.CurrentPrefs().AutoUpdate().Apply; got != want {
t.Errorf("got: %q, want %q", got, want)
}
})
}
}

View File

@@ -62,10 +62,6 @@ type peerAPIServer struct {
taildrop *taildrop.Manager
}
var (
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
)
func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net.Listener, err error) {
// Android for whatever reason often has problems creating the peerapi listener.
// But since we started intercepting it with netstack, it's not even important that

View File

@@ -23,6 +23,7 @@ import (
"go4.org/netipx"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/appc"
"tailscale.com/appc/appctest"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
@@ -114,7 +115,6 @@ func hexAll(v string) string {
}
func TestHandlePeerAPI(t *testing.T) {
const nodeFQDN = "self-node.tail-scale.ts.net."
tests := []struct {
name string
isSelf bool // the peer sending the request is owned by us
@@ -686,10 +686,11 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
}
func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
ctx := context.Background()
var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
rc := &routeCollector{}
rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
h.ps = &peerAPIServer{
@@ -701,6 +702,7 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
},
}
h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
h.ps.b.appConnector.Wait(ctx)
h.ps.resolver = &fakeResolver{}
f := filter.NewAllowAllForTest(logger.Discard)
@@ -718,10 +720,11 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
if w.Code != http.StatusOK {
t.Errorf("unexpected status code: %v", w.Code)
}
h.ps.b.appConnector.Wait(ctx)
wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
if !slices.Equal(rc.routes, wantRoutes) {
t.Errorf("got %v; want %v", rc.routes, wantRoutes)
if !slices.Equal(rc.Routes(), wantRoutes) {
t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
}
}

View File

@@ -4,6 +4,7 @@
package ipnlocal
import (
"cmp"
"encoding/json"
"errors"
"fmt"
@@ -17,7 +18,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cmpx"
)
var errAlreadyMigrated = errors.New("profile migration already completed")
@@ -113,7 +113,7 @@ func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
}
}
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
return cmpx.Compare(a.Name, b.Name)
return cmp.Compare(a.Name, b.Name)
})
return out
}

View File

@@ -34,6 +34,7 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -48,8 +49,7 @@ const (
// current etag of a resource.
var ErrETagMismatch = errors.New("etag mismatch")
// serveHTTPContextKey is the context.Value key for a *serveHTTPContext.
type serveHTTPContextKey struct{}
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
type serveHTTPContext struct {
SrcAddr netip.AddrPort
@@ -433,7 +433,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
hs := &http.Server{
Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context {
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
SrcAddr: srcAddr,
DestPort: dport,
})
@@ -500,11 +500,6 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
return nil
}
func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) {
c, ok = r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
return c, ok
}
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
var z ipn.HTTPHandlerView // zero value
@@ -521,7 +516,7 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
hostname = r.TLS.ServerName
}
sctx, ok := getServeHTTPContext(r)
sctx, ok := serveHTTPContextKey.ValueOk(r.Context())
if !ok {
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false
@@ -610,7 +605,20 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
p := &httputil.ReverseProxy{Rewrite: func(r *httputil.ProxyRequest) {
oldOutPath := r.Out.URL.Path
r.SetURL(rp.url)
// If mount point matches the request path exactly, the outbound
// request URL was set to empty string in serveWebHandler which
// would have resulted in the outbound path set to <proxy path>
// + '/' in SetURL. In that case, if the proxy path was set, we
// want to send the request to the <proxy path> (without the
// '/') .
if oldOutPath == "" && rp.url.Path != "" {
r.Out.URL.Path = rp.url.Path
r.Out.URL.RawPath = rp.url.RawPath
}
r.Out.Host = r.In.Host
addProxyForwardedHeaders(r)
rp.lb.addTailscaleIdentityHeaders(r)
@@ -684,7 +692,7 @@ func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
if r.In.TLS != nil {
r.Out.Header.Set("X-Forwarded-Proto", "https")
}
if c, ok := getServeHTTPContext(r.Out); ok {
if c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()); ok {
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
}
}
@@ -696,7 +704,7 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
r.Out.Header.Del("Tailscale-User-Profile-Pic")
r.Out.Header.Del("Tailscale-Headers-Info")
c, ok := getServeHTTPContext(r.Out)
c, ok := serveHTTPContextKey.ValueOk(r.Out.Context())
if !ok {
return
}

View File

@@ -158,7 +158,7 @@ func TestGetServeHandler(t *testing.T) {
TLS: &tls.ConnectionState{ServerName: serverName},
}
port := cmpx.Or(tt.port, 443)
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
DestPort: port,
}))
@@ -348,7 +348,109 @@ func TestServeConfigETag(t *testing.T) {
}
}
func TestServeHTTPProxy(t *testing.T) {
func TestServeHTTPProxyPath(t *testing.T) {
b := newTestBackend(t)
// Start test serve endpoint.
testServ := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Set the request URL path to a response header, so the
// requested URL path can be checked in tests.
t.Logf("adding path %s", r.URL.Path)
w.Header().Add("Path", r.URL.Path)
},
))
defer testServ.Close()
tests := []struct {
name string
mountPoint string
proxyPath string
requestPath string
wantRequestPath string
}{
{
name: "/foo -> /foo, with mount point and path /foo",
mountPoint: "/foo",
proxyPath: "/foo",
requestPath: "/foo",
wantRequestPath: "/foo",
},
{
name: "/foo/ -> /foo/, with mount point and path /foo",
mountPoint: "/foo",
proxyPath: "/foo",
requestPath: "/foo/",
wantRequestPath: "/foo/",
},
{
name: "/foo -> /foo/, with mount point and path /foo/",
mountPoint: "/foo/",
proxyPath: "/foo/",
requestPath: "/foo",
wantRequestPath: "/foo/",
},
{
name: "/-> /, with mount point and path /",
mountPoint: "/",
proxyPath: "/",
requestPath: "/",
wantRequestPath: "/",
},
{
name: "/foo -> /foo, with mount point and path /",
mountPoint: "/",
proxyPath: "/",
requestPath: "/foo",
wantRequestPath: "/foo",
},
{
name: "/foo/bar -> /foo/bar, with mount point and path /foo",
mountPoint: "/foo",
proxyPath: "/foo",
requestPath: "/foo/bar",
wantRequestPath: "/foo/bar",
},
{
name: "/foo/bar/baz -> /foo/bar/baz, with mount point and path /foo",
mountPoint: "/foo",
proxyPath: "/foo",
requestPath: "/foo/bar/baz",
wantRequestPath: "/foo/bar/baz",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
conf := &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
tt.mountPoint: {Proxy: testServ.URL + tt.proxyPath},
}},
},
}
if err := b.SetServeConfig(conf, ""); err != nil {
t.Fatal(err)
}
req := &http.Request{
URL: &url.URL{Path: tt.requestPath},
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
}
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(),
&serveHTTPContext{
DestPort: 443,
SrcAddr: netip.MustParseAddrPort("1.2.3.4:1234"), // random src
}))
w := httptest.NewRecorder()
b.serveWebHandler(w, req)
// Verify what path was requested
p := w.Result().Header.Get("Path")
if p != tt.wantRequestPath {
t.Errorf("wanted request path %s got %s", tt.wantRequestPath, p)
}
})
}
}
func TestServeHTTPProxyHeaders(t *testing.T) {
b := newTestBackend(t)
// Start test serve endpoint.
@@ -428,7 +530,7 @@ func TestServeHTTPProxy(t *testing.T) {
URL: &url.URL{Path: "/"},
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
}
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
DestPort: 443,
SrcAddr: netip.MustParseAddrPort(tt.srcIP + ":1234"), // random src port for tests
}))

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