Compare commits
177 Commits
bradfitz/r
...
awly/cli-j
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
776ab357b1 | ||
|
|
a93dc6cdb1 | ||
|
|
7bac5dffcb | ||
|
|
b3fc345aba | ||
|
|
9106187a95 | ||
|
|
9b08399d9e | ||
|
|
153a476957 | ||
|
|
227509547f | ||
|
|
e3f047618b | ||
|
|
91d2e1772d | ||
|
|
3b6849e362 | ||
|
|
0fd73746dd | ||
|
|
17c88a19be | ||
|
|
25f0a3fc8f | ||
|
|
a7a394e7d9 | ||
|
|
07e2487c1d | ||
|
|
1dd9c44d51 | ||
|
|
0a6eb12f05 | ||
|
|
f205efcf18 | ||
|
|
a917718353 | ||
|
|
4099a36468 | ||
|
|
d9d9d525d9 | ||
|
|
9939374c48 | ||
|
|
4055b63b9b | ||
|
|
f0230ce0b5 | ||
|
|
cc370314e7 | ||
|
|
655b4f8fc5 | ||
|
|
004dded0a8 | ||
|
|
0def4f8e38 | ||
|
|
7bc2ddaedc | ||
|
|
949b15d858 | ||
|
|
8a8ecac6a7 | ||
|
|
eead25560f | ||
|
|
1b64961320 | ||
|
|
32308fcf71 | ||
|
|
34de96d06e | ||
|
|
575feb486f | ||
|
|
2ab1d532e8 | ||
|
|
360046e5c3 | ||
|
|
35a8fca379 | ||
|
|
19b0c8a024 | ||
|
|
3088c6105e | ||
|
|
a21bf100f3 | ||
|
|
1bf7ed0348 | ||
|
|
c5623e0471 | ||
|
|
1bf82ddf84 | ||
|
|
6840f471c0 | ||
|
|
90be06bd5b | ||
|
|
cf97cff33b | ||
|
|
855da47777 | ||
|
|
43375c6efb | ||
|
|
ba7f2d129e | ||
|
|
57856fc0d5 | ||
|
|
9904421853 | ||
|
|
5d09649b0b | ||
|
|
d500a92926 | ||
|
|
1f94047475 | ||
|
|
bd54b61746 | ||
|
|
20562a4fb9 | ||
|
|
e7bf6e716b | ||
|
|
32ce18716b | ||
|
|
0f57b9340b | ||
|
|
b2c522ce95 | ||
|
|
54f58d1143 | ||
|
|
485018696a | ||
|
|
1608831c33 | ||
|
|
d3af54444c | ||
|
|
d97cddd876 | ||
|
|
f77821fd63 | ||
|
|
0b32adf9ec | ||
|
|
1ac14d7216 | ||
|
|
4ff276cf52 | ||
|
|
2742153f84 | ||
|
|
646990a7d0 | ||
|
|
8882c6b730 | ||
|
|
35d2efd692 | ||
|
|
fc074a6b9f | ||
|
|
014bf25c0a | ||
|
|
0834712c91 | ||
|
|
fec41e4904 | ||
|
|
fd0acc4faf | ||
|
|
380a3a0834 | ||
|
|
5d61d1c7b0 | ||
|
|
9609b26541 | ||
|
|
7403d8e9a8 | ||
|
|
f0b9d3f477 | ||
|
|
3f3edeec07 | ||
|
|
808b4139ee | ||
|
|
49bf63cdd0 | ||
|
|
d209b032ab | ||
|
|
fc28c8e7f3 | ||
|
|
b7c3cfe049 | ||
|
|
8d7b78f3f7 | ||
|
|
041733d3d1 | ||
|
|
874972b683 | ||
|
|
b546a6e758 | ||
|
|
c6af5bbfe8 | ||
|
|
e92f4c6af8 | ||
|
|
986d60a094 | ||
|
|
6a982faa7d | ||
|
|
c8f258a904 | ||
|
|
726d5d507d | ||
|
|
2238ca8a05 | ||
|
|
8bd442ba8c | ||
|
|
7b1c764088 | ||
|
|
b8af91403d | ||
|
|
e21d8768f9 | ||
|
|
5576972261 | ||
|
|
ba517ab388 | ||
|
|
2b638f550d | ||
|
|
9102a5bb73 | ||
|
|
c8fe9f0064 | ||
|
|
42dac7c5c2 | ||
|
|
d2fef01206 | ||
|
|
9df107f4f0 | ||
|
|
e181f12a7b | ||
|
|
c4b20c5411 | ||
|
|
01a7726cf7 | ||
|
|
309afa53cf | ||
|
|
42f01afe26 | ||
|
|
59936e6d4a | ||
|
|
732af2f6e0 | ||
|
|
458decdeb0 | ||
|
|
4e5ef5b628 | ||
|
|
012933635b | ||
|
|
da32468988 | ||
|
|
ddf94a7b39 | ||
|
|
b56058d7e3 | ||
|
|
d780755340 | ||
|
|
489b990240 | ||
|
|
d15250aae9 | ||
|
|
8965e87fa8 | ||
|
|
114d1caf55 | ||
|
|
b565a9faa7 | ||
|
|
781f79408d | ||
|
|
4651827f20 | ||
|
|
8f7588900a | ||
|
|
0bb82561ba | ||
|
|
2064dc20d4 | ||
|
|
23c5870bd3 | ||
|
|
18939df0a7 | ||
|
|
1d6ab9f9db | ||
|
|
210264f942 | ||
|
|
6b801a8e9e | ||
|
|
b3f91845dc | ||
|
|
46fda6bf4c | ||
|
|
9766f0e110 | ||
|
|
94defc4056 | ||
|
|
b292f7f9ac | ||
|
|
5f177090e3 | ||
|
|
0323dd01b2 | ||
|
|
8487fd2ec2 | ||
|
|
a6b13e6972 | ||
|
|
75254178a0 | ||
|
|
787ead835f | ||
|
|
6e55d8f6a1 | ||
|
|
30f8d8199a | ||
|
|
da078b4c09 | ||
|
|
53a5d00fff | ||
|
|
8161024176 | ||
|
|
a475c435ec | ||
|
|
27033c6277 | ||
|
|
d5e692f7e7 | ||
|
|
94415e8029 | ||
|
|
3485e4bf5a | ||
|
|
7eb8a77ac8 | ||
|
|
24a40f54d9 | ||
|
|
d91e5c25ce | ||
|
|
ded7734c36 | ||
|
|
200d92121f | ||
|
|
7dd76c3411 | ||
|
|
591979b95f | ||
|
|
91786ff958 | ||
|
|
5ffb2668ef | ||
|
|
d7a4f9d31c | ||
|
|
0d6e71df70 | ||
|
|
dcb0f189cc |
10
.github/workflows/checklocks.yml
vendored
10
.github/workflows/checklocks.yml
vendored
@@ -24,5 +24,11 @@ jobs:
|
||||
run: ./tool/go build -o /tmp/checklocks gvisor.dev/gvisor/tools/checklocks/cmd/checklocks
|
||||
|
||||
- name: Run checklocks vet
|
||||
# TODO: remove || true once we have applied checklocks annotations everywhere.
|
||||
run: ./tool/go vet -vettool=/tmp/checklocks ./... || true
|
||||
# TODO(#12625): add more packages as we add annotations
|
||||
run: |-
|
||||
./tool/go vet -vettool=/tmp/checklocks \
|
||||
./envknob \
|
||||
./ipn/store/mem \
|
||||
./net/stun/stuntest \
|
||||
./net/wsconn \
|
||||
./proxymap
|
||||
|
||||
5
.github/workflows/installer.yml
vendored
5
.github/workflows/installer.yml
vendored
@@ -67,6 +67,11 @@ jobs:
|
||||
image: ${{ matrix.image }}
|
||||
options: --user root
|
||||
steps:
|
||||
- name: install dependencies (pacman)
|
||||
# Refresh the package databases to ensure that the tailscale package is
|
||||
# defined.
|
||||
run: pacman -Sy
|
||||
if: contains(matrix.image, 'archlinux')
|
||||
- name: install dependencies (yum)
|
||||
# tar and gzip are needed by the actions/checkout below.
|
||||
run: yum install -y --allowerasing tar gzip ${{ matrix.deps }}
|
||||
|
||||
14
Dockerfile
14
Dockerfile
@@ -1,17 +1,13 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
############################################################################
|
||||
# Note that this Dockerfile is currently NOT used to build any of the published
|
||||
# Tailscale container images and may have drifted from the image build mechanism
|
||||
# we use.
|
||||
# Tailscale images are currently built using https://github.com/tailscale/mkctr,
|
||||
# and the build script can be found in ./build_docker.sh.
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in container
|
||||
# environments, such as Docker and Kubernetes. Though it should work, we
|
||||
# don't regularly test it, and we know there are some feature limitations.
|
||||
#
|
||||
# See current bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -21,6 +21,7 @@ updatedeps: ## Update depaware deps
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
|
||||
depaware: ## Run depaware checks
|
||||
@@ -30,6 +31,7 @@ depaware: ## Run depaware checks
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
|
||||
buildwindows: ## Build tailscale CLI for windows/amd64
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.69.0
|
||||
1.71.0
|
||||
|
||||
3
api.md
3
api.md
@@ -1,3 +1,6 @@
|
||||
> [!IMPORTANT]
|
||||
> The Tailscale API documentation has moved to https://tailscale.com/api
|
||||
|
||||
# Tailscale API
|
||||
|
||||
The Tailscale API documentation is located in **[tailscale/publicapi](./publicapi/readme.md#tailscale-api)**.
|
||||
|
||||
@@ -11,6 +11,7 @@ package appc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -78,6 +80,42 @@ type RouteAdvertiser interface {
|
||||
UnadvertiseRoute(...netip.Prefix) error
|
||||
}
|
||||
|
||||
var (
|
||||
metricStoreRoutesRateBuckets = []int64{1, 2, 3, 4, 5, 10, 100, 1000}
|
||||
metricStoreRoutesNBuckets = []int64{1, 2, 3, 4, 5, 10, 100, 1000, 10000}
|
||||
metricStoreRoutesRate []*clientmetric.Metric
|
||||
metricStoreRoutesN []*clientmetric.Metric
|
||||
)
|
||||
|
||||
func initMetricStoreRoutes() {
|
||||
for _, n := range metricStoreRoutesRateBuckets {
|
||||
metricStoreRoutesRate = append(metricStoreRoutesRate, clientmetric.NewCounter(fmt.Sprintf("appc_store_routes_rate_%d", n)))
|
||||
}
|
||||
metricStoreRoutesRate = append(metricStoreRoutesRate, clientmetric.NewCounter("appc_store_routes_rate_over"))
|
||||
for _, n := range metricStoreRoutesNBuckets {
|
||||
metricStoreRoutesN = append(metricStoreRoutesN, clientmetric.NewCounter(fmt.Sprintf("appc_store_routes_n_routes_%d", n)))
|
||||
}
|
||||
metricStoreRoutesN = append(metricStoreRoutesN, clientmetric.NewCounter("appc_store_routes_n_routes_over"))
|
||||
}
|
||||
|
||||
func recordMetric(val int64, buckets []int64, metrics []*clientmetric.Metric) {
|
||||
if len(buckets) < 1 {
|
||||
return
|
||||
}
|
||||
// finds the first bucket where val <=, or len(buckets) if none match
|
||||
// for bucket values of 1, 10, 100; 0-1 goes to [0], 2-10 goes to [1], 11-100 goes to [2], 101+ goes to [3]
|
||||
bucket, _ := slices.BinarySearch(buckets, val)
|
||||
metrics[bucket].Add(1)
|
||||
}
|
||||
|
||||
func metricStoreRoutes(rate, nRoutes int64) {
|
||||
if len(metricStoreRoutesRate) == 0 {
|
||||
initMetricStoreRoutes()
|
||||
}
|
||||
recordMetric(rate, metricStoreRoutesRateBuckets, metricStoreRoutesRate)
|
||||
recordMetric(nRoutes, metricStoreRoutesNBuckets, metricStoreRoutesN)
|
||||
}
|
||||
|
||||
// RouteInfo is a data structure used to persist the in memory state of an AppConnector
|
||||
// so that we can know, even after a restart, which routes came from ACLs and which were
|
||||
// learned from domains.
|
||||
@@ -141,6 +179,7 @@ func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, routeInf
|
||||
}
|
||||
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, l)
|
||||
metricStoreRoutes(c, l)
|
||||
})
|
||||
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, l)
|
||||
@@ -442,8 +481,10 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
if len(toAdvertise) > 0 {
|
||||
e.logf("[v2] observed new routes for %s: %s", domain, toAdvertise)
|
||||
e.scheduleAdvertisement(domain, toAdvertise...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
@@ -569,3 +570,35 @@ func TestRateLogger(t *testing.T) {
|
||||
t.Fatalf("wasCalled: got false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRouteStoreMetrics(t *testing.T) {
|
||||
metricStoreRoutes(1, 1)
|
||||
metricStoreRoutes(1, 1) // the 1 buckets value should be 2
|
||||
metricStoreRoutes(5, 5) // the 5 buckets value should be 1
|
||||
metricStoreRoutes(6, 6) // the 10 buckets value should be 1
|
||||
metricStoreRoutes(10001, 10001) // the over buckets value should be 1
|
||||
wanted := map[string]int64{
|
||||
"appc_store_routes_n_routes_1": 2,
|
||||
"appc_store_routes_rate_1": 2,
|
||||
"appc_store_routes_n_routes_5": 1,
|
||||
"appc_store_routes_rate_5": 1,
|
||||
"appc_store_routes_n_routes_10": 1,
|
||||
"appc_store_routes_rate_10": 1,
|
||||
"appc_store_routes_n_routes_over": 1,
|
||||
"appc_store_routes_rate_over": 1,
|
||||
}
|
||||
for _, x := range clientmetric.Metrics() {
|
||||
if x.Value() != wanted[x.Name()] {
|
||||
t.Errorf("%s: want: %d, got: %d", x.Name(), wanted[x.Name()], x.Value())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricBucketsAreSorted(t *testing.T) {
|
||||
if !slices.IsSorted(metricStoreRoutesRateBuckets) {
|
||||
t.Errorf("metricStoreRoutesRateBuckets must be in order")
|
||||
}
|
||||
if !slices.IsSorted(metricStoreRoutesNBuckets) {
|
||||
t.Errorf("metricStoreRoutesNBuckets must be in order")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package appctest contains code to help test App Connectors.
|
||||
package appctest
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Runs `go build` with flags configured for docker distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries inside docker, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in container
|
||||
# environments, such as Docker and Kubernetes. Though it should work, we
|
||||
# don't regularly test it, and we know there are some feature limitations.
|
||||
#
|
||||
# See current bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
# This script builds Tailscale container images using
|
||||
# github.com/tailscale/mkctr.
|
||||
# By default the images will be tagged with the current version and git
|
||||
# hash of this repository as produced by ./cmd/mkversion.
|
||||
# This is the image build mechanim used to build the official Tailscale
|
||||
# container images.
|
||||
|
||||
set -eu
|
||||
|
||||
@@ -49,7 +39,7 @@ case "$TARGET" in
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--gotags="ts_kube" \
|
||||
--gotags="ts_kube,ts_package_container" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
|
||||
@@ -37,6 +37,16 @@ type ACLTest struct {
|
||||
Allow []string `json:"allow,omitempty"` // old name for accept
|
||||
}
|
||||
|
||||
// NodeAttrGrant defines additional string attributes that apply to specific devices.
|
||||
type NodeAttrGrant struct {
|
||||
// Target specifies which nodes the attributes apply to. The nodes can be a
|
||||
// tag (tag:server), user (alice@example.com), group (group:kids), or *.
|
||||
Target []string `json:"target,omitempty"`
|
||||
|
||||
// Attr are the attributes to set on Target(s).
|
||||
Attr []string `json:"attr,omitempty"`
|
||||
}
|
||||
|
||||
// ACLDetails contains all the details for an ACL.
|
||||
type ACLDetails struct {
|
||||
Tests []ACLTest `json:"tests,omitempty"`
|
||||
@@ -44,6 +54,7 @@ type ACLDetails struct {
|
||||
Groups map[string][]string `json:"groups,omitempty"`
|
||||
TagOwners map[string][]string `json:"tagowners,omitempty"`
|
||||
Hosts map[string]string `json:"hosts,omitempty"`
|
||||
NodeAttrs []NodeAttrGrant `json:"nodeAttrs,omitempty"`
|
||||
}
|
||||
|
||||
// ACL contains an ACLDetails and metadata.
|
||||
@@ -150,7 +161,12 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
|
||||
// ACLTestFailureSummary specifies the JSON format sent to the
|
||||
// JavaScript client to be rendered in the HTML.
|
||||
type ACLTestFailureSummary struct {
|
||||
User string `json:"user,omitempty"`
|
||||
// User is the source ("src") value of the ACL test that failed.
|
||||
// The name "user" is a legacy holdover from the original naming and
|
||||
// is kept for compatibility but it may also contain any value
|
||||
// that's valid in a ACL test "src" field.
|
||||
User string `json:"user,omitempty"`
|
||||
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
|
||||
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(lc.socket())
|
||||
return safesocket.ConnectContext(ctx, lc.socket())
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
@@ -933,7 +933,20 @@ func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err e
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
|
||||
return lc.CertPairWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// CertPairWithValidity returns a cert and private key for the provided DNS
|
||||
// domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
// When minValidity is non-zero, the returned certificate will be valid for at
|
||||
// least the given duration, if permitted by the CA. If the certificate is
|
||||
// valid, but for less than minValidity, it will be synchronously renewed.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "18.16.1",
|
||||
"node": "18.20.4",
|
||||
"yarn": "1.22.19"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -248,6 +248,11 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
// CanAutoUpdate reports whether auto-updating via the clientupdate package
|
||||
// is supported for the current os/distro.
|
||||
func CanAutoUpdate() bool {
|
||||
if version.IsMacSysExt() {
|
||||
// Macsys uses Sparkle for auto-updates, which doesn't have an update
|
||||
// function in this package.
|
||||
return true
|
||||
}
|
||||
_, canAutoUpdate := (&Updater{}).getUpdateFunction()
|
||||
return canAutoUpdate
|
||||
}
|
||||
|
||||
@@ -78,7 +78,11 @@ func main() {
|
||||
w(" return false")
|
||||
w("}")
|
||||
}
|
||||
cloneOutput := pkg.Name + "_clone.go"
|
||||
cloneOutput := pkg.Name + "_clone"
|
||||
if *flagBuildTags == "test" {
|
||||
cloneOutput += "_test"
|
||||
}
|
||||
cloneOutput += ".go"
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -91,16 +95,19 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
}
|
||||
|
||||
name := typ.Obj().Name()
|
||||
typeParams := typ.Origin().TypeParams()
|
||||
_, typeParamNames := codegen.FormatTypeParams(typeParams, it)
|
||||
nameWithParams := name + typeParamNames
|
||||
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
|
||||
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", nameWithParams, nameWithParams)
|
||||
writef := func(format string, args ...any) {
|
||||
fmt.Fprintf(buf, "\t"+format+"\n", args...)
|
||||
}
|
||||
writef("if src == nil {")
|
||||
writef("\treturn nil")
|
||||
writef("}")
|
||||
writef("dst := new(%s)", name)
|
||||
writef("dst := new(%s)", nameWithParams)
|
||||
writef("*dst = *src")
|
||||
for i := range t.NumFields() {
|
||||
fname := t.Field(i).Name()
|
||||
@@ -126,16 +133,23 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
|
||||
writef("for i := range dst.%s {", fname)
|
||||
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("if src.%s[i] == nil { dst.%s[i] = nil } else {", fname, fname)
|
||||
writef("\tdst.%s[i] = ptr.To(*src.%s[i])", fname, fname)
|
||||
writef("}")
|
||||
writef("if src.%s[i] == nil { dst.%s[i] = nil } else {", fname, fname)
|
||||
if codegen.ContainsPointers(ptr.Elem()) {
|
||||
if _, isIface := ptr.Elem().Underlying().(*types.Interface); isIface {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\tdst.%s[i] = ptr.To((*src.%s[i]).Clone())", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\tdst.%s[i] = ptr.To(*src.%s[i])", fname, fname)
|
||||
}
|
||||
writef("}")
|
||||
} else if ft.Elem().String() == "encoding/json.RawMessage" {
|
||||
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
|
||||
} else if _, isIface := ft.Elem().Underlying().(*types.Interface); isIface {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
@@ -145,14 +159,19 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
|
||||
}
|
||||
case *types.Pointer:
|
||||
if named, _ := ft.Elem().(*types.Named); named != nil && codegen.ContainsPointers(ft.Elem()) {
|
||||
base := ft.Elem()
|
||||
hasPtrs := codegen.ContainsPointers(base)
|
||||
if named, _ := base.(*types.Named); named != nil && hasPtrs {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = ptr.To(*src.%s)", fname, fname)
|
||||
if codegen.ContainsPointers(ft.Elem()) {
|
||||
if _, isIface := base.Underlying().(*types.Interface); isIface && hasPtrs {
|
||||
writef("\tdst.%s = ptr.To((*src.%s).Clone())", fname, fname)
|
||||
} else if !hasPtrs {
|
||||
writef("\tdst.%s = ptr.To(*src.%s)", fname, fname)
|
||||
} else {
|
||||
writef("\t" + `panic("TODO pointers in pointers")`)
|
||||
}
|
||||
writef("}")
|
||||
@@ -172,18 +191,50 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(elem))
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
switch elem.(type) {
|
||||
|
||||
switch elem := elem.Underlying().(type) {
|
||||
case *types.Pointer:
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
writef("\t\tif v == nil { dst.%s[k] = nil } else {", fname)
|
||||
if base := elem.Elem().Underlying(); codegen.ContainsPointers(base) {
|
||||
if _, isIface := base.(*types.Interface); isIface {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\t\t\tdst.%s[k] = ptr.To((*v).Clone())", fname)
|
||||
} else {
|
||||
writef("\t\t\tdst.%s[k] = v.Clone()", fname)
|
||||
}
|
||||
} else {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\t\t\tdst.%s[k] = ptr.To(*v)", fname)
|
||||
}
|
||||
writef("}")
|
||||
case *types.Interface:
|
||||
if cloneResultType := methodResultType(elem, "Clone"); cloneResultType != nil {
|
||||
if _, isPtr := cloneResultType.(*types.Pointer); isPtr {
|
||||
writef("\t\tdst.%s[k] = *(v.Clone())", fname)
|
||||
} else {
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
}
|
||||
} else {
|
||||
writef(`panic("%s (%v) does not have a Clone method")`, fname, elem)
|
||||
}
|
||||
default:
|
||||
writef("\t\tdst.%s[k] = *(v.Clone())", fname)
|
||||
}
|
||||
|
||||
writef("\t}")
|
||||
writef("}")
|
||||
} else {
|
||||
it.Import("maps")
|
||||
writef("\tdst.%s = maps.Clone(src.%s)", fname, fname)
|
||||
}
|
||||
case *types.Interface:
|
||||
// If ft is an interface with a "Clone() ft" method, it can be used to clone the field.
|
||||
// This includes scenarios where ft is a constrained type parameter.
|
||||
if cloneResultType := methodResultType(ft, "Clone"); cloneResultType.Underlying() == ft {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
writef(`panic("%s (%v) does not have a compatible Clone method")`, fname, ft)
|
||||
default:
|
||||
writef(`panic("TODO: %s (%T)")`, fname, ft)
|
||||
}
|
||||
@@ -191,7 +242,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("return dst")
|
||||
fmt.Fprintf(buf, "}\n\n")
|
||||
|
||||
buf.Write(codegen.AssertStructUnchanged(t, name, "Clone", it))
|
||||
buf.Write(codegen.AssertStructUnchanged(t, name, typeParams, "Clone", it))
|
||||
}
|
||||
|
||||
// hasBasicUnderlying reports true when typ.Underlying() is a slice or a map.
|
||||
@@ -203,3 +254,15 @@ func hasBasicUnderlying(typ types.Type) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func methodResultType(typ types.Type, method string) types.Type {
|
||||
viewMethod := codegen.LookupMethod(typ, method)
|
||||
if viewMethod == nil {
|
||||
return nil
|
||||
}
|
||||
sig, ok := viewMethod.Type().(*types.Signature)
|
||||
if !ok || sig.Results().Len() != 1 {
|
||||
return nil
|
||||
}
|
||||
return sig.Results().At(0).Type()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer
|
||||
|
||||
// Package clonerex is an example package for the cloner tool.
|
||||
package clonerex
|
||||
|
||||
type SliceContainer struct {
|
||||
|
||||
@@ -476,18 +476,20 @@ runLoop:
|
||||
newCurentEgressIPs = deephash.Hash(&egressAddrs)
|
||||
egressIPsHaveChanged = newCurentEgressIPs != currentEgressIPs
|
||||
if egressIPsHaveChanged && len(egressAddrs) != 0 {
|
||||
var rulesInstalled bool
|
||||
for _, egressAddr := range egressAddrs {
|
||||
ea := egressAddr.Addr()
|
||||
// TODO (irbekrm): make it work for IPv6 too.
|
||||
if ea.Is6() {
|
||||
log.Println("Not installing egress forwarding rules for IPv6 as this is currently not supported")
|
||||
continue
|
||||
}
|
||||
log.Printf("Installing forwarding rules for destination %v", ea.String())
|
||||
if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil {
|
||||
log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err)
|
||||
if ea.Is4() || (ea.Is6() && nfr.HasIPV6NAT()) {
|
||||
rulesInstalled = true
|
||||
log.Printf("Installing forwarding rules for destination %v", ea.String())
|
||||
if err := installEgressForwardingRule(ctx, ea.String(), addrs, nfr); err != nil {
|
||||
log.Fatalf("installing egress proxy rules for destination %s: %v", ea.String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !rulesInstalled {
|
||||
log.Fatalf("no forwarding rules for egress addresses %v, host supports IPv6: %v", egressAddrs, nfr.HasIPV6NAT())
|
||||
}
|
||||
}
|
||||
currentEgressIPs = newCurentEgressIPs
|
||||
}
|
||||
@@ -941,7 +943,7 @@ func enableIPForwarding(v4Forwarding, v6Forwarding bool, root string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func installEgressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
func installEgressForwardingRule(_ context.Context, dstStr string, tsIPs []netip.Prefix, nfr linuxfw.NetfilterRunner) error {
|
||||
dst, err := netip.ParseAddr(dstStr)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
}
|
||||
defer kube.Close()
|
||||
|
||||
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: func(s string) *string { return &s }("foo"), Version: "alpha0"}
|
||||
tailscaledConf := &ipn.ConfigVAlpha{AuthKey: ptr.To("foo"), Version: "alpha0"}
|
||||
tailscaledConfBytes, err := json.Marshal(tailscaledConf)
|
||||
if err != nil {
|
||||
t.Fatalf("error unmarshaling tailscaled config: %v", err)
|
||||
@@ -116,6 +116,9 @@ func TestContainerBoot(t *testing.T) {
|
||||
// WantFiles files that should exist in the container and their
|
||||
// contents.
|
||||
WantFiles map[string]string
|
||||
// WantFatalLog is the fatal log message we expect from containerboot.
|
||||
// If set for a phase, the test will finish on that phase.
|
||||
WantFatalLog string
|
||||
}
|
||||
runningNotify := &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
@@ -349,12 +352,57 @@ func TestContainerBoot(t *testing.T) {
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "egress_proxy_fqdn_ipv6_target_on_ipv4_host",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_TAILNET_TARGET_FQDN": "ipv6-node.test.ts.net", // resolves to IPv6 address
|
||||
"TS_USERSPACE": "false",
|
||||
"TS_TEST_FAKE_NETFILTER_6": "false",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
WantFiles: map[string]string{
|
||||
"proc/sys/net/ipv4/ip_forward": "1",
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": "0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: &ipn.Notify{
|
||||
State: ptr.To(ipn.Running),
|
||||
NetMap: &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("myID"),
|
||||
Name: "test-node.test.ts.net",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
StableID: tailcfg.StableNodeID("ipv6ID"),
|
||||
Name: "ipv6-node.test.ts.net",
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("::1/128")},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
WantFatalLog: "no forwarding rules for egress addresses [::1/128], host supports IPv6: false",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "authkey_once",
|
||||
Env: map[string]string{
|
||||
@@ -697,6 +745,25 @@ func TestContainerBoot(t *testing.T) {
|
||||
var wantCmds []string
|
||||
for i, p := range test.Phases {
|
||||
lapi.Notify(p.Notify)
|
||||
if p.WantFatalLog != "" {
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
state, err := cmd.Process.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if state.ExitCode() != 1 {
|
||||
return fmt.Errorf("process exited with code %d but wanted %d", state.ExitCode(), 1)
|
||||
}
|
||||
waitLogLine(t, time.Second, cbOut, p.WantFatalLog)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Early test return, we don't expect the successful startup log message.
|
||||
return
|
||||
}
|
||||
wantCmds = append(wantCmds, p.WantCmds...)
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||
err := tstest.WaitFor(2*time.Second, func() error {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
This is the code for the [Tailscale DERP server](https://tailscale.com/kb/1232/derp-servers).
|
||||
|
||||
In general, you should not need to nor want to run this code. The overwhelming majority of Tailscale users (both individuals and companies) do not.
|
||||
In general, you should not need to or want to run this code. The overwhelming
|
||||
majority of Tailscale users (both individuals and companies) do not.
|
||||
|
||||
In the happy path, Tailscale establishes direct connections between peers and
|
||||
data plane traffic flows directly between them, without using DERP for more than
|
||||
@@ -11,7 +12,7 @@ find yourself wanting DERP for more bandwidth, the real problem is usually the
|
||||
network configuration of your Tailscale node(s), making sure that Tailscale can
|
||||
get direction connections via some mechanism.
|
||||
|
||||
But if you've decided or been advised to run your own `derper`, then read on.
|
||||
If you've decided or been advised to run your own `derper`, then read on.
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -28,7 +29,10 @@ But if you've decided or been advised to run your own `derper`, then read on.
|
||||
|
||||
* You must build and update the `cmd/derper` binary yourself. There are no
|
||||
packages. Use `go install tailscale.com/cmd/derper@latest` with the latest
|
||||
version of Go.
|
||||
version of Go. You should update this binary approximately as regularly as
|
||||
you update Tailscale nodes. If using `--verify-clients`, the `derper` binary
|
||||
and `tailscaled` binary on the machine must be built from the same git revision.
|
||||
(It might work otherwise, but they're developed and only tested together.)
|
||||
|
||||
* The DERP protocol does a protocol switch inside TLS from HTTP to a custom
|
||||
bidirectional binary protocol. It is thus incompatible with many HTTP proxies.
|
||||
@@ -55,7 +59,7 @@ rely on its DNS which might be broken and dependent on DERP to get back up.
|
||||
* Monitor your DERP servers with [`cmd/derpprobe`](../derpprobe/).
|
||||
|
||||
* If using `--verify-clients`, a `tailscaled` must be running alongside the
|
||||
`derper`.
|
||||
`derper`, and all clients must be visible to the derper tailscaled in the ACL.
|
||||
|
||||
* If using `--verify-clients`, a `tailscaled` must also be running alongside
|
||||
your `derpprobe`, and `derpprobe` needs to use `--derp-map=local`.
|
||||
@@ -72,3 +76,34 @@ rely on its DNS which might be broken and dependent on DERP to get back up.
|
||||
* Don't rate-limit UDP STUN packets.
|
||||
|
||||
* Don't rate-limit outbound TCP traffic (only inbound).
|
||||
|
||||
## Diagnostics
|
||||
|
||||
This is not a complete guide on DERP diagnostics.
|
||||
|
||||
Running your own DERP services requires exeprtise in multi-layer network and
|
||||
application diagnostics. As the DERP runs multiple protocols at multiple layers
|
||||
and is not a regular HTTP(s) server you will need expertise in correlative
|
||||
analysis to diagnose the most tricky problems. There is no "plain text" or
|
||||
"open" mode of operation for DERP.
|
||||
|
||||
* The debug handler is accessible at URL path `/debug/`. It is only accessible
|
||||
over localhost or from a Tailscale IP address.
|
||||
|
||||
* Go pprof can be accessed via the debug handler at `/debug/pprof/`
|
||||
|
||||
* Prometheus compatible metrics can be gathered from the debug handler at
|
||||
`/debug/varz`.
|
||||
|
||||
* `cmd/stunc` in the Tailscale repository provides a basic tool for diagnosing
|
||||
issues with STUN.
|
||||
|
||||
* `cmd/derpprobe` provides a service for monitoring DERP cluster health.
|
||||
|
||||
* `tailscale debug derp` and `tailscale netcheck` provide additional client
|
||||
driven diagnostic information for DERP communications.
|
||||
|
||||
* Tailscale logs may provide insight for certain problems, such as if DERPs are
|
||||
unreachable or peers are regularly not reachable in their DERP home regions.
|
||||
There are many possible misconfiguration causes for these problems, but
|
||||
regular log entries are a good first indicator that there is a problem.
|
||||
|
||||
@@ -10,6 +10,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
@@ -99,7 +105,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/net/stunserver
|
||||
@@ -114,7 +120,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/derp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/derp
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The derper binary is a simple DERP server.
|
||||
//
|
||||
// For more information, see:
|
||||
//
|
||||
// - About: https://tailscale.com/kb/1232/derp-servers
|
||||
// - Protocol & Go docs: https://pkg.go.dev/tailscale.com/derp
|
||||
// - Running a DERP server: https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp
|
||||
package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
@@ -22,6 +28,9 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
runtimemetrics "runtime/metrics"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -206,11 +215,16 @@ func main() {
|
||||
io.WriteString(w, `<html><body>
|
||||
<h1>DERP</h1>
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://tailscale.com/">Tailscale</a>
|
||||
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
|
||||
server.
|
||||
This is a <a href="https://tailscale.com/">Tailscale</a> DERP server.
|
||||
</p>
|
||||
<p>
|
||||
Documentation:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="https://tailscale.com/kb/1232/derp-servers">About DERP</a></li>
|
||||
<li><a href="https://pkg.go.dev/tailscale.com/derp">Protocol & Go docs</a></li>
|
||||
<li><a href="https://github.com/tailscale/tailscale/tree/main/cmd/derper#derp">How to run a DERP server</a></li>
|
||||
</ul>
|
||||
`)
|
||||
if !*runDERP {
|
||||
io.WriteString(w, `<p>Status: <b>disabled</b></p>`)
|
||||
@@ -236,6 +250,20 @@ func main() {
|
||||
}
|
||||
}))
|
||||
debug.Handle("traffic", "Traffic check", http.HandlerFunc(s.ServeDebugTraffic))
|
||||
debug.Handle("set-mutex-profile-fraction", "SetMutexProfileFraction", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
s := r.FormValue("rate")
|
||||
if s == "" || r.Header.Get("Sec-Debug") != "derp" {
|
||||
http.Error(w, "To set, use: curl -HSec-Debug:derp 'http://derp/debug/set-mutex-profile-fraction?rate=100'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
http.Error(w, "bad rate value", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
old := runtime.SetMutexProfileFraction(v)
|
||||
fmt.Fprintf(w, "mutex changed from %v to %v\n", old, v)
|
||||
}))
|
||||
|
||||
// Longer lived DERP connections send an application layer keepalive. Note
|
||||
// if the keepalive is hit, the user timeout will take precedence over the
|
||||
@@ -452,3 +480,16 @@ func (l *rateLimitedListener) Accept() (net.Conn, error) {
|
||||
l.numAccepts.Add(1)
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
expvar.Publish("go_sync_mutex_wait_seconds", expvar.Func(func() any {
|
||||
const name = "/sync/mutex/wait/total:seconds" // Go 1.20+
|
||||
var s [1]runtimemetrics.Sample
|
||||
s[0].Name = name
|
||||
runtimemetrics.Read(s[:])
|
||||
if v := s[0].Value; v.Kind() == runtimemetrics.KindFloat64 {
|
||||
return v.Float64()
|
||||
}
|
||||
return 0
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -9,14 +9,12 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -71,8 +69,8 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.NodePublic) { s.RemovePacketForwarder(k, c) }
|
||||
add := func(m derp.PeerPresentMessage) { s.AddPacketForwarder(m.Key, c) }
|
||||
remove := func(m derp.PeerGoneMessage) { s.RemovePacketForwarder(m.Peer, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -70,8 +68,13 @@ func main() {
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux)
|
||||
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
|
||||
d := tsweb.Debugger(mux)
|
||||
d.Handle("probe-run", "Run a probe", tsweb.StdHandler(tsweb.ReturnHandlerFunc(p.RunHandler), tsweb.HandlerOptions{Logf: log.Printf}))
|
||||
mux.Handle("/", tsweb.StdHandler(p.StatusHandler(
|
||||
prober.WithTitle("DERP Prober"),
|
||||
prober.WithPageLink("Prober metrics", "/debug/varz"),
|
||||
prober.WithProbeLink("Run Probe", "/debug/probe-run?name={{.Name}}"),
|
||||
), tsweb.HandlerOptions{Logf: log.Printf}))
|
||||
log.Printf("Listening on %s", *listen)
|
||||
log.Fatal(http.ListenAndServe(*listen, mux))
|
||||
}
|
||||
@@ -105,26 +108,3 @@ func getOverallStatus(p *prober.Prober) (o overallStatus) {
|
||||
sort.Strings(o.good)
|
||||
return
|
||||
}
|
||||
|
||||
func serveFunc(p *prober.Prober) func(w http.ResponseWriter, r *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
st := getOverallStatus(p)
|
||||
summary := "All good"
|
||||
if (float64(len(st.bad)) / float64(len(st.bad)+len(st.good))) > 0.25 {
|
||||
// Returning a 500 allows monitoring this server externally and configuring
|
||||
// an alert on HTTP response code.
|
||||
w.WriteHeader(500)
|
||||
summary = fmt.Sprintf("%d problems", len(st.bad))
|
||||
}
|
||||
|
||||
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
|
||||
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
|
||||
for _, s := range st.bad {
|
||||
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
|
||||
}
|
||||
for _, s := range st.good {
|
||||
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
|
||||
}
|
||||
io.WriteString(w, "</ul></body></html>\n")
|
||||
}
|
||||
}
|
||||
|
||||
997
cmd/k8s-operator/depaware.txt
Normal file
997
cmd/k8s-operator/depaware.txt
Normal file
@@ -0,0 +1,997 @@
|
||||
tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/depaware)
|
||||
|
||||
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
|
||||
filippo.io/edwards25519/field from filippo.io/edwards25519
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/defaults+
|
||||
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
|
||||
L github.com/aws/aws-sdk-go-v2/aws/ratelimit from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||
L github.com/aws/aws-sdk-go-v2/aws/retry from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4 from github.com/aws/aws-sdk-go-v2/aws/signer/v4
|
||||
L github.com/aws/aws-sdk-go-v2/aws/signer/v4 from github.com/aws/aws-sdk-go-v2/internal/auth/smithy+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/transport/http from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/config from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/credentials from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/processcreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/ssocreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
|
||||
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
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||
L github.com/aws/aws-sdk-go-v2/internal/shareddefaults from github.com/aws/aws-sdk-go-v2/config+
|
||||
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
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssm/types from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssooidc
|
||||
L github.com/aws/aws-sdk-go-v2/service/ssooidc/types from github.com/aws/aws-sdk-go-v2/service/ssooidc
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
|
||||
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+
|
||||
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
|
||||
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/smithy-go/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/private/requestcompression from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
github.com/bits-and-blooms/bitset from github.com/gaissmai/bart
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
|
||||
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/util/osdiag+
|
||||
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
|
||||
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
github.com/distribution/reference from tailscale.com/cmd/k8s-operator
|
||||
github.com/emicklei/go-restful/v3 from k8s.io/kube-openapi/pkg/common
|
||||
github.com/emicklei/go-restful/v3/log from github.com/emicklei/go-restful/v3
|
||||
github.com/evanphx/json-patch/v5 from sigs.k8s.io/controller-runtime/pkg/client
|
||||
github.com/evanphx/json-patch/v5/internal/json from github.com/evanphx/json-patch/v5
|
||||
💣 github.com/fsnotify/fsnotify from sigs.k8s.io/controller-runtime/pkg/certwatcher
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/ipset+
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
|
||||
github.com/go-logr/logr from github.com/go-logr/logr/slogr+
|
||||
github.com/go-logr/logr/slogr from github.com/go-logr/zapr
|
||||
github.com/go-logr/zapr from sigs.k8s.io/controller-runtime/pkg/log/zap+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
github.com/go-openapi/jsonpointer from github.com/go-openapi/jsonreference
|
||||
github.com/go-openapi/jsonreference from k8s.io/kube-openapi/pkg/internal+
|
||||
github.com/go-openapi/jsonreference/internal from github.com/go-openapi/jsonreference
|
||||
github.com/go-openapi/swag from github.com/go-openapi/jsonpointer+
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
💣 github.com/gogo/protobuf/proto from k8s.io/api/admission/v1+
|
||||
github.com/gogo/protobuf/sortkeys from k8s.io/api/admission/v1+
|
||||
github.com/golang/groupcache/lru from k8s.io/client-go/tools/record+
|
||||
github.com/golang/protobuf/proto from k8s.io/client-go/discovery+
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
github.com/google/gnostic-models/compiler from github.com/google/gnostic-models/openapiv2+
|
||||
github.com/google/gnostic-models/extensions from github.com/google/gnostic-models/compiler
|
||||
github.com/google/gnostic-models/jsonschema from github.com/google/gnostic-models/compiler
|
||||
github.com/google/gnostic-models/openapiv2 from k8s.io/client-go/discovery+
|
||||
github.com/google/gnostic-models/openapiv3 from k8s.io/kube-openapi/pkg/handler3+
|
||||
💣 github.com/google/go-cmp/cmp from k8s.io/apimachinery/pkg/util/diff+
|
||||
github.com/google/go-cmp/cmp/internal/diff from github.com/google/go-cmp/cmp
|
||||
github.com/google/go-cmp/cmp/internal/flags from github.com/google/go-cmp/cmp+
|
||||
github.com/google/go-cmp/cmp/internal/function from github.com/google/go-cmp/cmp
|
||||
💣 github.com/google/go-cmp/cmp/internal/value from github.com/google/go-cmp/cmp
|
||||
github.com/google/gofuzz from k8s.io/apimachinery/pkg/apis/meta/v1+
|
||||
github.com/google/gofuzz/bytesource from github.com/google/gofuzz
|
||||
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+
|
||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||
github.com/google/uuid from github.com/prometheus-community/pro-bing+
|
||||
github.com/gorilla/csrf from tailscale.com/client/web
|
||||
github.com/gorilla/securecookie from github.com/gorilla/csrf
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/clientupdate/distsign+
|
||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||
github.com/imdario/mergo from k8s.io/client-go/tools/clientcmd
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
github.com/josharian/intern from github.com/mailru/easyjson/jlexer
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/netmon
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
💣 github.com/json-iterator/go from sigs.k8s.io/structured-merge-diff/v4/fieldpath+
|
||||
github.com/klauspost/compress from github.com/klauspost/compress/zstd
|
||||
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/huff0+
|
||||
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
|
||||
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
|
||||
github.com/mailru/easyjson/buffer from github.com/mailru/easyjson/jwriter
|
||||
💣 github.com/mailru/easyjson/jlexer from github.com/go-openapi/swag
|
||||
github.com/mailru/easyjson/jwriter from github.com/go-openapi/swag
|
||||
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
|
||||
L 💣 github.com/mdlayher/netlink from github.com/google/nftables+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
github.com/miekg/dns from tailscale.com/net/dns/recursive
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
github.com/modern-go/concurrent from github.com/json-iterator/go
|
||||
💣 github.com/modern-go/reflect2 from github.com/json-iterator/go
|
||||
github.com/munnerz/goautoneg from k8s.io/kube-openapi/pkg/handler3
|
||||
github.com/opencontainers/go-digest from github.com/distribution/reference
|
||||
L github.com/pierrec/lz4/v4 from github.com/u-root/uio/uio
|
||||
L github.com/pierrec/lz4/v4/internal/lz4block from github.com/pierrec/lz4/v4+
|
||||
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/evanphx/json-patch/v5+
|
||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||
💣 github.com/prometheus/client_golang/prometheus from github.com/prometheus/client_golang/prometheus/collectors+
|
||||
github.com/prometheus/client_golang/prometheus/collectors from sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus+
|
||||
github.com/prometheus/client_golang/prometheus/promhttp from sigs.k8s.io/controller-runtime/pkg/metrics/server+
|
||||
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
|
||||
L 💣 github.com/safchain/ethtool from tailscale.com/doctor/ethtool+
|
||||
github.com/spf13/pflag from k8s.io/client-go/tools/clientcmd
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||
W github.com/tailscale/go-winio/internal/stringbuffer from github.com/tailscale/go-winio/internal/fs
|
||||
W github.com/tailscale/go-winio/pkg/guid from github.com/tailscale/go-winio+
|
||||
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/tailscale/golang-x-crypto/internal/poly1305 from github.com/tailscale/golang-x-crypto/ssh
|
||||
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tailscale/hujson from tailscale.com/ipn/conffile
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/net/routetable+
|
||||
github.com/tailscale/peercred from tailscale.com/ipn/ipnauth
|
||||
github.com/tailscale/web-client-prebuilt from tailscale.com/client/web
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/conn/winrio from github.com/tailscale/wireguard-go/conn
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/net/tstun+
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/namedpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
|
||||
L github.com/vishvananda/netns from github.com/tailscale/netlink+
|
||||
github.com/x448/float16 from github.com/fxamacker/cbor/v2
|
||||
go.uber.org/multierr from go.uber.org/zap+
|
||||
go.uber.org/zap from github.com/go-logr/zapr+
|
||||
go.uber.org/zap/buffer from go.uber.org/zap/internal/bufferpool+
|
||||
go.uber.org/zap/internal from go.uber.org/zap
|
||||
go.uber.org/zap/internal/bufferpool from go.uber.org/zap+
|
||||
go.uber.org/zap/internal/color from go.uber.org/zap/zapcore
|
||||
go.uber.org/zap/internal/exit from go.uber.org/zap/zapcore
|
||||
go.uber.org/zap/internal/pool from go.uber.org/zap+
|
||||
go.uber.org/zap/internal/stacktrace from go.uber.org/zap
|
||||
go.uber.org/zap/zapcore from github.com/go-logr/zapr+
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
go4.org/netipx from tailscale.com/ipn/ipnlocal+
|
||||
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
|
||||
gomodules.xyz/jsonpatch/v2 from sigs.k8s.io/controller-runtime/pkg/webhook+
|
||||
google.golang.org/protobuf/encoding/protodelim from github.com/prometheus/common/expfmt
|
||||
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/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/editiondefaults from google.golang.org/protobuf/internal/filedesc+
|
||||
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/protodelim+
|
||||
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/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 github.com/google/gnostic-models/openapiv3+
|
||||
google.golang.org/protobuf/types/gofeaturespb from google.golang.org/protobuf/reflect/protodesc
|
||||
google.golang.org/protobuf/types/known/anypb from github.com/google/gnostic-models/compiler+
|
||||
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
|
||||
gopkg.in/inf.v0 from k8s.io/apimachinery/pkg/api/resource
|
||||
gopkg.in/yaml.v2 from k8s.io/kube-openapi/pkg/util/proto+
|
||||
gopkg.in/yaml.v3 from github.com/go-openapi/swag+
|
||||
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/buffer+
|
||||
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/buffer
|
||||
💣 gvisor.dev/gvisor/pkg/buffer from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/buffer+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/atomicbitops+
|
||||
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
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/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/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+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/net/tstun+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack/gro from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/noop from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context+
|
||||
k8s.io/api/admission/v1 from sigs.k8s.io/controller-runtime/pkg/webhook/admission
|
||||
k8s.io/api/admission/v1beta1 from sigs.k8s.io/controller-runtime/pkg/webhook/admission
|
||||
k8s.io/api/admissionregistration/v1 from k8s.io/api/admissionregistration/v1alpha1+
|
||||
k8s.io/api/admissionregistration/v1alpha1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1+
|
||||
k8s.io/api/admissionregistration/v1beta1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1+
|
||||
k8s.io/api/apidiscovery/v2 from k8s.io/client-go/discovery
|
||||
k8s.io/api/apidiscovery/v2beta1 from k8s.io/client-go/discovery
|
||||
k8s.io/api/apiserverinternal/v1alpha1 from k8s.io/client-go/applyconfigurations/apiserverinternal/v1alpha1+
|
||||
k8s.io/api/apps/v1 from k8s.io/client-go/applyconfigurations/apps/v1+
|
||||
k8s.io/api/apps/v1beta1 from k8s.io/api/extensions/v1beta1+
|
||||
k8s.io/api/apps/v1beta2 from k8s.io/client-go/applyconfigurations/apps/v1beta2+
|
||||
k8s.io/api/authentication/v1 from k8s.io/api/admission/v1+
|
||||
k8s.io/api/authentication/v1alpha1 from k8s.io/client-go/kubernetes/scheme+
|
||||
k8s.io/api/authentication/v1beta1 from k8s.io/client-go/kubernetes/scheme+
|
||||
k8s.io/api/authorization/v1 from k8s.io/client-go/kubernetes/scheme+
|
||||
k8s.io/api/authorization/v1beta1 from k8s.io/client-go/kubernetes/scheme+
|
||||
k8s.io/api/autoscaling/v1 from k8s.io/client-go/applyconfigurations/autoscaling/v1+
|
||||
k8s.io/api/autoscaling/v2 from k8s.io/client-go/applyconfigurations/autoscaling/v2+
|
||||
k8s.io/api/autoscaling/v2beta1 from k8s.io/client-go/applyconfigurations/autoscaling/v2beta1+
|
||||
k8s.io/api/autoscaling/v2beta2 from k8s.io/client-go/applyconfigurations/autoscaling/v2beta2+
|
||||
k8s.io/api/batch/v1 from k8s.io/api/batch/v1beta1+
|
||||
k8s.io/api/batch/v1beta1 from k8s.io/client-go/applyconfigurations/batch/v1beta1+
|
||||
k8s.io/api/certificates/v1 from k8s.io/client-go/applyconfigurations/certificates/v1+
|
||||
k8s.io/api/certificates/v1alpha1 from k8s.io/client-go/applyconfigurations/certificates/v1alpha1+
|
||||
k8s.io/api/certificates/v1beta1 from k8s.io/client-go/applyconfigurations/certificates/v1beta1+
|
||||
k8s.io/api/coordination/v1 from k8s.io/client-go/applyconfigurations/coordination/v1+
|
||||
k8s.io/api/coordination/v1beta1 from k8s.io/client-go/applyconfigurations/coordination/v1beta1+
|
||||
k8s.io/api/core/v1 from k8s.io/api/apps/v1+
|
||||
k8s.io/api/discovery/v1 from k8s.io/client-go/applyconfigurations/discovery/v1+
|
||||
k8s.io/api/discovery/v1beta1 from k8s.io/client-go/applyconfigurations/discovery/v1beta1+
|
||||
k8s.io/api/events/v1 from k8s.io/client-go/applyconfigurations/events/v1+
|
||||
k8s.io/api/events/v1beta1 from k8s.io/client-go/applyconfigurations/events/v1beta1+
|
||||
k8s.io/api/extensions/v1beta1 from k8s.io/client-go/applyconfigurations/extensions/v1beta1+
|
||||
k8s.io/api/flowcontrol/v1 from k8s.io/client-go/applyconfigurations/flowcontrol/v1+
|
||||
k8s.io/api/flowcontrol/v1beta1 from k8s.io/client-go/applyconfigurations/flowcontrol/v1beta1+
|
||||
k8s.io/api/flowcontrol/v1beta2 from k8s.io/client-go/applyconfigurations/flowcontrol/v1beta2+
|
||||
k8s.io/api/flowcontrol/v1beta3 from k8s.io/client-go/applyconfigurations/flowcontrol/v1beta3+
|
||||
k8s.io/api/networking/v1 from k8s.io/client-go/applyconfigurations/networking/v1+
|
||||
k8s.io/api/networking/v1alpha1 from k8s.io/client-go/applyconfigurations/networking/v1alpha1+
|
||||
k8s.io/api/networking/v1beta1 from k8s.io/client-go/applyconfigurations/networking/v1beta1+
|
||||
k8s.io/api/node/v1 from k8s.io/client-go/applyconfigurations/node/v1+
|
||||
k8s.io/api/node/v1alpha1 from k8s.io/client-go/applyconfigurations/node/v1alpha1+
|
||||
k8s.io/api/node/v1beta1 from k8s.io/client-go/applyconfigurations/node/v1beta1+
|
||||
k8s.io/api/policy/v1 from k8s.io/client-go/applyconfigurations/policy/v1+
|
||||
k8s.io/api/policy/v1beta1 from k8s.io/client-go/applyconfigurations/policy/v1beta1+
|
||||
k8s.io/api/rbac/v1 from k8s.io/client-go/applyconfigurations/rbac/v1+
|
||||
k8s.io/api/rbac/v1alpha1 from k8s.io/client-go/applyconfigurations/rbac/v1alpha1+
|
||||
k8s.io/api/rbac/v1beta1 from k8s.io/client-go/applyconfigurations/rbac/v1beta1+
|
||||
k8s.io/api/resource/v1alpha2 from k8s.io/client-go/applyconfigurations/resource/v1alpha2+
|
||||
k8s.io/api/scheduling/v1 from k8s.io/client-go/applyconfigurations/scheduling/v1+
|
||||
k8s.io/api/scheduling/v1alpha1 from k8s.io/client-go/applyconfigurations/scheduling/v1alpha1+
|
||||
k8s.io/api/scheduling/v1beta1 from k8s.io/client-go/applyconfigurations/scheduling/v1beta1+
|
||||
k8s.io/api/storage/v1 from k8s.io/client-go/applyconfigurations/storage/v1+
|
||||
k8s.io/api/storage/v1alpha1 from k8s.io/client-go/applyconfigurations/storage/v1alpha1+
|
||||
k8s.io/api/storage/v1beta1 from k8s.io/client-go/applyconfigurations/storage/v1beta1+
|
||||
k8s.io/api/storagemigration/v1alpha1 from k8s.io/client-go/applyconfigurations/storagemigration/v1alpha1+
|
||||
k8s.io/apiextensions-apiserver/pkg/apis/apiextensions from k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1
|
||||
💣 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1 from sigs.k8s.io/controller-runtime/pkg/webhook/conversion
|
||||
k8s.io/apimachinery/pkg/api/equality from k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1+
|
||||
k8s.io/apimachinery/pkg/api/errors from k8s.io/apimachinery/pkg/util/managedfields/internal+
|
||||
k8s.io/apimachinery/pkg/api/meta from k8s.io/apimachinery/pkg/api/validation+
|
||||
k8s.io/apimachinery/pkg/api/resource from k8s.io/api/autoscaling/v1+
|
||||
k8s.io/apimachinery/pkg/api/validation from k8s.io/apimachinery/pkg/util/managedfields/internal+
|
||||
💣 k8s.io/apimachinery/pkg/apis/meta/internalversion from k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme+
|
||||
k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme from k8s.io/client-go/metadata
|
||||
💣 k8s.io/apimachinery/pkg/apis/meta/v1 from k8s.io/api/admission/v1+
|
||||
k8s.io/apimachinery/pkg/apis/meta/v1/unstructured from k8s.io/apimachinery/pkg/runtime/serializer/versioning+
|
||||
k8s.io/apimachinery/pkg/apis/meta/v1/validation from k8s.io/apimachinery/pkg/api/validation+
|
||||
💣 k8s.io/apimachinery/pkg/apis/meta/v1beta1 from k8s.io/apimachinery/pkg/apis/meta/internalversion
|
||||
k8s.io/apimachinery/pkg/conversion from k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1+
|
||||
k8s.io/apimachinery/pkg/conversion/queryparams from k8s.io/apimachinery/pkg/runtime+
|
||||
k8s.io/apimachinery/pkg/fields from k8s.io/apimachinery/pkg/api/equality+
|
||||
k8s.io/apimachinery/pkg/labels from k8s.io/apimachinery/pkg/api/equality+
|
||||
k8s.io/apimachinery/pkg/runtime from k8s.io/api/admission/v1+
|
||||
k8s.io/apimachinery/pkg/runtime/schema from k8s.io/api/admission/v1+
|
||||
k8s.io/apimachinery/pkg/runtime/serializer from k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme+
|
||||
k8s.io/apimachinery/pkg/runtime/serializer/json from k8s.io/apimachinery/pkg/runtime/serializer+
|
||||
k8s.io/apimachinery/pkg/runtime/serializer/protobuf from k8s.io/apimachinery/pkg/runtime/serializer
|
||||
k8s.io/apimachinery/pkg/runtime/serializer/recognizer from k8s.io/apimachinery/pkg/runtime/serializer+
|
||||
k8s.io/apimachinery/pkg/runtime/serializer/streaming from k8s.io/client-go/rest+
|
||||
k8s.io/apimachinery/pkg/runtime/serializer/versioning from k8s.io/apimachinery/pkg/runtime/serializer+
|
||||
k8s.io/apimachinery/pkg/selection from k8s.io/apimachinery/pkg/apis/meta/v1+
|
||||
k8s.io/apimachinery/pkg/types from k8s.io/api/admission/v1+
|
||||
k8s.io/apimachinery/pkg/util/cache from k8s.io/client-go/tools/cache
|
||||
k8s.io/apimachinery/pkg/util/diff from k8s.io/client-go/tools/cache
|
||||
k8s.io/apimachinery/pkg/util/dump from k8s.io/apimachinery/pkg/util/diff+
|
||||
k8s.io/apimachinery/pkg/util/errors from k8s.io/apimachinery/pkg/api/meta+
|
||||
k8s.io/apimachinery/pkg/util/framer from k8s.io/apimachinery/pkg/runtime/serializer/json+
|
||||
k8s.io/apimachinery/pkg/util/intstr from k8s.io/api/apps/v1+
|
||||
k8s.io/apimachinery/pkg/util/json from k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1+
|
||||
k8s.io/apimachinery/pkg/util/managedfields from k8s.io/client-go/applyconfigurations/admissionregistration/v1+
|
||||
k8s.io/apimachinery/pkg/util/managedfields/internal from k8s.io/apimachinery/pkg/util/managedfields
|
||||
k8s.io/apimachinery/pkg/util/mergepatch from k8s.io/apimachinery/pkg/util/strategicpatch
|
||||
k8s.io/apimachinery/pkg/util/naming from k8s.io/apimachinery/pkg/runtime+
|
||||
k8s.io/apimachinery/pkg/util/net from k8s.io/apimachinery/pkg/watch+
|
||||
k8s.io/apimachinery/pkg/util/rand from k8s.io/apiserver/pkg/storage/names
|
||||
k8s.io/apimachinery/pkg/util/runtime from k8s.io/apimachinery/pkg/apis/meta/internalversion/scheme+
|
||||
k8s.io/apimachinery/pkg/util/sets from k8s.io/apimachinery/pkg/api/meta+
|
||||
k8s.io/apimachinery/pkg/util/strategicpatch from k8s.io/client-go/tools/record+
|
||||
k8s.io/apimachinery/pkg/util/uuid from sigs.k8s.io/controller-runtime/pkg/internal/controller+
|
||||
k8s.io/apimachinery/pkg/util/validation from k8s.io/apimachinery/pkg/api/validation+
|
||||
k8s.io/apimachinery/pkg/util/validation/field from k8s.io/apimachinery/pkg/api/errors+
|
||||
k8s.io/apimachinery/pkg/util/wait from k8s.io/client-go/tools/cache+
|
||||
k8s.io/apimachinery/pkg/util/yaml from k8s.io/apimachinery/pkg/runtime/serializer/json
|
||||
k8s.io/apimachinery/pkg/version from k8s.io/client-go/discovery+
|
||||
k8s.io/apimachinery/pkg/watch from k8s.io/apimachinery/pkg/apis/meta/v1+
|
||||
k8s.io/apimachinery/third_party/forked/golang/json from k8s.io/apimachinery/pkg/util/strategicpatch
|
||||
k8s.io/apimachinery/third_party/forked/golang/reflect from k8s.io/apimachinery/pkg/conversion
|
||||
k8s.io/apiserver/pkg/storage/names from tailscale.com/cmd/k8s-operator
|
||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1+
|
||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1alpha1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/admissionregistration/v1beta1 from k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/apiserverinternal/v1alpha1 from k8s.io/client-go/kubernetes/typed/apiserverinternal/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/apps/v1 from k8s.io/client-go/kubernetes/typed/apps/v1
|
||||
k8s.io/client-go/applyconfigurations/apps/v1beta1 from k8s.io/client-go/kubernetes/typed/apps/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/apps/v1beta2 from k8s.io/client-go/kubernetes/typed/apps/v1beta2
|
||||
k8s.io/client-go/applyconfigurations/autoscaling/v1 from k8s.io/client-go/kubernetes/typed/apps/v1+
|
||||
k8s.io/client-go/applyconfigurations/autoscaling/v2 from k8s.io/client-go/kubernetes/typed/autoscaling/v2
|
||||
k8s.io/client-go/applyconfigurations/autoscaling/v2beta1 from k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1
|
||||
k8s.io/client-go/applyconfigurations/autoscaling/v2beta2 from k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2
|
||||
k8s.io/client-go/applyconfigurations/batch/v1 from k8s.io/client-go/applyconfigurations/batch/v1beta1+
|
||||
k8s.io/client-go/applyconfigurations/batch/v1beta1 from k8s.io/client-go/kubernetes/typed/batch/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1 from k8s.io/client-go/kubernetes/typed/certificates/v1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1alpha1 from k8s.io/client-go/kubernetes/typed/certificates/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/certificates/v1beta1 from k8s.io/client-go/kubernetes/typed/certificates/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/coordination/v1 from k8s.io/client-go/kubernetes/typed/coordination/v1
|
||||
k8s.io/client-go/applyconfigurations/coordination/v1beta1 from k8s.io/client-go/kubernetes/typed/coordination/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/core/v1 from k8s.io/client-go/applyconfigurations/apps/v1+
|
||||
k8s.io/client-go/applyconfigurations/discovery/v1 from k8s.io/client-go/kubernetes/typed/discovery/v1
|
||||
k8s.io/client-go/applyconfigurations/discovery/v1beta1 from k8s.io/client-go/kubernetes/typed/discovery/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/events/v1 from k8s.io/client-go/kubernetes/typed/events/v1
|
||||
k8s.io/client-go/applyconfigurations/events/v1beta1 from k8s.io/client-go/kubernetes/typed/events/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/extensions/v1beta1 from k8s.io/client-go/kubernetes/typed/extensions/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/flowcontrol/v1 from k8s.io/client-go/kubernetes/typed/flowcontrol/v1
|
||||
k8s.io/client-go/applyconfigurations/flowcontrol/v1beta1 from k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/flowcontrol/v1beta2 from k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta2
|
||||
k8s.io/client-go/applyconfigurations/flowcontrol/v1beta3 from k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta3
|
||||
k8s.io/client-go/applyconfigurations/internal from k8s.io/client-go/applyconfigurations/admissionregistration/v1+
|
||||
k8s.io/client-go/applyconfigurations/meta/v1 from k8s.io/client-go/applyconfigurations/admissionregistration/v1+
|
||||
k8s.io/client-go/applyconfigurations/networking/v1 from k8s.io/client-go/kubernetes/typed/networking/v1
|
||||
k8s.io/client-go/applyconfigurations/networking/v1alpha1 from k8s.io/client-go/kubernetes/typed/networking/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/networking/v1beta1 from k8s.io/client-go/kubernetes/typed/networking/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/node/v1 from k8s.io/client-go/kubernetes/typed/node/v1
|
||||
k8s.io/client-go/applyconfigurations/node/v1alpha1 from k8s.io/client-go/kubernetes/typed/node/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/node/v1beta1 from k8s.io/client-go/kubernetes/typed/node/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/policy/v1 from k8s.io/client-go/kubernetes/typed/policy/v1
|
||||
k8s.io/client-go/applyconfigurations/policy/v1beta1 from k8s.io/client-go/kubernetes/typed/policy/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/rbac/v1 from k8s.io/client-go/kubernetes/typed/rbac/v1
|
||||
k8s.io/client-go/applyconfigurations/rbac/v1alpha1 from k8s.io/client-go/kubernetes/typed/rbac/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/rbac/v1beta1 from k8s.io/client-go/kubernetes/typed/rbac/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/resource/v1alpha2 from k8s.io/client-go/kubernetes/typed/resource/v1alpha2
|
||||
k8s.io/client-go/applyconfigurations/scheduling/v1 from k8s.io/client-go/kubernetes/typed/scheduling/v1
|
||||
k8s.io/client-go/applyconfigurations/scheduling/v1alpha1 from k8s.io/client-go/kubernetes/typed/scheduling/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/scheduling/v1beta1 from k8s.io/client-go/kubernetes/typed/scheduling/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/storage/v1 from k8s.io/client-go/kubernetes/typed/storage/v1
|
||||
k8s.io/client-go/applyconfigurations/storage/v1alpha1 from k8s.io/client-go/kubernetes/typed/storage/v1alpha1
|
||||
k8s.io/client-go/applyconfigurations/storage/v1beta1 from k8s.io/client-go/kubernetes/typed/storage/v1beta1
|
||||
k8s.io/client-go/applyconfigurations/storagemigration/v1alpha1 from k8s.io/client-go/kubernetes/typed/storagemigration/v1alpha1
|
||||
k8s.io/client-go/discovery from k8s.io/client-go/applyconfigurations/meta/v1+
|
||||
k8s.io/client-go/dynamic from sigs.k8s.io/controller-runtime/pkg/cache/internal+
|
||||
k8s.io/client-go/features from k8s.io/client-go/tools/cache
|
||||
k8s.io/client-go/kubernetes from k8s.io/client-go/tools/leaderelection/resourcelock
|
||||
k8s.io/client-go/kubernetes/scheme from k8s.io/client-go/discovery+
|
||||
k8s.io/client-go/kubernetes/typed/admissionregistration/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/admissionregistration/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/admissionregistration/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/apiserverinternal/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/apps/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/apps/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/apps/v1beta2 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/authentication/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/authentication/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/authentication/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/authorization/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/authorization/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v2 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v2beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/autoscaling/v2beta2 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/batch/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/batch/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/certificates/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/coordination/v1 from k8s.io/client-go/kubernetes+
|
||||
k8s.io/client-go/kubernetes/typed/coordination/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/core/v1 from k8s.io/client-go/kubernetes+
|
||||
k8s.io/client-go/kubernetes/typed/discovery/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/discovery/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/events/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/events/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/extensions/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/flowcontrol/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta2 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/flowcontrol/v1beta3 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/networking/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/networking/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/networking/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/node/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/node/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/node/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/policy/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/policy/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/rbac/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/rbac/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/rbac/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/resource/v1alpha2 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/scheduling/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/scheduling/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/scheduling/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/storage/v1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/storage/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/storage/v1beta1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/kubernetes/typed/storagemigration/v1alpha1 from k8s.io/client-go/kubernetes
|
||||
k8s.io/client-go/metadata from sigs.k8s.io/controller-runtime/pkg/cache/internal+
|
||||
k8s.io/client-go/openapi from k8s.io/client-go/discovery
|
||||
k8s.io/client-go/pkg/apis/clientauthentication from k8s.io/client-go/pkg/apis/clientauthentication/install+
|
||||
k8s.io/client-go/pkg/apis/clientauthentication/install from k8s.io/client-go/plugin/pkg/client/auth/exec
|
||||
💣 k8s.io/client-go/pkg/apis/clientauthentication/v1 from k8s.io/client-go/pkg/apis/clientauthentication/install+
|
||||
💣 k8s.io/client-go/pkg/apis/clientauthentication/v1beta1 from k8s.io/client-go/pkg/apis/clientauthentication/install+
|
||||
k8s.io/client-go/pkg/version from k8s.io/client-go/rest
|
||||
k8s.io/client-go/plugin/pkg/client/auth/exec from k8s.io/client-go/rest
|
||||
k8s.io/client-go/rest from k8s.io/client-go/discovery+
|
||||
k8s.io/client-go/rest/watch from k8s.io/client-go/rest
|
||||
k8s.io/client-go/restmapper from sigs.k8s.io/controller-runtime/pkg/client/apiutil
|
||||
k8s.io/client-go/tools/auth from k8s.io/client-go/tools/clientcmd
|
||||
k8s.io/client-go/tools/cache from sigs.k8s.io/controller-runtime/pkg/cache+
|
||||
k8s.io/client-go/tools/cache/synctrack from k8s.io/client-go/tools/cache
|
||||
k8s.io/client-go/tools/clientcmd from sigs.k8s.io/controller-runtime/pkg/client/config
|
||||
k8s.io/client-go/tools/clientcmd/api from k8s.io/client-go/plugin/pkg/client/auth/exec+
|
||||
k8s.io/client-go/tools/clientcmd/api/latest from k8s.io/client-go/tools/clientcmd
|
||||
💣 k8s.io/client-go/tools/clientcmd/api/v1 from k8s.io/client-go/tools/clientcmd/api/latest
|
||||
k8s.io/client-go/tools/internal/events from k8s.io/client-go/tools/record
|
||||
k8s.io/client-go/tools/leaderelection from sigs.k8s.io/controller-runtime/pkg/manager+
|
||||
k8s.io/client-go/tools/leaderelection/resourcelock from k8s.io/client-go/tools/leaderelection+
|
||||
k8s.io/client-go/tools/metrics from k8s.io/client-go/plugin/pkg/client/auth/exec+
|
||||
k8s.io/client-go/tools/pager from k8s.io/client-go/tools/cache
|
||||
k8s.io/client-go/tools/record from sigs.k8s.io/controller-runtime/pkg/cluster+
|
||||
k8s.io/client-go/tools/record/util from k8s.io/client-go/tools/record
|
||||
k8s.io/client-go/tools/reference from k8s.io/client-go/kubernetes/typed/core/v1+
|
||||
k8s.io/client-go/transport from k8s.io/client-go/plugin/pkg/client/auth/exec+
|
||||
k8s.io/client-go/util/cert from k8s.io/client-go/rest+
|
||||
k8s.io/client-go/util/connrotation from k8s.io/client-go/plugin/pkg/client/auth/exec+
|
||||
k8s.io/client-go/util/flowcontrol from k8s.io/client-go/kubernetes+
|
||||
k8s.io/client-go/util/homedir from k8s.io/client-go/tools/clientcmd
|
||||
k8s.io/client-go/util/keyutil from k8s.io/client-go/util/cert
|
||||
k8s.io/client-go/util/workqueue from k8s.io/client-go/transport+
|
||||
k8s.io/klog/v2 from k8s.io/apimachinery/pkg/api/meta+
|
||||
k8s.io/klog/v2/internal/buffer from k8s.io/klog/v2
|
||||
k8s.io/klog/v2/internal/clock from k8s.io/klog/v2
|
||||
k8s.io/klog/v2/internal/dbg from k8s.io/klog/v2
|
||||
k8s.io/klog/v2/internal/serialize from k8s.io/klog/v2
|
||||
k8s.io/klog/v2/internal/severity from k8s.io/klog/v2+
|
||||
k8s.io/klog/v2/internal/sloghandler from k8s.io/klog/v2
|
||||
k8s.io/kube-openapi/pkg/cached from k8s.io/kube-openapi/pkg/handler3
|
||||
k8s.io/kube-openapi/pkg/common from k8s.io/kube-openapi/pkg/handler3
|
||||
k8s.io/kube-openapi/pkg/handler3 from k8s.io/client-go/openapi
|
||||
k8s.io/kube-openapi/pkg/internal from k8s.io/kube-openapi/pkg/spec3+
|
||||
k8s.io/kube-openapi/pkg/internal/third_party/go-json-experiment/json from k8s.io/kube-openapi/pkg/internal+
|
||||
k8s.io/kube-openapi/pkg/schemaconv from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
k8s.io/kube-openapi/pkg/spec3 from k8s.io/client-go/openapi+
|
||||
k8s.io/kube-openapi/pkg/util/proto from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
k8s.io/kube-openapi/pkg/validation/spec from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
k8s.io/utils/buffer from k8s.io/client-go/tools/cache
|
||||
k8s.io/utils/clock from k8s.io/apimachinery/pkg/util/cache+
|
||||
k8s.io/utils/clock/testing from k8s.io/client-go/util/flowcontrol
|
||||
k8s.io/utils/internal/third_party/forked/golang/net from k8s.io/utils/net
|
||||
k8s.io/utils/net from k8s.io/apimachinery/pkg/util/net+
|
||||
k8s.io/utils/pointer from k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1+
|
||||
k8s.io/utils/ptr from k8s.io/client-go/tools/cache+
|
||||
k8s.io/utils/strings/slices from k8s.io/apimachinery/pkg/labels
|
||||
k8s.io/utils/trace from k8s.io/client-go/tools/cache
|
||||
nhooyr.io/websocket from tailscale.com/control/controlhttp+
|
||||
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/controller-runtime/pkg/builder from tailscale.com/cmd/k8s-operator
|
||||
sigs.k8s.io/controller-runtime/pkg/cache from sigs.k8s.io/controller-runtime/pkg/cluster+
|
||||
sigs.k8s.io/controller-runtime/pkg/cache/internal from sigs.k8s.io/controller-runtime/pkg/cache
|
||||
sigs.k8s.io/controller-runtime/pkg/certwatcher from sigs.k8s.io/controller-runtime/pkg/metrics/server+
|
||||
sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics from sigs.k8s.io/controller-runtime/pkg/certwatcher
|
||||
sigs.k8s.io/controller-runtime/pkg/client from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/client/apiutil from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/client/config from tailscale.com/cmd/k8s-operator
|
||||
sigs.k8s.io/controller-runtime/pkg/cluster from sigs.k8s.io/controller-runtime/pkg/manager
|
||||
sigs.k8s.io/controller-runtime/pkg/config from sigs.k8s.io/controller-runtime/pkg/manager
|
||||
sigs.k8s.io/controller-runtime/pkg/controller from sigs.k8s.io/controller-runtime/pkg/builder
|
||||
sigs.k8s.io/controller-runtime/pkg/conversion from sigs.k8s.io/controller-runtime/pkg/webhook/conversion
|
||||
sigs.k8s.io/controller-runtime/pkg/event from sigs.k8s.io/controller-runtime/pkg/handler+
|
||||
sigs.k8s.io/controller-runtime/pkg/handler from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/healthz from sigs.k8s.io/controller-runtime/pkg/manager+
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/controller from sigs.k8s.io/controller-runtime/pkg/controller
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/controller/metrics from sigs.k8s.io/controller-runtime/pkg/internal/controller
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/field/selector from sigs.k8s.io/controller-runtime/pkg/cache/internal
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/httpserver from sigs.k8s.io/controller-runtime/pkg/manager+
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/log from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/recorder from sigs.k8s.io/controller-runtime/pkg/cluster+
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/source from sigs.k8s.io/controller-runtime/pkg/source
|
||||
sigs.k8s.io/controller-runtime/pkg/internal/syncs from sigs.k8s.io/controller-runtime/pkg/cache/internal
|
||||
sigs.k8s.io/controller-runtime/pkg/leaderelection from sigs.k8s.io/controller-runtime/pkg/manager
|
||||
sigs.k8s.io/controller-runtime/pkg/log from sigs.k8s.io/controller-runtime/pkg/client+
|
||||
sigs.k8s.io/controller-runtime/pkg/log/zap from tailscale.com/cmd/k8s-operator
|
||||
sigs.k8s.io/controller-runtime/pkg/manager from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/manager/signals from tailscale.com/cmd/k8s-operator
|
||||
sigs.k8s.io/controller-runtime/pkg/metrics from sigs.k8s.io/controller-runtime/pkg/certwatcher/metrics+
|
||||
sigs.k8s.io/controller-runtime/pkg/metrics/server from sigs.k8s.io/controller-runtime/pkg/manager
|
||||
sigs.k8s.io/controller-runtime/pkg/predicate from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/ratelimiter from sigs.k8s.io/controller-runtime/pkg/controller+
|
||||
sigs.k8s.io/controller-runtime/pkg/reconcile from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/recorder from sigs.k8s.io/controller-runtime/pkg/leaderelection+
|
||||
sigs.k8s.io/controller-runtime/pkg/source from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/webhook from sigs.k8s.io/controller-runtime/pkg/manager
|
||||
sigs.k8s.io/controller-runtime/pkg/webhook/admission from sigs.k8s.io/controller-runtime/pkg/builder+
|
||||
sigs.k8s.io/controller-runtime/pkg/webhook/conversion from sigs.k8s.io/controller-runtime/pkg/builder
|
||||
sigs.k8s.io/controller-runtime/pkg/webhook/internal/metrics from sigs.k8s.io/controller-runtime/pkg/webhook+
|
||||
sigs.k8s.io/json from k8s.io/apimachinery/pkg/runtime/serializer/json+
|
||||
sigs.k8s.io/json/internal/golang/encoding/json from sigs.k8s.io/json
|
||||
💣 sigs.k8s.io/structured-merge-diff/v4/fieldpath from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
sigs.k8s.io/structured-merge-diff/v4/merge from k8s.io/apimachinery/pkg/util/managedfields/internal
|
||||
sigs.k8s.io/structured-merge-diff/v4/schema from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
sigs.k8s.io/structured-merge-diff/v4/typed from k8s.io/apimachinery/pkg/util/managedfields+
|
||||
sigs.k8s.io/structured-merge-diff/v4/value from k8s.io/apimachinery/pkg/runtime+
|
||||
sigs.k8s.io/yaml from k8s.io/apimachinery/pkg/runtime/serializer/json+
|
||||
sigs.k8s.io/yaml/goyaml.v2 from sigs.k8s.io/yaml
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/appc from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/client/web+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/web from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/clientupdate from tailscale.com/client/web+
|
||||
tailscale.com/clientupdate/distsign from tailscale.com/clientupdate
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlhttp from tailscale.com/control/controlclient
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/ipn/localapi+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/ethtool from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/drive from tailscale.com/client/tailscale+
|
||||
tailscale.com/envknob from tailscale.com/client/tailscale+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/hostinfo from tailscale.com/client/web+
|
||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||
tailscale.com/ipn from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/localapi+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/ipn/localapi from tailscale.com/tsnet
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/ipn/ipnlocal+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
|
||||
tailscale.com/ipn/store/kubestore from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/k8s-operator from tailscale.com/cmd/k8s-operator
|
||||
tailscale.com/k8s-operator/apis from tailscale.com/k8s-operator/apis/v1alpha1
|
||||
tailscale.com/k8s-operator/apis/v1alpha1 from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/k8s-operator/sessionrecording from tailscale.com/cmd/k8s-operator
|
||||
tailscale.com/k8s-operator/sessionrecording/conn from tailscale.com/k8s-operator/sessionrecording/spdy
|
||||
tailscale.com/k8s-operator/sessionrecording/spdy from tailscale.com/k8s-operator/sessionrecording
|
||||
tailscale.com/k8s-operator/sessionrecording/tsrecorder from tailscale.com/k8s-operator/sessionrecording+
|
||||
tailscale.com/kube from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/licenses from tailscale.com/client/web
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/captivedetection from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dns/resolvconffile from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/net/dns/resolver from tailscale.com/net/dns
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
tailscale.com/net/ipset from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/memnet from tailscale.com/tsnet
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
💣 tailscale.com/net/netmon from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
tailscale.com/net/packet/checksum from tailscale.com/net/tstun
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/portmapper from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/proxymux from tailscale.com/tsnet
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/tsnet
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/ipn/localapi+
|
||||
L tailscale.com/net/tcpinfo from tailscale.com/derp
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/client/web+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/tstun from tailscale.com/tsd+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/omit from tailscale.com/ipn/conffile
|
||||
tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
tailscale.com/sessionrecording from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/syncs from tailscale.com/control/controlknobs+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tsd from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tsnet from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/tstime from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/derp+
|
||||
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/ipn+
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/lazy from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/logger from tailscale.com/appc+
|
||||
tailscale.com/types/logid from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/netlogtype from tailscale.com/net/connstats+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/ipn/localapi+
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/ptr from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/tkatype from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/views from tailscale.com/appc+
|
||||
tailscale.com/util/cibuild from tailscale.com/health
|
||||
tailscale.com/util/clientmetric from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/cmpver from tailscale.com/clientupdate+
|
||||
tailscale.com/util/ctxkey from tailscale.com/cmd/k8s-operator+
|
||||
💣 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/appc+
|
||||
tailscale.com/util/execqueue from tailscale.com/appc+
|
||||
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/groupmember from tailscale.com/client/web+
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/httphdr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/httpm from tailscale.com/client/tailscale+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
|
||||
tailscale.com/util/mak from tailscale.com/appc+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
|
||||
💣 tailscale.com/util/osdiag from tailscale.com/ipn/localapi
|
||||
W 💣 tailscale.com/util/osdiag/internal/wsc from tailscale.com/util/osdiag
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/util/osuser from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/progresstracking from tailscale.com/ipn/localapi
|
||||
tailscale.com/util/race from tailscale.com/net/dns/resolver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/set from tailscale.com/cmd/k8s-operator+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/appc+
|
||||
tailscale.com/util/syspolicy from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/testenv from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/truncate from tailscale.com/logtail
|
||||
tailscale.com/util/uniq from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/client/web+
|
||||
tailscale.com/version/distro from tailscale.com/client/web+
|
||||
tailscale.com/wgengine from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/filter/filtertype from tailscale.com/types/netmap+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/netlog from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/tsnet
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf
|
||||
golang.org/x/crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/exp/constraints from github.com/dblohm7/wingoes/pe+
|
||||
golang.org/x/exp/maps from sigs.k8s.io/controller-runtime/pkg/cache+
|
||||
golang.org/x/exp/slices from tailscale.com/cmd/k8s-operator+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
|
||||
golang.org/x/net/http/httpproxy from net/http+
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from github.com/prometheus-community/pro-bing+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/miekg/dns+
|
||||
golang.org/x/net/ipv6 from github.com/miekg/dns+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials+
|
||||
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/k8s-operator
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2+
|
||||
golang.org/x/sync/errgroup from github.com/mdlayher/socket+
|
||||
golang.org/x/sys/cpu from github.com/josharian/native+
|
||||
LD golang.org/x/sys/unix from github.com/fsnotify/fsnotify+
|
||||
W golang.org/x/sys/windows from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/registry from github.com/dblohm7/wingoes+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
|
||||
golang.org/x/term from k8s.io/client-go/plugin/pkg/client/auth/exec+
|
||||
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
|
||||
golang.org/x/time/rate from gvisor.dev/gvisor/pkg/log+
|
||||
archive/tar from tailscale.com/clientupdate
|
||||
bufio from compress/flate+
|
||||
bytes from archive/tar+
|
||||
cmp from github.com/gaissmai/bart+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding+
|
||||
compress/zlib from debug/pe+
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp+
|
||||
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 github.com/aws/aws-sdk-go-v2/aws/transport/http+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
database/sql from github.com/prometheus/client_golang/prometheus/collectors
|
||||
database/sql/driver from database/sql+
|
||||
W debug/dwarf from debug/pe
|
||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/gob+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from github.com/fxamacker/cbor/v2+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/csv from github.com/spf13/pflag
|
||||
encoding/gob from github.com/gorilla/securecookie
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/aws/aws-sdk-go-v2/aws/protocol/xml+
|
||||
errors from archive/tar+
|
||||
expvar from github.com/prometheus/client_golang/prometheus+
|
||||
flag from github.com/spf13/pflag+
|
||||
fmt from archive/tar+
|
||||
go/ast from go/doc+
|
||||
go/build/constraint from go/parser
|
||||
go/doc from k8s.io/apimachinery/pkg/runtime
|
||||
go/doc/comment from go/doc
|
||||
go/parser from k8s.io/apimachinery/pkg/runtime
|
||||
go/scanner from go/ast+
|
||||
go/token from go/ast+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from google.golang.org/protobuf/internal/detrand
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template+
|
||||
html/template from github.com/gorilla/csrf
|
||||
io from archive/tar+
|
||||
io/fs from archive/tar+
|
||||
io/ioutil from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
log from expvar+
|
||||
log/internal from log+
|
||||
log/slog from github.com/go-logr/logr+
|
||||
log/slog/internal from log/slog
|
||||
maps from sigs.k8s.io/controller-runtime/pkg/predicate+
|
||||
math from archive/tar+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/google/go-cmp/cmp+
|
||||
math/rand/v2 from tailscale.com/derp+
|
||||
mime from github.com/prometheus/common/expfmt+
|
||||
mime/multipart from github.com/go-openapi/swag+
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/httptest from tailscale.com/control/controlclient
|
||||
net/http/httptrace from github.com/prometheus-community/pro-bing+
|
||||
net/http/httputil from github.com/aws/smithy-go/transport/http+
|
||||
net/http/internal from net/http+
|
||||
net/http/pprof from sigs.k8s.io/controller-runtime/pkg/manager+
|
||||
net/netip from github.com/gaissmai/bart+
|
||||
net/textproto from github.com/aws/aws-sdk-go-v2/aws/signer/v4+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/aws/aws-sdk-go-v2/credentials/processcreds+
|
||||
os/signal from sigs.k8s.io/controller-runtime/pkg/manager/signals
|
||||
os/user from archive/tar+
|
||||
path from archive/tar+
|
||||
path/filepath from archive/tar+
|
||||
reflect from archive/tar+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
|
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/trace from net/http/pprof
|
||||
slices from encoding/base32+
|
||||
sort from archive/tar+
|
||||
strconv from archive/tar+
|
||||
strings from archive/tar+
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from archive/tar+
|
||||
text/tabwriter from k8s.io/apimachinery/pkg/util/diff+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from archive/tar+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from crypto/x509+
|
||||
unicode/utf8 from bufio+
|
||||
@@ -77,6 +77,9 @@ spec:
|
||||
value: "{{ .Values.apiServerProxyConfig.mode }}"
|
||||
- name: PROXY_FIREWALL_MODE
|
||||
value: {{ .Values.proxyConfig.firewallMode }}
|
||||
{{- with .Values.operatorConfig.extraEnv }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
- name: oauth
|
||||
mountPath: /oauth
|
||||
|
||||
@@ -48,6 +48,13 @@ operatorConfig:
|
||||
|
||||
securityContext: {}
|
||||
|
||||
extraEnv: []
|
||||
# - name: EXTRA_VAR1
|
||||
# value: "value1"
|
||||
# - name: EXTRA_VAR2
|
||||
# value: "value2"
|
||||
|
||||
|
||||
# proxyConfig contains configuraton that will be applied to any ingress/egress
|
||||
# proxies created by the operator.
|
||||
# https://tailscale.com/kb/1236/kubernetes-operator/#cluster-ingress
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// tailscale-operator provides a way to expose services running in a Kubernetes
|
||||
// cluster to your Tailnet and to make Tailscale nodes available to cluster
|
||||
// workloads
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// The generate command creates tailscale.com CRDs.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -51,8 +51,8 @@ import (
|
||||
// Generate static manifests for deploying Tailscale operator on Kubernetes from the operator's Helm chart.
|
||||
//go:generate go run tailscale.com/cmd/k8s-operator/generate staticmanifests
|
||||
|
||||
// Generate CRD docs from the yamls
|
||||
//go:generate go run fybrik.io/crdoc --resources=./deploy/crds --output=../../k8s-operator/api.md
|
||||
// Generate CRD API docs.
|
||||
//go:generate go run github.com/elastic/crd-ref-docs --renderer=markdown --source-path=../../k8s-operator/apis/ --config=../../k8s-operator/api-docs-config.yaml --output-path=../../k8s-operator/api.md
|
||||
|
||||
func main() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
|
||||
@@ -11,16 +11,20 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
kubesessionrecording "tailscale.com/k8s-operator/sessionrecording"
|
||||
tskube "tailscale.com/kube"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -30,10 +34,26 @@ import (
|
||||
|
||||
var whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
|
||||
|
||||
var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
|
||||
var (
|
||||
// counterNumRequestsproxies counts the number of API server requests proxied via this proxy.
|
||||
counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
|
||||
)
|
||||
|
||||
type apiServerProxyMode int
|
||||
|
||||
func (a apiServerProxyMode) String() string {
|
||||
switch a {
|
||||
case apiserverProxyModeDisabled:
|
||||
return "disabled"
|
||||
case apiserverProxyModeEnabled:
|
||||
return "auth"
|
||||
case apiserverProxyModeNoAuth:
|
||||
return "noauth"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
apiserverProxyModeDisabled apiServerProxyMode = iota
|
||||
apiserverProxyModeEnabled
|
||||
@@ -97,26 +117,7 @@ func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config,
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode)
|
||||
}
|
||||
|
||||
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type apiserverProxy struct {
|
||||
log *zap.SugaredLogger
|
||||
lc *tailscale.LocalClient
|
||||
rp *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
if err != nil {
|
||||
h.log.Errorf("failed to authenticate caller: %v", err)
|
||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
h.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode, restConfig.Host)
|
||||
}
|
||||
|
||||
// runAPIServerProxy runs an HTTP server that authenticates requests using the
|
||||
@@ -133,64 +134,42 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// are passed through to the Kubernetes API.
|
||||
//
|
||||
// It never returns.
|
||||
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode) {
|
||||
func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode, host string) {
|
||||
if mode == apiserverProxyModeDisabled {
|
||||
return
|
||||
}
|
||||
ln, err := s.Listen("tcp", ":443")
|
||||
ln, err := ts.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatalf("could not listen on :443: %v", err)
|
||||
}
|
||||
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
lc, err := ts.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get local client: %v", err)
|
||||
}
|
||||
|
||||
ap := &apiserverProxy{
|
||||
log: log,
|
||||
lc: lc,
|
||||
rp: &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
|
||||
r.Out.URL.Scheme = u.Scheme
|
||||
r.Out.URL.Host = u.Host
|
||||
if mode == apiserverProxyModeNoAuth {
|
||||
// If we are not providing authentication, then we are just
|
||||
// proxying to the Kubernetes API, so we don't need to do
|
||||
// anything else.
|
||||
return
|
||||
}
|
||||
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Out.Header.Del("Authorization")
|
||||
r.Out.Header.Del("Impersonate-Group")
|
||||
r.Out.Header.Del("Impersonate-User")
|
||||
r.Out.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Out.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
r.Out.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
if err := addImpersonationHeaders(r.Out, log); err != nil {
|
||||
panic("failed to add impersonation headers: " + err.Error())
|
||||
}
|
||||
},
|
||||
Transport: rt,
|
||||
},
|
||||
log: log,
|
||||
lc: lc,
|
||||
mode: mode,
|
||||
upstreamURL: u,
|
||||
ts: ts,
|
||||
}
|
||||
ap.rp = &httputil.ReverseProxy{
|
||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
ap.addImpersonationHeadersAsRequired(pr.Out)
|
||||
},
|
||||
Transport: rt,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", ap.serveDefault)
|
||||
mux.HandleFunc("/api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExec)
|
||||
|
||||
hs := &http.Server{
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
@@ -199,14 +178,120 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLo
|
||||
NextProtos: []string{"http/1.1"},
|
||||
},
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: ap,
|
||||
Handler: mux,
|
||||
}
|
||||
log.Infof("listening on %s", ln.Addr())
|
||||
log.Infof("API server proxy in %q mode is listening on %s", mode, ln.Addr())
|
||||
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// apiserverProxy is an [net/http.Handler] that authenticates requests using the Tailscale
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type apiserverProxy struct {
|
||||
log *zap.SugaredLogger
|
||||
lc *tailscale.LocalClient
|
||||
rp *httputil.ReverseProxy
|
||||
|
||||
mode apiServerProxyMode
|
||||
ts *tsnet.Server
|
||||
upstreamURL *url.URL
|
||||
}
|
||||
|
||||
// serveDefault is the default handler for Kubernetes API server requests.
|
||||
func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := ap.whoIs(r)
|
||||
if err != nil {
|
||||
ap.authError(w, err)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
}
|
||||
|
||||
// serveExec serves 'kubectl exec' requests, optionally configuring the kubectl
|
||||
// exec sessions to be recorded.
|
||||
func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := ap.whoIs(r)
|
||||
if err != nil {
|
||||
ap.authError(w, err)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
failOpen, addrs, err := determineRecorderConfig(who)
|
||||
if err != nil {
|
||||
ap.log.Errorf("error trying to determine whether the 'kubectl exec' session needs to be recorded: %v", err)
|
||||
return
|
||||
}
|
||||
if failOpen && len(addrs) == 0 { // will not record
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
return
|
||||
}
|
||||
kubesessionrecording.CounterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
|
||||
if !failOpen && len(addrs) == 0 {
|
||||
msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
|
||||
ap.log.Error(msg)
|
||||
http.Error(w, msg, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" || r.Header.Get("Upgrade") != "SPDY/3.1" {
|
||||
msg := "'kubectl exec' session recording is configured, but the request is not over SPDY. Session recording is currently only supported for SPDY based clients"
|
||||
if failOpen {
|
||||
msg = msg + "; failure mode is 'fail open'; continuing session without recording."
|
||||
ap.log.Warn(msg)
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
return
|
||||
}
|
||||
ap.log.Error(msg)
|
||||
msg += "; failure mode is 'fail closed'; closing connection."
|
||||
http.Error(w, msg, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
spdyH := kubesessionrecording.New(ap.ts, r, who, w, r.PathValue("pod"), r.PathValue("namespace"), kubesessionrecording.SPDYProtocol, addrs, failOpen, sessionrecording.ConnectToRecorder, ap.log)
|
||||
|
||||
ap.rp.ServeHTTP(spdyH, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
}
|
||||
|
||||
func (h *apiserverProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
||||
r.URL.Scheme = h.upstreamURL.Scheme
|
||||
r.URL.Host = h.upstreamURL.Host
|
||||
if h.mode == apiserverProxyModeNoAuth {
|
||||
// If we are not providing authentication, then we are just
|
||||
// proxying to the Kubernetes API, so we don't need to do
|
||||
// anything else.
|
||||
return
|
||||
}
|
||||
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Header.Del("Authorization")
|
||||
r.Header.Del("Impersonate-Group")
|
||||
r.Header.Del("Impersonate-User")
|
||||
r.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
r.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
if err := addImpersonationHeaders(r, h.log); err != nil {
|
||||
log.Printf("failed to add impersonation headers: " + err.Error())
|
||||
}
|
||||
}
|
||||
func (ap *apiserverProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
|
||||
return ap.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
}
|
||||
func (ap *apiserverProxy) authError(w http.ResponseWriter, err error) {
|
||||
ap.log.Errorf("failed to authenticate caller: %v", err)
|
||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
const (
|
||||
// oldCapabilityName is a legacy form of
|
||||
// tailfcg.PeerCapabilityKubernetes capability. The only capability rule
|
||||
@@ -266,3 +351,34 @@ func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineRecorderConfig determines recorder config from requester's peer
|
||||
// capabilities. Determines whether a 'kubectl exec' session from this requester
|
||||
// needs to be recorded and what recorders the recording should be sent to.
|
||||
func determineRecorderConfig(who *apitype.WhoIsResponse) (failOpen bool, recorderAddresses []netip.AddrPort, _ error) {
|
||||
if who == nil {
|
||||
return false, nil, errors.New("[unexpected] cannot determine caller")
|
||||
}
|
||||
failOpen = true
|
||||
rules, err := tailcfg.UnmarshalCapJSON[tskube.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
|
||||
if err != nil {
|
||||
return failOpen, nil, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err)
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return failOpen, nil, nil
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if len(rule.RecorderAddrs) != 0 {
|
||||
// TODO (irbekrm): here or later determine if the
|
||||
// recorders behind those addrs are online - else we
|
||||
// spend 30s trying to reach a recorder whose tailscale
|
||||
// status is offline.
|
||||
recorderAddresses = append(recorderAddresses, rule.RecorderAddrs...)
|
||||
}
|
||||
if rule.EnforceRecorder {
|
||||
failOpen = false
|
||||
}
|
||||
}
|
||||
return failOpen, recorderAddresses, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -126,3 +128,72 @@ func TestImpersonationHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_determineRecorderConfig(t *testing.T) {
|
||||
addr1, addr2 := netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"), netip.MustParseAddrPort("100.99.99.99:80")
|
||||
tests := []struct {
|
||||
name string
|
||||
wantFailOpen bool
|
||||
wantRecorderAddresses []netip.AddrPort
|
||||
who *apitype.WhoIsResponse
|
||||
}{
|
||||
{
|
||||
name: "two_ips_fail_closed",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
|
||||
},
|
||||
{
|
||||
name: "two_ips_fail_open",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
|
||||
wantFailOpen: true,
|
||||
},
|
||||
{
|
||||
name: "odd_rule_combination_fail_closed",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr2, addr1},
|
||||
},
|
||||
{
|
||||
name: "no_caps",
|
||||
who: whoResp(map[string][]string{}),
|
||||
wantFailOpen: true,
|
||||
},
|
||||
{
|
||||
name: "no_recorder_caps",
|
||||
who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(tailcfg.PeerCapabilityKubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}),
|
||||
wantFailOpen: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotFailOpen, gotRecorderAddresses, err := determineRecorderConfig(tt.who)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotFailOpen != tt.wantFailOpen {
|
||||
t.Errorf("determineRecorderConfig() gotFailOpen = %v, want %v", gotFailOpen, tt.wantFailOpen)
|
||||
}
|
||||
if !reflect.DeepEqual(gotRecorderAddresses, tt.wantRecorderAddresses) {
|
||||
t.Errorf("determineRecorderConfig() gotRecorderAddresses = %v, want %v", gotRecorderAddresses, tt.wantRecorderAddresses)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func whoResp(capMap map[string][]string) *apitype.WhoIsResponse {
|
||||
resp := &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{},
|
||||
}
|
||||
for cap, rules := range capMap {
|
||||
resp.CapMap[tailcfg.PeerCapability(cap)] = raw(rules...)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func raw(in ...string) []tailcfg.RawMessage {
|
||||
var out []tailcfg.RawMessage
|
||||
for _, i := range in {
|
||||
out = append(out, tailcfg.RawMessage(i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dockerref "github.com/distribution/reference"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -25,6 +28,8 @@ import (
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,8 +46,20 @@ type ProxyClassReconciler struct {
|
||||
recorder record.EventRecorder
|
||||
logger *zap.SugaredLogger
|
||||
clock tstime.Clock
|
||||
|
||||
mu sync.Mutex // protects following
|
||||
|
||||
// managedProxyClasses is a set of all ProxyClass resources that we're currently
|
||||
// managing. This is only used for metrics.
|
||||
managedProxyClasses set.Slice[types.UID]
|
||||
}
|
||||
|
||||
var (
|
||||
// gaugeProxyClassResources tracks the number of ProxyClass resources
|
||||
// that we're currently managing.
|
||||
gaugeProxyClassResources = clientmetric.NewGauge("k8s_proxyclass_resources")
|
||||
)
|
||||
|
||||
func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
|
||||
logger := pcr.logger.With("ProxyClass", req.Name)
|
||||
logger.Debugf("starting reconcile")
|
||||
@@ -57,9 +74,26 @@ func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Re
|
||||
return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com ProxyClass: %w", err)
|
||||
}
|
||||
if !pc.DeletionTimestamp.IsZero() {
|
||||
logger.Debugf("ProxyClass is being deleted, do nothing")
|
||||
return reconcile.Result{}, nil
|
||||
logger.Debugf("ProxyClass is being deleted")
|
||||
return reconcile.Result{}, pcr.maybeCleanup(ctx, logger, pc)
|
||||
}
|
||||
|
||||
// Add a finalizer so that we can ensure that metrics get updated when
|
||||
// this ProxyClass is deleted.
|
||||
if !slices.Contains(pc.Finalizers, FinalizerName) {
|
||||
logger.Debugf("updating ProxyClass finalizers")
|
||||
pc.Finalizers = append(pc.Finalizers, FinalizerName)
|
||||
if err := pcr.Update(ctx, pc); err != nil {
|
||||
return res, fmt.Errorf("failed to add finalizer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure this ProxyClass is tracked in metrics.
|
||||
pcr.mu.Lock()
|
||||
pcr.managedProxyClasses.Add(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
pcr.mu.Unlock()
|
||||
|
||||
oldPCStatus := pc.Status.DeepCopy()
|
||||
if errs := pcr.validate(pc); errs != nil {
|
||||
msg := fmt.Sprintf(messageProxyClassInvalid, errs.ToAggregate().Error())
|
||||
@@ -77,7 +111,7 @@ func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Re
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
|
||||
func (pcr *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
|
||||
if sts := pc.Spec.StatefulSet; sts != nil {
|
||||
if len(sts.Labels) > 0 {
|
||||
if errs := metavalidation.ValidateLabels(sts.Labels, field.NewPath(".spec.statefulSet.labels")); errs != nil {
|
||||
@@ -103,13 +137,13 @@ func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.
|
||||
if tc := pod.TailscaleContainer; tc != nil {
|
||||
for _, e := range tc.Env {
|
||||
if strings.HasPrefix(string(e.Name), "TS_") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_TS_CONFIGFILE_PATH") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
}
|
||||
if tc.Image != "" {
|
||||
@@ -135,3 +169,27 @@ func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.
|
||||
// time.
|
||||
return violations
|
||||
}
|
||||
|
||||
// maybeCleanup removes tailscale.com finalizer and ensures that the ProxyClass
|
||||
// is no longer counted towards k8s_proxyclass_resources.
|
||||
func (pcr *ProxyClassReconciler) maybeCleanup(ctx context.Context, logger *zap.SugaredLogger, pc *tsapi.ProxyClass) error {
|
||||
ix := slices.Index(pc.Finalizers, FinalizerName)
|
||||
if ix < 0 {
|
||||
logger.Debugf("no finalizer, nothing to do")
|
||||
pcr.mu.Lock()
|
||||
defer pcr.mu.Unlock()
|
||||
pcr.managedProxyClasses.Remove(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
return nil
|
||||
}
|
||||
pc.Finalizers = append(pc.Finalizers[:ix], pc.Finalizers[ix+1:]...)
|
||||
if err := pcr.Update(ctx, pc); err != nil {
|
||||
return fmt.Errorf("failed to remove finalizer: %w", err)
|
||||
}
|
||||
pcr.mu.Lock()
|
||||
defer pcr.mu.Unlock()
|
||||
pcr.managedProxyClasses.Remove(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
logger.Infof("ProxyClass resources have been cleaned up")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ func TestProxyClass(t *testing.T) {
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
UID: types.UID("1234-UID"),
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
},
|
||||
Spec: tsapi.ProxyClassSpec{
|
||||
StatefulSet: &tsapi.StatefulSet{
|
||||
|
||||
@@ -294,6 +294,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
|
||||
Selector: map[string]string{
|
||||
"app": sts.ParentResourceUID,
|
||||
},
|
||||
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||
},
|
||||
}
|
||||
logger.Debugf("reconciling headless service for StatefulSet")
|
||||
|
||||
@@ -319,7 +319,8 @@ func expectedHeadlessService(name string, parentType string) *corev1.Service {
|
||||
Selector: map[string]string{
|
||||
"app": "1234-UID",
|
||||
},
|
||||
ClusterIP: "None",
|
||||
ClusterIP: "None",
|
||||
IPFamilyPolicy: ptr.To(corev1.IPFamilyPolicyPreferDualStack),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,7 +448,7 @@ func (c *connector) handleTCPFlow(src, dst netip.AddrPort) (handler func(net.Con
|
||||
// in --ignore-destinations
|
||||
func (c *connector) ignoreDestination(dstAddrs []netip.Addr) bool {
|
||||
for _, a := range dstAddrs {
|
||||
if _, ok := c.ignoreDsts.Get(a); ok {
|
||||
if _, ok := c.ignoreDsts.Lookup(a); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -489,7 +489,7 @@ type perPeerState struct {
|
||||
func (ps *perPeerState) domainForIP(ip netip.Addr) (_ string, ok bool) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
return ps.addrToDomain.Get(ip)
|
||||
return ps.addrToDomain.Lookup(ip)
|
||||
}
|
||||
|
||||
// ipForDomain assigns a pair of unique IP addresses for the given domain and
|
||||
@@ -515,7 +515,7 @@ func (ps *perPeerState) ipForDomain(domain string) ([]netip.Addr, error) {
|
||||
// domain.
|
||||
// ps.mu must be held.
|
||||
func (ps *perPeerState) isIPUsedLocked(ip netip.Addr) bool {
|
||||
_, ok := ps.addrToDomain.Get(ip)
|
||||
_, ok := ps.addrToDomain.Lookup(ip)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ var (
|
||||
backendAddr = flag.String("backend-addr", "", "Address of the Grafana server served over HTTP, in host:port format. Typically localhost:nnnn.")
|
||||
tailscaleDir = flag.String("state-dir", "./", "Alternate directory to use for Tailscale state storage. If empty, a default is used.")
|
||||
useHTTPS = flag.Bool("use-https", false, "Serve over HTTPS via your *.ts.net subdomain if enabled in Tailscale admin.")
|
||||
loginServer = flag.String("login-server", "", "URL to alternative control server. If empty, the default Tailscale control is used.")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -57,8 +58,9 @@ func main() {
|
||||
log.Fatal("missing --backend-addr")
|
||||
}
|
||||
ts := &tsnet.Server{
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
ControlURL: *loginServer,
|
||||
}
|
||||
|
||||
// TODO(bradfitz,maisem): move this to a method on tsnet.Server probably.
|
||||
|
||||
@@ -2,6 +2,12 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/google/uuid from tailscale.com/util/fastuuid
|
||||
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
|
||||
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
|
||||
@@ -59,7 +65,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
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/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+
|
||||
@@ -128,6 +134,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base32 from github.com/go-json-experiment/json
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
encoding/hex from crypto/x509+
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
type api struct {
|
||||
db *db
|
||||
mux *http.ServeMux
|
||||
}
|
||||
|
||||
func newAPI(db *db) *api {
|
||||
a := &api{
|
||||
db: db,
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/query", a.query)
|
||||
a.mux = mux
|
||||
return a
|
||||
}
|
||||
|
||||
type apiResult struct {
|
||||
At int `json:"at"` // time.Time.Unix()
|
||||
RegionID int `json:"regionID"`
|
||||
Hostname string `json:"hostname"`
|
||||
Af int `json:"af"` // 4 or 6
|
||||
Addr string `json:"addr"`
|
||||
Source int `json:"source"` // timestampSourceUserspace (0) or timestampSourceKernel (1)
|
||||
StableConn bool `json:"stableConn"`
|
||||
DstPort int `json:"dstPort"`
|
||||
RttNS *int `json:"rttNS"`
|
||||
}
|
||||
|
||||
func getTimeBounds(vals url.Values) (from time.Time, to time.Time, err error) {
|
||||
lastForm, ok := vals["last"]
|
||||
if ok && len(lastForm) > 0 {
|
||||
dur, err := time.ParseDuration(lastForm[0])
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
now := time.Now()
|
||||
return now.Add(-dur), now, nil
|
||||
}
|
||||
|
||||
fromForm, ok := vals["from"]
|
||||
if ok && len(fromForm) > 0 {
|
||||
fromUnixSec, err := strconv.Atoi(fromForm[0])
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
from = time.Unix(int64(fromUnixSec), 0)
|
||||
toForm, ok := vals["to"]
|
||||
if ok && len(toForm) > 0 {
|
||||
toUnixSec, err := strconv.Atoi(toForm[0])
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
to = time.Unix(int64(toUnixSec), 0)
|
||||
} else {
|
||||
return time.Time{}, time.Time{}, errors.New("from specified without to")
|
||||
}
|
||||
return from, to, nil
|
||||
}
|
||||
|
||||
// no time bounds specified, default to last 1h
|
||||
now := time.Now()
|
||||
return now.Add(-time.Hour), now, nil
|
||||
}
|
||||
|
||||
func (a *api) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (a *api) query(w http.ResponseWriter, r *http.Request) {
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
from, to, err := getTimeBounds(r.Form)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
sb := sq.Select("at_unix", "region_id", "hostname", "af", "address", "timestamp_source", "stable_conn", "dst_port", "rtt_ns").From("rtt")
|
||||
sb = sb.Where(sq.And{
|
||||
sq.GtOrEq{"at_unix": from.Unix()},
|
||||
sq.LtOrEq{"at_unix": to.Unix()},
|
||||
})
|
||||
query, args, err := sb.ToSql()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := a.db.Query(query, args...)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
results := make([]apiResult, 0)
|
||||
for rows.Next() {
|
||||
rtt := 0
|
||||
result := apiResult{
|
||||
RttNS: &rtt,
|
||||
}
|
||||
err = rows.Scan(&result.At, &result.RegionID, &result.Hostname, &result.Af, &result.Addr, &result.Source, &result.StableConn, &result.DstPort, &result.RttNS)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
http.Error(w, rows.Err().Error(), 500)
|
||||
return
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
gz := gzip.NewWriter(w)
|
||||
defer gz.Close()
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
err = json.NewEncoder(gz).Encode(&results)
|
||||
} else {
|
||||
err = json.NewEncoder(w).Encode(&results)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,8 @@ import (
|
||||
|
||||
var (
|
||||
flagDERPMap = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map")
|
||||
flagOut = flag.String("out", "", "output sqlite filename")
|
||||
flagInterval = flag.Duration("interval", time.Minute, "interval to probe at in time.ParseDuration() format")
|
||||
flagAPI = flag.String("api", "", "listen addr for HTTP API")
|
||||
flagIPv6 = flag.Bool("ipv6", false, "probe IPv6 addresses")
|
||||
flagRetention = flag.Duration("retention", time.Hour*24*7, "sqlite retention period in time.ParseDuration() format")
|
||||
flagRemoteWriteURL = flag.String("rw-url", "", "prometheus remote write URL")
|
||||
flagInstance = flag.String("instance", "", "instance label value; defaults to hostname if unspecified")
|
||||
flagDstPorts = flag.String("dst-ports", "", "comma-separated list of destination ports to monitor")
|
||||
@@ -63,10 +60,13 @@ func getDERPMap(ctx context.Context, url string) (*tailcfg.DERPMap, error) {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("non-200 derp map resp: %d", resp.StatusCode)
|
||||
}
|
||||
dm := tailcfg.DERPMap{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&dm)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, fmt.Errorf("failed to decode derp map resp: %v", err)
|
||||
}
|
||||
return &dm, nil
|
||||
}
|
||||
@@ -89,13 +89,19 @@ func (t timestampSource) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
type result struct {
|
||||
at time.Time
|
||||
// resultKey contains the stable dimensions and their values for a given
|
||||
// timeseries, i.e. not time and not rtt/timeout.
|
||||
type resultKey struct {
|
||||
meta nodeMeta
|
||||
timestampSource timestampSource
|
||||
connStability connStability
|
||||
dstPort int
|
||||
rtt *time.Duration // nil signifies failure, e.g. timeout
|
||||
}
|
||||
|
||||
type result struct {
|
||||
key resultKey
|
||||
at time.Time
|
||||
rtt *time.Duration // nil signifies failure, e.g. timeout
|
||||
}
|
||||
|
||||
func measureRTT(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error) {
|
||||
@@ -149,6 +155,10 @@ type nodeMeta struct {
|
||||
|
||||
type measureFn func(conn io.ReadWriteCloser, dst *net.UDPAddr) (rtt time.Duration, err error)
|
||||
|
||||
// probe measures STUN round trip time for the node described by meta over
|
||||
// conn against dstPort. It may return a nil duration and nil error if the
|
||||
// STUN request timed out. A non-nil error indicates an unrecoverable or
|
||||
// non-temporary error.
|
||||
func probe(meta nodeMeta, conn io.ReadWriteCloser, fn measureFn, dstPort int) (*time.Duration, error) {
|
||||
ua := &net.UDPAddr{
|
||||
IP: net.IP(meta.addr.AsSlice()),
|
||||
@@ -162,10 +172,15 @@ func probe(meta nodeMeta, conn io.ReadWriteCloser, fn measureFn, dstPort int) (*
|
||||
log.Printf("temp error measuring RTT to %s(%s): %v", meta.hostname, ua.String(), err)
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &rtt, nil
|
||||
}
|
||||
|
||||
// nodeMetaFromDERPMap parses the provided DERP map in order to update nodeMeta
|
||||
// in the provided nodeMetaByAddr. It returns a slice of nodeMeta containing
|
||||
// the nodes that are no longer seen in the DERP map, but were previously held
|
||||
// in nodeMetaByAddr.
|
||||
func nodeMetaFromDERPMap(dm *tailcfg.DERPMap, nodeMetaByAddr map[netip.Addr]nodeMeta, ipv6 bool) (stale []nodeMeta, err error) {
|
||||
// Parse the new derp map before making any state changes in nodeMetaByAddr.
|
||||
// If parse fails we just stick with the old state.
|
||||
@@ -271,10 +286,12 @@ func probeNodes(nodeMetaByAddr map[netip.Addr]nodeMeta, stableConns map[netip.Ad
|
||||
doProbe := func(conn io.ReadWriteCloser, meta nodeMeta, source timestampSource, dstPort int) {
|
||||
defer wg.Done()
|
||||
r := result{
|
||||
at: at,
|
||||
meta: meta,
|
||||
timestampSource: source,
|
||||
dstPort: dstPort,
|
||||
key: resultKey{
|
||||
meta: meta,
|
||||
timestampSource: source,
|
||||
dstPort: dstPort,
|
||||
},
|
||||
at: at,
|
||||
}
|
||||
if conn == nil {
|
||||
var err error
|
||||
@@ -293,7 +310,7 @@ func probeNodes(nodeMetaByAddr map[netip.Addr]nodeMeta, stableConns map[netip.Ad
|
||||
}
|
||||
defer conn.Close()
|
||||
} else {
|
||||
r.connStability = stableConn
|
||||
r.key.connStability = stableConn
|
||||
}
|
||||
fn := measureRTT
|
||||
if source == timestampSourceKernel {
|
||||
@@ -373,7 +390,12 @@ const (
|
||||
stableConn connStability = true
|
||||
)
|
||||
|
||||
func timeSeriesLabels(meta nodeMeta, instance string, source timestampSource, stability connStability, dstPort int) []prompb.Label {
|
||||
const (
|
||||
rttMetricName = "stunstamp_derp_stun_rtt_ns"
|
||||
timeoutsMetricName = "stunstamp_derp_stun_timeouts_total"
|
||||
)
|
||||
|
||||
func timeSeriesLabels(metricName string, meta nodeMeta, instance string, source timestampSource, stability connStability, dstPort int) []prompb.Label {
|
||||
addressFamily := "ipv4"
|
||||
if meta.addr.Is6() {
|
||||
addressFamily = "ipv6"
|
||||
@@ -409,7 +431,7 @@ func timeSeriesLabels(meta nodeMeta, instance string, source timestampSource, st
|
||||
})
|
||||
labels = append(labels, prompb.Label{
|
||||
Name: "__name__",
|
||||
Value: "stunstamp_derp_stun_rtt_ns",
|
||||
Value: metricName,
|
||||
})
|
||||
labels = append(labels, prompb.Label{
|
||||
Name: "timestamp_source",
|
||||
@@ -443,20 +465,36 @@ func staleMarkersFromNodeMeta(stale []nodeMeta, instance string, dstPorts []int)
|
||||
},
|
||||
}
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(s, instance, timestampSourceUserspace, unstableConn, dstPort),
|
||||
Labels: timeSeriesLabels(rttMetricName, s, instance, timestampSourceUserspace, unstableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(s, instance, timestampSourceUserspace, stableConn, dstPort),
|
||||
Labels: timeSeriesLabels(rttMetricName, s, instance, timestampSourceUserspace, stableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(timeoutsMetricName, s, instance, timestampSourceUserspace, unstableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(timeoutsMetricName, s, instance, timestampSourceUserspace, stableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
if supportsKernelTS() {
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(s, instance, timestampSourceKernel, unstableConn, dstPort),
|
||||
Labels: timeSeriesLabels(rttMetricName, s, instance, timestampSourceKernel, unstableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(s, instance, timestampSourceKernel, stableConn, dstPort),
|
||||
Labels: timeSeriesLabels(rttMetricName, s, instance, timestampSourceKernel, stableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(timeoutsMetricName, s, instance, timestampSourceKernel, unstableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
staleMarkers = append(staleMarkers, prompb.TimeSeries{
|
||||
Labels: timeSeriesLabels(timeoutsMetricName, s, instance, timestampSourceKernel, stableConn, dstPort),
|
||||
Samples: samples,
|
||||
})
|
||||
}
|
||||
@@ -465,25 +503,47 @@ func staleMarkersFromNodeMeta(stale []nodeMeta, instance string, dstPorts []int)
|
||||
return staleMarkers
|
||||
}
|
||||
|
||||
func resultToPromTimeSeries(r result, instance string) prompb.TimeSeries {
|
||||
labels := timeSeriesLabels(r.meta, instance, r.timestampSource, r.connStability, r.dstPort)
|
||||
samples := make([]prompb.Sample, 1)
|
||||
samples[0].Timestamp = r.at.UnixMilli()
|
||||
if r.rtt != nil {
|
||||
samples[0].Value = float64(*r.rtt)
|
||||
} else {
|
||||
samples[0].Value = math.NaN()
|
||||
// TODO: timeout counter
|
||||
// resultsToPromTimeSeries returns a slice of prometheus TimeSeries for the
|
||||
// provided results and instance. timeouts is updated based on results, i.e.
|
||||
// all result.key's are added to timeouts if they do not exist, and removed
|
||||
// from timeouts if they are not present in results.
|
||||
func resultsToPromTimeSeries(results []result, instance string, timeouts map[resultKey]uint64) []prompb.TimeSeries {
|
||||
all := make([]prompb.TimeSeries, 0, len(results)*2)
|
||||
seenKeys := make(map[resultKey]bool)
|
||||
for _, r := range results {
|
||||
timeoutsCount := timeouts[r.key] // a non-existent key will return a zero val
|
||||
seenKeys[r.key] = true
|
||||
rttLabels := timeSeriesLabels(rttMetricName, r.key.meta, instance, r.key.timestampSource, r.key.connStability, r.key.dstPort)
|
||||
rttSamples := make([]prompb.Sample, 1)
|
||||
rttSamples[0].Timestamp = r.at.UnixMilli()
|
||||
if r.rtt != nil {
|
||||
rttSamples[0].Value = float64(*r.rtt)
|
||||
} else {
|
||||
rttSamples[0].Value = math.NaN()
|
||||
timeoutsCount++
|
||||
}
|
||||
rttTS := prompb.TimeSeries{
|
||||
Labels: rttLabels,
|
||||
Samples: rttSamples,
|
||||
}
|
||||
all = append(all, rttTS)
|
||||
timeouts[r.key] = timeoutsCount
|
||||
timeoutsLabels := timeSeriesLabels(timeoutsMetricName, r.key.meta, instance, r.key.timestampSource, r.key.connStability, r.key.dstPort)
|
||||
timeoutsSamples := make([]prompb.Sample, 1)
|
||||
timeoutsSamples[0].Timestamp = r.at.UnixMilli()
|
||||
timeoutsSamples[0].Value = float64(timeoutsCount)
|
||||
timeoutsTS := prompb.TimeSeries{
|
||||
Labels: timeoutsLabels,
|
||||
Samples: timeoutsSamples,
|
||||
}
|
||||
all = append(all, timeoutsTS)
|
||||
}
|
||||
ts := prompb.TimeSeries{
|
||||
Labels: labels,
|
||||
Samples: samples,
|
||||
for k := range timeouts {
|
||||
if !seenKeys[k] {
|
||||
delete(timeouts, k)
|
||||
}
|
||||
}
|
||||
slices.SortFunc(ts.Labels, func(a, b prompb.Label) int {
|
||||
// prometheus remote-write spec requires lexicographically sorted label names
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
return ts
|
||||
return all
|
||||
}
|
||||
|
||||
type remoteWriteClient struct {
|
||||
@@ -579,15 +639,9 @@ func main() {
|
||||
if len(*flagDERPMap) < 1 {
|
||||
log.Fatal("derp-map flag is unset")
|
||||
}
|
||||
if len(*flagOut) < 1 {
|
||||
log.Fatal("out flag is unset")
|
||||
}
|
||||
if *flagInterval < minInterval || *flagInterval > maxBufferDuration {
|
||||
log.Fatalf("interval must be >= %s and <= %s", minInterval, maxBufferDuration)
|
||||
}
|
||||
if *flagRetention < *flagInterval {
|
||||
log.Fatal("retention must be >= interval")
|
||||
}
|
||||
if len(*flagRemoteWriteURL) < 1 {
|
||||
log.Fatal("rw-url flag is unset")
|
||||
}
|
||||
@@ -633,49 +687,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
db, err := newDB(*flagOut)
|
||||
if err != nil {
|
||||
log.Fatalf("error opening output file for writing: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
_, err = db.Exec("PRAGMA journal_mode=WAL")
|
||||
if err != nil {
|
||||
log.Fatalf("error enabling WAL mode: %v", err)
|
||||
}
|
||||
|
||||
// No indices or primary key. Keep it simple for now. Reads will be full
|
||||
// scans. We can AUTOINCREMENT rowid in the future and hold an in-memory
|
||||
// index to at_unix if needed as reads are almost always going to be
|
||||
// time-bound (e.g. WHERE at_unix >= ?). At the time of authorship we have
|
||||
// ~300 data points per-interval w/o ipv6 w/kernel timestamping resulting
|
||||
// in ~2.6m rows in 24h w/a 10s probe interval.
|
||||
_, err = db.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT, address TEXT, timestamp_source INT, stable_conn INT, dst_port INT, rtt_ns INT)
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatalf("error initializing db: %v", err)
|
||||
}
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
httpErrCh := make(chan error, 1)
|
||||
var httpServer *http.Server
|
||||
if len(*flagAPI) > 0 {
|
||||
api := newAPI(db)
|
||||
httpServer = &http.Server{
|
||||
Addr: *flagAPI,
|
||||
Handler: api,
|
||||
ReadTimeout: time.Second * 60,
|
||||
WriteTimeout: time.Second * 60,
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
err := httpServer.ListenAndServe()
|
||||
httpErrCh <- err
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
tsCh := make(chan []prompb.TimeSeries, maxBufferDuration / *flagInterval)
|
||||
remoteWriteDoneCh := make(chan struct{})
|
||||
rwc := newRemoteWriteClient(*flagRemoteWriteURL)
|
||||
@@ -685,9 +696,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
}()
|
||||
|
||||
shutdown := func() {
|
||||
if httpServer != nil {
|
||||
httpServer.Close()
|
||||
}
|
||||
close(tsCh)
|
||||
select {
|
||||
case <-time.After(time.Second * 10): // give goroutine some time to flush
|
||||
@@ -706,7 +714,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
cancel()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -719,24 +726,17 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
// comes into play.
|
||||
stableConns := make(map[netip.Addr]map[int][2]io.ReadWriteCloser)
|
||||
|
||||
// timeouts holds counts of timeout events. Values are persisted for the
|
||||
// lifetime of the related node in the DERP map.
|
||||
timeouts := make(map[resultKey]uint64)
|
||||
|
||||
derpMapTicker := time.NewTicker(time.Minute * 5)
|
||||
defer derpMapTicker.Stop()
|
||||
probeTicker := time.NewTicker(*flagInterval)
|
||||
defer probeTicker.Stop()
|
||||
cleanupTicker := time.NewTicker(time.Hour)
|
||||
defer cleanupTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cleanupTicker.C:
|
||||
older := time.Now().Add(-*flagRetention)
|
||||
log.Printf("cleaning up measurements older than %v", older)
|
||||
_, err := db.Exec("DELETE FROM rtt WHERE at_unix < ?", older.Unix())
|
||||
if err != nil {
|
||||
log.Printf("error cleaning up old data: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
}
|
||||
case <-probeTicker.C:
|
||||
results, err := probeNodes(nodeMetaByAddr, stableConns, dstPorts)
|
||||
if err != nil {
|
||||
@@ -744,10 +744,7 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
shutdown()
|
||||
return
|
||||
}
|
||||
ts := make([]prompb.TimeSeries, 0, len(results))
|
||||
for _, r := range results {
|
||||
ts = append(ts, resultToPromTimeSeries(r, *flagInstance))
|
||||
}
|
||||
ts := resultsToPromTimeSeries(results, *flagInstance, timeouts)
|
||||
select {
|
||||
case tsCh <- ts:
|
||||
default:
|
||||
@@ -758,32 +755,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
tsCh <- ts
|
||||
}
|
||||
}
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
log.Printf("error beginning sqlite tx: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
}
|
||||
for _, result := range results {
|
||||
af := 4
|
||||
if result.meta.addr.Is6() {
|
||||
af = 6
|
||||
}
|
||||
_, err = tx.Exec("INSERT INTO rtt(at_unix, region_id, hostname, af, address, timestamp_source, stable_conn, dst_port, rtt_ns) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
result.at.Unix(), result.meta.regionID, result.meta.hostname, af, result.meta.addr.String(), result.timestampSource, result.connStability, result.dstPort, result.rtt)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Printf("error adding result to tx: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
log.Printf("error committing tx: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
}
|
||||
case dm := <-dmCh:
|
||||
staleMeta, err := nodeMetaFromDERPMap(dm, nodeMetaByAddr, *flagIPv6)
|
||||
if err != nil {
|
||||
@@ -813,10 +784,6 @@ CREATE TABLE IF NOT EXISTS rtt(at_unix INT, region_id INT, hostname TEXT, af INT
|
||||
dmCh <- updatedDM
|
||||
}
|
||||
}()
|
||||
case err := <-httpErrCh:
|
||||
log.Printf("http server error: %v", err)
|
||||
shutdown()
|
||||
return
|
||||
case <-sigCh:
|
||||
shutdown()
|
||||
return
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(windows && 386)
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type db struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func newDB(path string) (*db, error) {
|
||||
d, err := sql.Open("sqlite", *flagOut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &db{
|
||||
DB: d,
|
||||
}, nil
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type db struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
func newDB(path string) (*db, error) {
|
||||
return nil, errors.New("unsupported platform")
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
@@ -34,14 +35,16 @@ var certCmd = &ffcli.Command{
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file or \"-\" for stdout; defaults to DOMAIN.crt if --cert-file and --key-file are both unset")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output key file or \"-\" for stdout; defaults to DOMAIN.key if --cert-file and --key-file are both unset")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
fs.DurationVar(&certArgs.minValidity, "min-validity", 0, "ensure the certificate is valid for at least this duration; the output certificate is never expired if this flag is unset or 0, but the lifetime may vary; the maximum allowed min-validity depends on the CA")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var certArgs struct {
|
||||
certFile string
|
||||
keyFile string
|
||||
serve bool
|
||||
certFile string
|
||||
keyFile string
|
||||
serve bool
|
||||
minValidity time.Duration
|
||||
}
|
||||
|
||||
func runCert(ctx context.Context, args []string) error {
|
||||
@@ -102,7 +105,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
certArgs.certFile = domain + ".crt"
|
||||
certArgs.keyFile = domain + ".key"
|
||||
}
|
||||
certPEM, keyPEM, err := localClient.CertPair(ctx, domain)
|
||||
certPEM, keyPEM, err := localClient.CertPairWithValidity(ctx, domain, certArgs.minValidity)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -159,8 +160,10 @@ func newRootCmd() *ffcli.Command {
|
||||
return nil
|
||||
})
|
||||
rootfs.Lookup("socket").DefValue = localClient.Socket
|
||||
jsonDocs := rootfs.Bool("json-docs", false, hidden+"print JSON-encoded docs for all subcommands and flags")
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
var rootCmd *ffcli.Command
|
||||
rootCmd = &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
@@ -202,6 +205,9 @@ change in the future.
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
if *jsonDocs {
|
||||
return printJSONDocs(rootCmd)
|
||||
}
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("tailscale: unknown subcommand: %s", args[0])
|
||||
}
|
||||
@@ -401,3 +407,54 @@ func colorableOutput() (w io.Writer, ok bool) {
|
||||
}
|
||||
return colorable.NewColorableStdout(), true
|
||||
}
|
||||
|
||||
type commandDoc struct {
|
||||
Name string
|
||||
Desc string
|
||||
Subcommands []commandDoc `json:",omitempty"`
|
||||
Flags []flagDoc `json:",omitempty"`
|
||||
}
|
||||
|
||||
type flagDoc struct {
|
||||
Name string
|
||||
Desc string
|
||||
}
|
||||
|
||||
func printJSONDocs(root *ffcli.Command) error {
|
||||
docs := jsonDocsWalk(root)
|
||||
return json.NewEncoder(os.Stdout).Encode(docs)
|
||||
}
|
||||
|
||||
func jsonDocsWalk(cmd *ffcli.Command) *commandDoc {
|
||||
res := &commandDoc{
|
||||
Name: cmd.Name,
|
||||
}
|
||||
if cmd.LongHelp != "" {
|
||||
res.Desc = cmd.LongHelp
|
||||
} else if cmd.ShortHelp != "" {
|
||||
res.Desc = cmd.ShortHelp
|
||||
} else {
|
||||
res.Desc = cmd.ShortUsage
|
||||
}
|
||||
if strings.HasPrefix(res.Desc, hidden) {
|
||||
return nil
|
||||
}
|
||||
if cmd.FlagSet != nil {
|
||||
cmd.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
if strings.HasPrefix(f.Usage, hidden) {
|
||||
return
|
||||
}
|
||||
res.Flags = append(res.Flags, flagDoc{
|
||||
Name: f.Name,
|
||||
Desc: f.Usage,
|
||||
})
|
||||
})
|
||||
}
|
||||
for _, sub := range cmd.Subcommands {
|
||||
subj := jsonDocsWalk(sub)
|
||||
if subj != nil {
|
||||
res.Subcommands = append(res.Subcommands, *subj)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
@@ -66,9 +67,14 @@ func runDriveShare(ctx context.Context, args []string) error {
|
||||
|
||||
name, path := args[0], args[1]
|
||||
|
||||
err := localClient.DriveShareSet(ctx, &drive.Share{
|
||||
absolutePath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = localClient.DriveShareSet(ctx, &drive.Share{
|
||||
Name: name,
|
||||
Path: path,
|
||||
Path: absolutePath,
|
||||
})
|
||||
if err == nil {
|
||||
fmt.Printf("Sharing %q as %q\n", path, name)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/envknob"
|
||||
@@ -136,6 +137,7 @@ func runExitNodeList(ctx context.Context, args []string) error {
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "# To view the complete list of exit nodes for a country, use `tailscale exit-node list --filter=` followed by the country name.")
|
||||
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP.")
|
||||
if hasAnyExitNodeSuggestions(peers) {
|
||||
fmt.Fprintln(w, "# To have Tailscale suggest an exit node, use `tailscale exit-node suggest`.")
|
||||
@@ -154,7 +156,7 @@ func runExitNodeSuggest(ctx context.Context, args []string) error {
|
||||
fmt.Println("No exit node suggestion is available.")
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", res.Name, res.ID)
|
||||
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", res.Name, shellquote.Join(res.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -229,7 +231,7 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
for _, ps := range peers {
|
||||
loc := cmp.Or(ps.Location, noLocation)
|
||||
|
||||
if filterBy != "" && loc.Country != filterBy {
|
||||
if filterBy != "" && !strings.EqualFold(loc.Country, filterBy) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -269,9 +271,14 @@ func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string)
|
||||
countryAnyPeer = append(countryAnyPeer, city.Peers...)
|
||||
var reducedCityPeers []*ipnstate.PeerStatus
|
||||
for i, peer := range city.Peers {
|
||||
if filterBy != "" {
|
||||
// If the peers are being filtered, we return all peers to the user.
|
||||
reducedCityPeers = append(reducedCityPeers, city.Peers...)
|
||||
break
|
||||
}
|
||||
// If the peers are not being filtered, we only return the highest priority peer and any peer that
|
||||
// is currently the active exit node.
|
||||
if i == 0 || peer.ExitNode {
|
||||
// We only return the highest priority peer and any peer that
|
||||
// is currently the active exit node.
|
||||
reducedCityPeers = append(reducedCityPeers, peer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestFilterFormatAndSortExitNodes(t *testing.T) {
|
||||
{
|
||||
Name: "Rainier",
|
||||
Peers: []*ipnstate.PeerStatus{
|
||||
ps[2],
|
||||
ps[2], ps[3],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package internal contains internal code for the ffcomplete package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
|
||||
@@ -52,9 +52,15 @@ func runNetcheck(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure that we close the portmapper after running a netcheck; this
|
||||
// will release any port mappings created.
|
||||
pm := portmapper.NewClient(logf, netMon, nil, nil, nil)
|
||||
defer pm.Close()
|
||||
|
||||
c := &netcheck.Client{
|
||||
NetMon: netMon,
|
||||
PortMapper: portmapper.NewClient(logf, netMon, nil, nil, nil),
|
||||
PortMapper: pm,
|
||||
UseDNSCache: false, // always resolve, don't cache
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
|
||||
@@ -789,7 +789,7 @@ func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
fmt.Printf(`Run the following command on another machine with a trusted tailnet lock key:
|
||||
%s lock recover-compromised-key --cosign %X
|
||||
%s lock revoke-keys --cosign %X
|
||||
`, os.Args[0], aumBytes)
|
||||
return nil
|
||||
}
|
||||
@@ -813,10 +813,10 @@ func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
|
||||
fmt.Printf(`Co-signing completed successfully.
|
||||
|
||||
To accumulate an additional signature, run the following command on another machine with a trusted tailnet lock key:
|
||||
%s lock recover-compromised-key --cosign %X
|
||||
%s lock revoke-keys --cosign %X
|
||||
|
||||
Alternatively if you are done with co-signing, complete recovery by running the following command:
|
||||
%s lock recover-compromised-key --finish %X
|
||||
%s lock revoke-keys --finish %X
|
||||
`, os.Args[0], aumBytes, os.Args[0], aumBytes)
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
@@ -89,7 +89,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -103,7 +103,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -117,7 +117,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -131,7 +131,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -157,10 +157,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/abc": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -171,7 +171,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -182,7 +182,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
@@ -236,7 +236,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -247,10 +247,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/abc": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -261,7 +261,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -272,7 +272,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
@@ -361,7 +361,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -372,10 +372,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -439,7 +439,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TCPForward: "localhost:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -466,7 +466,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:123",
|
||||
TCPForward: "localhost:123",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -560,7 +560,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -572,7 +572,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -584,10 +584,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -599,10 +599,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -614,10 +614,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/bar": {Proxy: "http://127.0.0.1:3001"},
|
||||
"/bar": {Proxy: "http://localhost:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -628,7 +628,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -636,10 +636,10 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
{ // start a tcp forwarder on 8443
|
||||
command: cmd("serve --bg --tcp=8443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "localhost:5432"}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -647,7 +647,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
{ // remove primary port http handler
|
||||
command: cmd("serve off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "localhost:5432"}},
|
||||
},
|
||||
},
|
||||
{ // remove tcp forwarder
|
||||
@@ -717,7 +717,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TCPForward: "localhost:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
@@ -738,7 +738,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -758,7 +758,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -769,8 +769,8 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{4545: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:4545": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/foo": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/bar": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/foo": {Proxy: "http://localhost:3000"},
|
||||
"/bar": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
@@ -800,7 +800,7 @@ func TestServeDevConfigMutations(t *testing.T) {
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{3000: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:3000": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
"/": {Proxy: "http://localhost:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -210,6 +210,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
}
|
||||
}
|
||||
if maskedPrefs.AutoUpdateSet.ApplySet {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
// On macsys, tailscaled will set the Sparkle auto-update setting. It
|
||||
// does not use clientupdate.
|
||||
if version.IsMacSysExt() {
|
||||
@@ -221,10 +224,6 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
||||
}
|
||||
} else {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
}
|
||||
}
|
||||
checkPrefs := curPrefs.Clone()
|
||||
|
||||
@@ -9,6 +9,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/jsontext from github.com/go-json-experiment/json+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
L github.com/google/nftables from tailscale.com/util/linuxfw
|
||||
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
|
||||
@@ -94,16 +100,17 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+
|
||||
tailscale.com/licenses from tailscale.com/client/web+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/captivedetection from tailscale.com/net/netcheck
|
||||
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet
|
||||
tailscale.com/net/netaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
💣 tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/capture
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
@@ -121,7 +128,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tstime from tailscale.com/control/controlhttp+
|
||||
tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
|
||||
|
||||
@@ -90,11 +90,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun+
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/internal/jsonwire from github.com/go-json-experiment/json/jsontext+
|
||||
github.com/go-json-experiment/json/jsontext from tailscale.com/logtail+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
|
||||
@@ -211,7 +212,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/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+
|
||||
@@ -221,6 +221,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack/gro from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/internal/network from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
@@ -287,6 +288,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/filch from tailscale.com/log/sockstatlog+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/captivedetection from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/dns/publicdns from tailscale.com/net/dns+
|
||||
@@ -303,7 +305,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/netkernelconf from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
💣 tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
W 💣 tailscale.com/net/netstat from tailscale.com/portlist
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/net/connstats+
|
||||
@@ -328,6 +330,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/proxymap from tailscale.com/tsd+
|
||||
💣 tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
LD tailscale.com/sessionrecording from tailscale.com/ssh/tailssh
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
@@ -335,7 +338,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon
|
||||
W tailscale.com/tsconst from tailscale.com/net/netmon+
|
||||
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/tstime from tailscale.com/control/controlclient+
|
||||
tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
@@ -401,6 +404,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -394,7 +394,7 @@ func run() (err error) {
|
||||
// Always clean up, even if we're going to run the server. This covers cases
|
||||
// such as when a system was rebooted without shutting down, or tailscaled
|
||||
// crashed, and would for example restore system DNS configuration.
|
||||
dns.CleanUp(logf, netMon, args.tunname)
|
||||
dns.CleanUp(logf, netMon, sys.HealthTracker(), args.tunname)
|
||||
router.CleanUp(logf, netMon, args.tunname)
|
||||
// If the cleanUp flag was passed, then exit.
|
||||
if args.cleanUp {
|
||||
|
||||
@@ -7,9 +7,13 @@ package tests
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded --clone-only-type=OnlyGetClone
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded,GenericIntStruct,GenericNoPtrsStruct,GenericCloneableStruct,StructWithContainers --clone-only-type=OnlyGetClone
|
||||
|
||||
type StructWithoutPtrs struct {
|
||||
Int int
|
||||
@@ -25,12 +29,12 @@ type Map struct {
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int `json:"-"`
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
|
||||
// Unsupported views.
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int `json:"-"`
|
||||
StructWithPtrKey map[StructWithPtrs]int `json:"-"`
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}
|
||||
|
||||
type StructWithPtrs struct {
|
||||
@@ -50,12 +54,14 @@ type StructWithSlices struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
|
||||
Slice []string
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
|
||||
// Unsupported views.
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
}
|
||||
|
||||
type OnlyGetClone struct {
|
||||
@@ -66,3 +72,93 @@ type StructWithEmbedded struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}
|
||||
|
||||
type GenericIntStruct[T constraints.Integer] struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
|
||||
// Unsupported views.
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}
|
||||
|
||||
type BasicType interface {
|
||||
~bool | constraints.Integer | constraints.Float | constraints.Complex | ~string
|
||||
}
|
||||
|
||||
type GenericNoPtrsStruct[T StructWithoutPtrs | netip.Prefix | BasicType] struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
|
||||
// Unsupported views.
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}
|
||||
|
||||
type GenericCloneableStruct[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
Value T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
|
||||
// Unsupported views.
|
||||
Pointer *T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}
|
||||
|
||||
// Container is a pre-defined container type, such as a collection, an optional
|
||||
// value or a generic wrapper.
|
||||
type Container[T any] struct {
|
||||
Item T
|
||||
}
|
||||
|
||||
func (c *Container[T]) Clone() *Container[T] {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
if cloner, ok := any(c.Item).(views.Cloner[T]); ok {
|
||||
return &Container[T]{cloner.Clone()}
|
||||
}
|
||||
if !views.ContainsPointers[T]() {
|
||||
return ptr.To(*c)
|
||||
}
|
||||
panic(fmt.Errorf("%T contains pointers, but is not cloneable", c.Item))
|
||||
}
|
||||
|
||||
// ContainerView is a pre-defined readonly view of a Container[T].
|
||||
type ContainerView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Container[T]
|
||||
}
|
||||
|
||||
func (cv ContainerView[T, V]) Item() V {
|
||||
return cv.ж.Item.View()
|
||||
}
|
||||
|
||||
func ContainerViewOf[T views.ViewCloner[T, V], V views.StructView[T]](c *Container[T]) ContainerView[T, V] {
|
||||
return ContainerView[T, V]{c}
|
||||
}
|
||||
|
||||
type GenericBasicStruct[T BasicType] struct {
|
||||
Value T
|
||||
}
|
||||
|
||||
type StructWithContainers struct {
|
||||
IntContainer Container[int]
|
||||
CloneableContainer Container[*StructWithPtrs]
|
||||
BasicGenericContainer Container[GenericBasicStruct[int]]
|
||||
ClonableGenericContainer Container[*GenericNoPtrsStruct[int]]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"maps"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of StructWithPtrs.
|
||||
@@ -71,13 +73,21 @@ func (src *Map) Clone() *Map {
|
||||
if dst.StructPtrWithPtr != nil {
|
||||
dst.StructPtrWithPtr = map[string]*StructWithPtrs{}
|
||||
for k, v := range src.StructPtrWithPtr {
|
||||
dst.StructPtrWithPtr[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.StructPtrWithPtr[k] = nil
|
||||
} else {
|
||||
dst.StructPtrWithPtr[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
if dst.StructPtrWithoutPtr != nil {
|
||||
dst.StructPtrWithoutPtr = map[string]*StructWithoutPtrs{}
|
||||
for k, v := range src.StructPtrWithoutPtr {
|
||||
dst.StructPtrWithoutPtr[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.StructPtrWithoutPtr[k] = nil
|
||||
} else {
|
||||
dst.StructPtrWithoutPtr[k] = ptr.To(*v)
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.StructWithoutPtr = maps.Clone(src.StructWithoutPtr)
|
||||
@@ -94,6 +104,12 @@ func (src *Map) Clone() *Map {
|
||||
}
|
||||
}
|
||||
dst.StructWithoutPtrKey = maps.Clone(src.StructWithoutPtrKey)
|
||||
if dst.StructWithPtr != nil {
|
||||
dst.StructWithPtr = map[string]StructWithPtrs{}
|
||||
for k, v := range src.StructWithPtr {
|
||||
dst.StructWithPtr[k] = *(v.Clone())
|
||||
}
|
||||
}
|
||||
if dst.SliceIntPtr != nil {
|
||||
dst.SliceIntPtr = map[string][]*int{}
|
||||
for k := range src.SliceIntPtr {
|
||||
@@ -102,12 +118,6 @@ func (src *Map) Clone() *Map {
|
||||
}
|
||||
dst.PointerKey = maps.Clone(src.PointerKey)
|
||||
dst.StructWithPtrKey = maps.Clone(src.StructWithPtrKey)
|
||||
if dst.StructWithPtr != nil {
|
||||
dst.StructWithPtr = map[string]StructWithPtrs{}
|
||||
for k, v := range src.StructWithPtr {
|
||||
dst.StructWithPtr[k] = *(v.Clone())
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -121,10 +131,10 @@ var _MapCloneNeedsRegeneration = Map(struct {
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of StructWithSlices.
|
||||
@@ -139,15 +149,26 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
|
||||
if src.ValuePointers != nil {
|
||||
dst.ValuePointers = make([]*StructWithoutPtrs, len(src.ValuePointers))
|
||||
for i := range dst.ValuePointers {
|
||||
dst.ValuePointers[i] = src.ValuePointers[i].Clone()
|
||||
if src.ValuePointers[i] == nil {
|
||||
dst.ValuePointers[i] = nil
|
||||
} else {
|
||||
dst.ValuePointers[i] = ptr.To(*src.ValuePointers[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if src.StructPointers != nil {
|
||||
dst.StructPointers = make([]*StructWithPtrs, len(src.StructPointers))
|
||||
for i := range dst.StructPointers {
|
||||
dst.StructPointers[i] = src.StructPointers[i].Clone()
|
||||
if src.StructPointers[i] == nil {
|
||||
dst.StructPointers[i] = nil
|
||||
} else {
|
||||
dst.StructPointers[i] = src.StructPointers[i].Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.Slice = append(src.Slice[:0:0], src.Slice...)
|
||||
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)
|
||||
dst.Data = append(src.Data[:0:0], src.Data...)
|
||||
if src.Structs != nil {
|
||||
dst.Structs = make([]StructWithPtrs, len(src.Structs))
|
||||
for i := range dst.Structs {
|
||||
@@ -164,9 +185,6 @@ func (src *StructWithSlices) Clone() *StructWithSlices {
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.Slice = append(src.Slice[:0:0], src.Slice...)
|
||||
dst.Prefixes = append(src.Prefixes[:0:0], src.Prefixes...)
|
||||
dst.Data = append(src.Data[:0:0], src.Data...)
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -175,11 +193,11 @@ var _StructWithSlicesCloneNeedsRegeneration = StructWithSlices(struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
Slice []string
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of OnlyGetClone.
|
||||
@@ -216,3 +234,206 @@ var _StructWithEmbeddedCloneNeedsRegeneration = StructWithEmbedded(struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of GenericIntStruct.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *GenericIntStruct[T]) Clone() *GenericIntStruct[T] {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(GenericIntStruct[T])
|
||||
*dst = *src
|
||||
if dst.Pointer != nil {
|
||||
dst.Pointer = ptr.To(*src.Pointer)
|
||||
}
|
||||
dst.Slice = append(src.Slice[:0:0], src.Slice...)
|
||||
dst.Map = maps.Clone(src.Map)
|
||||
if src.PtrSlice != nil {
|
||||
dst.PtrSlice = make([]*T, len(src.PtrSlice))
|
||||
for i := range dst.PtrSlice {
|
||||
if src.PtrSlice[i] == nil {
|
||||
dst.PtrSlice[i] = nil
|
||||
} else {
|
||||
dst.PtrSlice[i] = ptr.To(*src.PtrSlice[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.PtrKeyMap = maps.Clone(src.PtrKeyMap)
|
||||
if dst.PtrValueMap != nil {
|
||||
dst.PtrValueMap = map[string]*T{}
|
||||
for k, v := range src.PtrValueMap {
|
||||
if v == nil {
|
||||
dst.PtrValueMap[k] = nil
|
||||
} else {
|
||||
dst.PtrValueMap[k] = ptr.To(*v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if dst.SliceMap != nil {
|
||||
dst.SliceMap = map[string][]T{}
|
||||
for k := range src.SliceMap {
|
||||
dst.SliceMap[k] = append([]T{}, src.SliceMap[k]...)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericIntStructCloneNeedsRegeneration[T constraints.Integer](GenericIntStruct[T]) {
|
||||
_GenericIntStructCloneNeedsRegeneration(struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of GenericNoPtrsStruct.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *GenericNoPtrsStruct[T]) Clone() *GenericNoPtrsStruct[T] {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(GenericNoPtrsStruct[T])
|
||||
*dst = *src
|
||||
if dst.Pointer != nil {
|
||||
dst.Pointer = ptr.To(*src.Pointer)
|
||||
}
|
||||
dst.Slice = append(src.Slice[:0:0], src.Slice...)
|
||||
dst.Map = maps.Clone(src.Map)
|
||||
if src.PtrSlice != nil {
|
||||
dst.PtrSlice = make([]*T, len(src.PtrSlice))
|
||||
for i := range dst.PtrSlice {
|
||||
if src.PtrSlice[i] == nil {
|
||||
dst.PtrSlice[i] = nil
|
||||
} else {
|
||||
dst.PtrSlice[i] = ptr.To(*src.PtrSlice[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.PtrKeyMap = maps.Clone(src.PtrKeyMap)
|
||||
if dst.PtrValueMap != nil {
|
||||
dst.PtrValueMap = map[string]*T{}
|
||||
for k, v := range src.PtrValueMap {
|
||||
if v == nil {
|
||||
dst.PtrValueMap[k] = nil
|
||||
} else {
|
||||
dst.PtrValueMap[k] = ptr.To(*v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if dst.SliceMap != nil {
|
||||
dst.SliceMap = map[string][]T{}
|
||||
for k := range src.SliceMap {
|
||||
dst.SliceMap[k] = append([]T{}, src.SliceMap[k]...)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericNoPtrsStructCloneNeedsRegeneration[T StructWithoutPtrs | netip.Prefix | BasicType](GenericNoPtrsStruct[T]) {
|
||||
_GenericNoPtrsStructCloneNeedsRegeneration(struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of GenericCloneableStruct.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *GenericCloneableStruct[T, V]) Clone() *GenericCloneableStruct[T, V] {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(GenericCloneableStruct[T, V])
|
||||
*dst = *src
|
||||
dst.Value = src.Value.Clone()
|
||||
if src.Slice != nil {
|
||||
dst.Slice = make([]T, len(src.Slice))
|
||||
for i := range dst.Slice {
|
||||
dst.Slice[i] = src.Slice[i].Clone()
|
||||
}
|
||||
}
|
||||
if dst.Map != nil {
|
||||
dst.Map = map[string]T{}
|
||||
for k, v := range src.Map {
|
||||
dst.Map[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
if dst.Pointer != nil {
|
||||
dst.Pointer = ptr.To((*src.Pointer).Clone())
|
||||
}
|
||||
if src.PtrSlice != nil {
|
||||
dst.PtrSlice = make([]*T, len(src.PtrSlice))
|
||||
for i := range dst.PtrSlice {
|
||||
if src.PtrSlice[i] == nil {
|
||||
dst.PtrSlice[i] = nil
|
||||
} else {
|
||||
dst.PtrSlice[i] = ptr.To((*src.PtrSlice[i]).Clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.PtrKeyMap = maps.Clone(src.PtrKeyMap)
|
||||
if dst.PtrValueMap != nil {
|
||||
dst.PtrValueMap = map[string]*T{}
|
||||
for k, v := range src.PtrValueMap {
|
||||
if v == nil {
|
||||
dst.PtrValueMap[k] = nil
|
||||
} else {
|
||||
dst.PtrValueMap[k] = ptr.To((*v).Clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
if dst.SliceMap != nil {
|
||||
dst.SliceMap = map[string][]T{}
|
||||
for k := range src.SliceMap {
|
||||
dst.SliceMap[k] = append([]T{}, src.SliceMap[k]...)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericCloneableStructCloneNeedsRegeneration[T views.ViewCloner[T, V], V views.StructView[T]](GenericCloneableStruct[T, V]) {
|
||||
_GenericCloneableStructCloneNeedsRegeneration(struct {
|
||||
Value T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
Pointer *T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of StructWithContainers.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *StructWithContainers) Clone() *StructWithContainers {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(StructWithContainers)
|
||||
*dst = *src
|
||||
dst.CloneableContainer = *src.CloneableContainer.Clone()
|
||||
dst.ClonableGenericContainer = *src.ClonableGenericContainer.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithContainersCloneNeedsRegeneration = StructWithContainers(struct {
|
||||
IntContainer Container[int]
|
||||
CloneableContainer Container[*StructWithPtrs]
|
||||
BasicGenericContainer Container[GenericBasicStruct[int]]
|
||||
ClonableGenericContainer Container[*GenericNoPtrsStruct[int]]
|
||||
}{})
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded,GenericIntStruct,GenericNoPtrsStruct,GenericCloneableStruct,StructWithContainers
|
||||
|
||||
// View returns a readonly view of StructWithPtrs.
|
||||
func (p *StructWithPtrs) View() StructWithPtrsView {
|
||||
@@ -221,15 +222,15 @@ func (v MapView) SlicesWithoutPtrs() views.MapFn[string, []*StructWithoutPtrs, v
|
||||
func (v MapView) StructWithoutPtrKey() views.Map[StructWithoutPtrs, int] {
|
||||
return views.MapOf(v.ж.StructWithoutPtrKey)
|
||||
}
|
||||
func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") }
|
||||
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
|
||||
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
|
||||
|
||||
func (v MapView) StructWithPtr() views.MapFn[string, StructWithPtrs, StructWithPtrsView] {
|
||||
return views.MapFnOf(v.ж.StructWithPtr, func(t StructWithPtrs) StructWithPtrsView {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
func (v MapView) SliceIntPtr() map[string][]*int { panic("unsupported") }
|
||||
func (v MapView) PointerKey() map[*string]int { panic("unsupported") }
|
||||
func (v MapView) StructWithPtrKey() map[StructWithPtrs]int { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _MapViewNeedsRegeneration = Map(struct {
|
||||
@@ -241,10 +242,10 @@ var _MapViewNeedsRegeneration = Map(struct {
|
||||
SlicesWithPtrs map[string][]*StructWithPtrs
|
||||
SlicesWithoutPtrs map[string][]*StructWithoutPtrs
|
||||
StructWithoutPtrKey map[StructWithoutPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
SliceIntPtr map[string][]*int
|
||||
PointerKey map[*string]int
|
||||
StructWithPtrKey map[StructWithPtrs]int
|
||||
StructWithPtr map[string]StructWithPtrs
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithSlices.
|
||||
@@ -301,24 +302,24 @@ func (v StructWithSlicesView) ValuePointers() views.SliceView[*StructWithoutPtrs
|
||||
func (v StructWithSlicesView) StructPointers() views.SliceView[*StructWithPtrs, StructWithPtrsView] {
|
||||
return views.SliceOfViews[*StructWithPtrs, StructWithPtrsView](v.ж.StructPointers)
|
||||
}
|
||||
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Slice() views.Slice[string] { return views.SliceOf(v.ж.Slice) }
|
||||
func (v StructWithSlicesView) Prefixes() views.Slice[netip.Prefix] {
|
||||
return views.SliceOf(v.ж.Prefixes)
|
||||
}
|
||||
func (v StructWithSlicesView) Data() views.ByteSlice[[]byte] { return views.ByteSliceOf(v.ж.Data) }
|
||||
func (v StructWithSlicesView) Structs() StructWithPtrs { panic("unsupported") }
|
||||
func (v StructWithSlicesView) Ints() *int { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
|
||||
Values []StructWithoutPtrs
|
||||
ValuePointers []*StructWithoutPtrs
|
||||
StructPointers []*StructWithPtrs
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
Slice []string
|
||||
Prefixes []netip.Prefix
|
||||
Data []byte
|
||||
Structs []StructWithPtrs
|
||||
Ints []*int
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of StructWithEmbedded.
|
||||
@@ -376,3 +377,294 @@ var _StructWithEmbeddedViewNeedsRegeneration = StructWithEmbedded(struct {
|
||||
A *StructWithPtrs
|
||||
StructWithSlices
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of GenericIntStruct.
|
||||
func (p *GenericIntStruct[T]) View() GenericIntStructView[T] {
|
||||
return GenericIntStructView[T]{ж: p}
|
||||
}
|
||||
|
||||
// GenericIntStructView[T] provides a read-only view over GenericIntStruct[T].
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type GenericIntStructView[T constraints.Integer] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *GenericIntStruct[T]
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v GenericIntStructView[T]) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v GenericIntStructView[T]) AsStruct() *GenericIntStruct[T] {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v GenericIntStructView[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *GenericIntStructView[T]) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x GenericIntStruct[T]
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v GenericIntStructView[T]) Value() T { return v.ж.Value }
|
||||
func (v GenericIntStructView[T]) Pointer() *T {
|
||||
if v.ж.Pointer == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Pointer
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v GenericIntStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) }
|
||||
|
||||
func (v GenericIntStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) }
|
||||
func (v GenericIntStructView[T]) PtrSlice() *T { panic("unsupported") }
|
||||
func (v GenericIntStructView[T]) PtrKeyMap() map[*T]string { panic("unsupported") }
|
||||
func (v GenericIntStructView[T]) PtrValueMap() map[string]*T { panic("unsupported") }
|
||||
func (v GenericIntStructView[T]) SliceMap() map[string][]T { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericIntStructViewNeedsRegeneration[T constraints.Integer](GenericIntStruct[T]) {
|
||||
_GenericIntStructViewNeedsRegeneration(struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// View returns a readonly view of GenericNoPtrsStruct.
|
||||
func (p *GenericNoPtrsStruct[T]) View() GenericNoPtrsStructView[T] {
|
||||
return GenericNoPtrsStructView[T]{ж: p}
|
||||
}
|
||||
|
||||
// GenericNoPtrsStructView[T] provides a read-only view over GenericNoPtrsStruct[T].
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type GenericNoPtrsStructView[T StructWithoutPtrs | netip.Prefix | BasicType] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *GenericNoPtrsStruct[T]
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v GenericNoPtrsStructView[T]) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v GenericNoPtrsStructView[T]) AsStruct() *GenericNoPtrsStruct[T] {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v GenericNoPtrsStructView[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *GenericNoPtrsStructView[T]) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x GenericNoPtrsStruct[T]
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v GenericNoPtrsStructView[T]) Value() T { return v.ж.Value }
|
||||
func (v GenericNoPtrsStructView[T]) Pointer() *T {
|
||||
if v.ж.Pointer == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Pointer
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v GenericNoPtrsStructView[T]) Slice() views.Slice[T] { return views.SliceOf(v.ж.Slice) }
|
||||
|
||||
func (v GenericNoPtrsStructView[T]) Map() views.Map[string, T] { return views.MapOf(v.ж.Map) }
|
||||
func (v GenericNoPtrsStructView[T]) PtrSlice() *T { panic("unsupported") }
|
||||
func (v GenericNoPtrsStructView[T]) PtrKeyMap() map[*T]string { panic("unsupported") }
|
||||
func (v GenericNoPtrsStructView[T]) PtrValueMap() map[string]*T { panic("unsupported") }
|
||||
func (v GenericNoPtrsStructView[T]) SliceMap() map[string][]T { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericNoPtrsStructViewNeedsRegeneration[T StructWithoutPtrs | netip.Prefix | BasicType](GenericNoPtrsStruct[T]) {
|
||||
_GenericNoPtrsStructViewNeedsRegeneration(struct {
|
||||
Value T
|
||||
Pointer *T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// View returns a readonly view of GenericCloneableStruct.
|
||||
func (p *GenericCloneableStruct[T, V]) View() GenericCloneableStructView[T, V] {
|
||||
return GenericCloneableStructView[T, V]{ж: p}
|
||||
}
|
||||
|
||||
// GenericCloneableStructView[T, V] provides a read-only view over GenericCloneableStruct[T, V].
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type GenericCloneableStructView[T views.ViewCloner[T, V], V views.StructView[T]] struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *GenericCloneableStruct[T, V]
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v GenericCloneableStructView[T, V]) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v GenericCloneableStructView[T, V]) AsStruct() *GenericCloneableStruct[T, V] {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v GenericCloneableStructView[T, V]) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *GenericCloneableStructView[T, V]) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x GenericCloneableStruct[T, V]
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v GenericCloneableStructView[T, V]) Value() V { return v.ж.Value.View() }
|
||||
func (v GenericCloneableStructView[T, V]) Slice() views.SliceView[T, V] {
|
||||
return views.SliceOfViews[T, V](v.ж.Slice)
|
||||
}
|
||||
|
||||
func (v GenericCloneableStructView[T, V]) Map() views.MapFn[string, T, V] {
|
||||
return views.MapFnOf(v.ж.Map, func(t T) V {
|
||||
return t.View()
|
||||
})
|
||||
}
|
||||
func (v GenericCloneableStructView[T, V]) Pointer() map[string]T { panic("unsupported") }
|
||||
func (v GenericCloneableStructView[T, V]) PtrSlice() *T { panic("unsupported") }
|
||||
func (v GenericCloneableStructView[T, V]) PtrKeyMap() map[*T]string { panic("unsupported") }
|
||||
func (v GenericCloneableStructView[T, V]) PtrValueMap() map[string]*T { panic("unsupported") }
|
||||
func (v GenericCloneableStructView[T, V]) SliceMap() map[string][]T { panic("unsupported") }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
func _GenericCloneableStructViewNeedsRegeneration[T views.ViewCloner[T, V], V views.StructView[T]](GenericCloneableStruct[T, V]) {
|
||||
_GenericCloneableStructViewNeedsRegeneration(struct {
|
||||
Value T
|
||||
Slice []T
|
||||
Map map[string]T
|
||||
Pointer *T
|
||||
PtrSlice []*T
|
||||
PtrKeyMap map[*T]string `json:"-"`
|
||||
PtrValueMap map[string]*T
|
||||
SliceMap map[string][]T
|
||||
}{})
|
||||
}
|
||||
|
||||
// View returns a readonly view of StructWithContainers.
|
||||
func (p *StructWithContainers) View() StructWithContainersView {
|
||||
return StructWithContainersView{ж: p}
|
||||
}
|
||||
|
||||
// StructWithContainersView provides a read-only view over StructWithContainers.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type StructWithContainersView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *StructWithContainers
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v StructWithContainersView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v StructWithContainersView) AsStruct() *StructWithContainers {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v StructWithContainersView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *StructWithContainersView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x StructWithContainers
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v StructWithContainersView) IntContainer() Container[int] { return v.ж.IntContainer }
|
||||
func (v StructWithContainersView) CloneableContainer() ContainerView[*StructWithPtrs, StructWithPtrsView] {
|
||||
return ContainerViewOf(&v.ж.CloneableContainer)
|
||||
}
|
||||
func (v StructWithContainersView) BasicGenericContainer() Container[GenericBasicStruct[int]] {
|
||||
return v.ж.BasicGenericContainer
|
||||
}
|
||||
func (v StructWithContainersView) ClonableGenericContainer() ContainerView[*GenericNoPtrsStruct[int], GenericNoPtrsStructView[int]] {
|
||||
return ContainerViewOf(&v.ж.ClonableGenericContainer)
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _StructWithContainersViewNeedsRegeneration = StructWithContainers(struct {
|
||||
IntContainer Container[int]
|
||||
CloneableContainer Container[*StructWithPtrs]
|
||||
BasicGenericContainer Container[GenericBasicStruct[int]]
|
||||
ClonableGenericContainer Container[*GenericNoPtrsStruct[int]]
|
||||
}{})
|
||||
|
||||
@@ -13,50 +13,52 @@ import (
|
||||
"html/template"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/codegen"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
const viewTemplateStr = `{{define "common"}}
|
||||
// View returns a readonly view of {{.StructName}}.
|
||||
func (p *{{.StructName}}) View() {{.ViewName}} {
|
||||
return {{.ViewName}}{ж: p}
|
||||
func (p *{{.StructName}}{{.TypeParamNames}}) View() {{.ViewName}}{{.TypeParamNames}} {
|
||||
return {{.ViewName}}{{.TypeParamNames}}{ж: p}
|
||||
}
|
||||
|
||||
// {{.ViewName}} provides a read-only view over {{.StructName}}.
|
||||
// {{.ViewName}}{{.TypeParamNames}} provides a read-only view over {{.StructName}}{{.TypeParamNames}}.
|
||||
//
|
||||
// Its methods should only be called if ` + "`Valid()`" + ` returns true.
|
||||
type {{.ViewName}} struct {
|
||||
type {{.ViewName}}{{.TypeParams}} struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *{{.StructName}}
|
||||
ж *{{.StructName}}{{.TypeParamNames}}
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v {{.ViewName}}) Valid() bool { return v.ж != nil }
|
||||
func (v {{.ViewName}}{{.TypeParamNames}}) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v {{.ViewName}}) AsStruct() *{{.StructName}}{
|
||||
func (v {{.ViewName}}{{.TypeParamNames}}) AsStruct() *{{.StructName}}{{.TypeParamNames}}{
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v {{.ViewName}}) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
func (v {{.ViewName}}{{.TypeParamNames}}) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
||||
func (v *{{.ViewName}}{{.TypeParamNames}}) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x {{.StructName}}
|
||||
var x {{.StructName}}{{.TypeParamNames}}
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,17 +67,19 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
||||
}
|
||||
|
||||
{{end}}
|
||||
{{define "valueField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
|
||||
{{define "valueField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} { return v.ж.{{.FieldName}} }
|
||||
{{end}}
|
||||
{{define "byteSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.ByteSlice[{{.FieldType}}] { return views.ByteSliceOf(v.ж.{{.FieldName}}) }
|
||||
{{define "byteSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.ByteSlice[{{.FieldType}}] { return views.ByteSliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "sliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
|
||||
{{define "sliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Slice[{{.FieldType}}] { return views.SliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "viewSliceField"}}func (v {{.ViewName}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
|
||||
{{define "viewSliceField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.SliceView[{{.FieldType}},{{.FieldViewName}}] { return views.SliceOfViews[{{.FieldType}},{{.FieldViewName}}](v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "viewField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}}View { return v.ж.{{.FieldName}}.View() }
|
||||
{{define "viewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return v.ж.{{.FieldName}}.View() }
|
||||
{{end}}
|
||||
{{define "valuePointerField"}}func (v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {
|
||||
{{define "makeViewField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldViewName}} { return {{.MakeViewFnName}}(&v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "valuePointerField"}}func (v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} {
|
||||
if v.ж.{{.FieldName}} == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -85,21 +89,21 @@ func (v *{{.ViewName}}) UnmarshalJSON(b []byte) error {
|
||||
|
||||
{{end}}
|
||||
{{define "mapField"}}
|
||||
func(v {{.ViewName}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
|
||||
func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.Map[{{.MapKeyType}},{{.MapValueType}}] { return views.MapOf(v.ж.{{.FieldName}})}
|
||||
{{end}}
|
||||
{{define "mapFnField"}}
|
||||
func(v {{.ViewName}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
|
||||
func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapFn[{{.MapKeyType}},{{.MapValueType}},{{.MapValueView}}] { return views.MapFnOf(v.ж.{{.FieldName}}, func (t {{.MapValueType}}) {{.MapValueView}} {
|
||||
return {{.MapFn}}
|
||||
})}
|
||||
{{end}}
|
||||
{{define "mapSliceField"}}
|
||||
func(v {{.ViewName}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) }
|
||||
func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() views.MapSlice[{{.MapKeyType}},{{.MapValueType}}] { return views.MapSliceOf(v.ж.{{.FieldName}}) }
|
||||
{{end}}
|
||||
{{define "unsupportedField"}}func(v {{.ViewName}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
|
||||
{{define "unsupportedField"}}func(v {{.ViewName}}{{.TypeParamNames}}) {{.FieldName}}() {{.FieldType}} {panic("unsupported")}
|
||||
{{end}}
|
||||
{{define "stringFunc"}}func(v {{.ViewName}}) String() string { return v.ж.String() }
|
||||
{{define "stringFunc"}}func(v {{.ViewName}}{{.TypeParamNames}}) String() string { return v.ж.String() }
|
||||
{{end}}
|
||||
{{define "equalFunc"}}func(v {{.ViewName}}) Equal(v2 {{.ViewName}}) bool { return v.ж.Equal(v2.ж) }
|
||||
{{define "equalFunc"}}func(v {{.ViewName}}{{.TypeParamNames}}) Equal(v2 {{.ViewName}}{{.TypeParamNames}}) bool { return v.ж.Equal(v2.ж) }
|
||||
{{end}}
|
||||
`
|
||||
|
||||
@@ -131,8 +135,11 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
it.Import("errors")
|
||||
|
||||
args := struct {
|
||||
StructName string
|
||||
ViewName string
|
||||
StructName string
|
||||
ViewName string
|
||||
TypeParams string // e.g. [T constraints.Integer]
|
||||
TypeParamNames string // e.g. [T]
|
||||
|
||||
FieldName string
|
||||
FieldType string
|
||||
FieldViewName string
|
||||
@@ -141,11 +148,17 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
MapValueType string
|
||||
MapValueView string
|
||||
MapFn string
|
||||
|
||||
// MakeViewFnName is the name of the function that accepts a value and returns a readonly view of it.
|
||||
MakeViewFnName string
|
||||
}{
|
||||
StructName: typ.Obj().Name(),
|
||||
ViewName: typ.Obj().Name() + "View",
|
||||
ViewName: typ.Origin().Obj().Name() + "View",
|
||||
}
|
||||
|
||||
typeParams := typ.Origin().TypeParams()
|
||||
args.TypeParams, args.TypeParamNames = codegen.FormatTypeParams(typeParams, it)
|
||||
|
||||
writeTemplate := func(name string) {
|
||||
if err := viewTemplate.ExecuteTemplate(buf, name, args); err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -182,19 +195,35 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
it.Import("tailscale.com/types/views")
|
||||
shallow, deep, base := requiresCloning(elem)
|
||||
if deep {
|
||||
if _, isPtr := elem.(*types.Pointer); isPtr {
|
||||
args.FieldViewName = it.QualifiedName(base) + "View"
|
||||
writeTemplate("viewSliceField")
|
||||
} else {
|
||||
writeTemplate("unsupportedField")
|
||||
switch elem.Underlying().(type) {
|
||||
case *types.Pointer:
|
||||
if _, isIface := base.Underlying().(*types.Interface); !isIface {
|
||||
args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View")
|
||||
writeTemplate("viewSliceField")
|
||||
} else {
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
continue
|
||||
case *types.Interface:
|
||||
if viewType := viewTypeForValueType(elem); viewType != nil {
|
||||
args.FieldViewName = it.QualifiedName(viewType)
|
||||
writeTemplate("viewSliceField")
|
||||
continue
|
||||
}
|
||||
}
|
||||
writeTemplate("unsupportedField")
|
||||
continue
|
||||
} else if shallow {
|
||||
if _, isBasic := base.(*types.Basic); isBasic {
|
||||
switch base.Underlying().(type) {
|
||||
case *types.Basic, *types.Interface:
|
||||
writeTemplate("unsupportedField")
|
||||
} else {
|
||||
args.FieldViewName = it.QualifiedName(base) + "View"
|
||||
writeTemplate("viewSliceField")
|
||||
default:
|
||||
if _, isIface := base.Underlying().(*types.Interface); !isIface {
|
||||
args.FieldViewName = appendNameSuffix(it.QualifiedName(base), "View")
|
||||
writeTemplate("viewSliceField")
|
||||
} else {
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -205,7 +234,18 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
strucT := underlying
|
||||
args.FieldType = it.QualifiedName(fieldType)
|
||||
if codegen.ContainsPointers(strucT) {
|
||||
writeTemplate("viewField")
|
||||
if viewType := viewTypeForValueType(fieldType); viewType != nil {
|
||||
args.FieldViewName = it.QualifiedName(viewType)
|
||||
writeTemplate("viewField")
|
||||
continue
|
||||
}
|
||||
if viewType, makeViewFn := viewTypeForContainerType(fieldType); viewType != nil {
|
||||
args.FieldViewName = it.QualifiedName(viewType)
|
||||
args.MakeViewFnName = it.PackagePrefix(makeViewFn.Pkg()) + makeViewFn.Name()
|
||||
writeTemplate("makeViewField")
|
||||
continue
|
||||
}
|
||||
writeTemplate("unsupportedField")
|
||||
continue
|
||||
}
|
||||
writeTemplate("valueField")
|
||||
@@ -229,7 +269,7 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
args.MapFn = "t.View()"
|
||||
template = "mapFnField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
args.MapValueView = args.MapValueType + "View"
|
||||
args.MapValueView = appendNameSuffix(args.MapValueType, "View")
|
||||
} else {
|
||||
template = "mapField"
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
@@ -249,15 +289,20 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
case *types.Pointer:
|
||||
ptr := x
|
||||
pElem := ptr.Elem()
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
ptrType := it.QualifiedName(ptr)
|
||||
viewType := it.QualifiedName(pElem) + "View"
|
||||
args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
|
||||
args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
|
||||
args.MapValueType = "[]" + ptrType
|
||||
template = "mapFnField"
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
if _, isIface := pElem.Underlying().(*types.Interface); !isIface {
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
ptrType := it.QualifiedName(ptr)
|
||||
viewType := appendNameSuffix(it.QualifiedName(pElem), "View")
|
||||
args.MapFn = fmt.Sprintf("views.SliceOfViews[%v,%v](t)", ptrType, viewType)
|
||||
args.MapValueView = fmt.Sprintf("views.SliceView[%v,%v]", ptrType, viewType)
|
||||
args.MapValueType = "[]" + ptrType
|
||||
template = "mapFnField"
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
} else {
|
||||
template = "unsupportedField"
|
||||
}
|
||||
default:
|
||||
@@ -266,13 +311,29 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
case *types.Pointer:
|
||||
ptr := u
|
||||
pElem := ptr.Elem()
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
args.MapValueType = it.QualifiedName(ptr)
|
||||
args.MapValueView = it.QualifiedName(pElem) + "View"
|
||||
if _, isIface := pElem.Underlying().(*types.Interface); !isIface {
|
||||
switch pElem.(type) {
|
||||
case *types.Struct, *types.Named:
|
||||
args.MapValueType = it.QualifiedName(ptr)
|
||||
args.MapValueView = appendNameSuffix(it.QualifiedName(pElem), "View")
|
||||
args.MapFn = "t.View()"
|
||||
template = "mapFnField"
|
||||
default:
|
||||
template = "unsupportedField"
|
||||
}
|
||||
} else {
|
||||
template = "unsupportedField"
|
||||
}
|
||||
case *types.Interface, *types.TypeParam:
|
||||
if viewType := viewTypeForValueType(u); viewType != nil {
|
||||
args.MapValueType = it.QualifiedName(u)
|
||||
args.MapValueView = it.QualifiedName(viewType)
|
||||
args.MapFn = "t.View()"
|
||||
template = "mapFnField"
|
||||
default:
|
||||
} else if !codegen.ContainsPointers(u) {
|
||||
args.MapValueType = it.QualifiedName(mElem)
|
||||
template = "mapField"
|
||||
} else {
|
||||
template = "unsupportedField"
|
||||
}
|
||||
default:
|
||||
@@ -283,14 +344,28 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
case *types.Pointer:
|
||||
ptr := underlying
|
||||
_, deep, base := requiresCloning(ptr)
|
||||
|
||||
if deep {
|
||||
args.FieldType = it.QualifiedName(base)
|
||||
writeTemplate("viewField")
|
||||
if _, isIface := base.Underlying().(*types.Interface); !isIface {
|
||||
args.FieldType = it.QualifiedName(base)
|
||||
args.FieldViewName = appendNameSuffix(args.FieldType, "View")
|
||||
writeTemplate("viewField")
|
||||
} else {
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
} else {
|
||||
args.FieldType = it.QualifiedName(ptr)
|
||||
writeTemplate("valuePointerField")
|
||||
}
|
||||
continue
|
||||
case *types.Interface:
|
||||
// If fieldType is an interface with a "View() {ViewType}" method, it can be used to clone the field.
|
||||
// This includes scenarios where fieldType is a constrained type parameter.
|
||||
if viewType := viewTypeForValueType(underlying); viewType != nil {
|
||||
args.FieldViewName = it.QualifiedName(viewType)
|
||||
writeTemplate("viewField")
|
||||
continue
|
||||
}
|
||||
}
|
||||
writeTemplate("unsupportedField")
|
||||
}
|
||||
@@ -318,7 +393,132 @@ func genView(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named, thi
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
buf.Write(codegen.AssertStructUnchanged(t, args.StructName, "View", it))
|
||||
buf.Write(codegen.AssertStructUnchanged(t, args.StructName, typeParams, "View", it))
|
||||
}
|
||||
|
||||
func appendNameSuffix(name, suffix string) string {
|
||||
if idx := strings.IndexRune(name, '['); idx != -1 {
|
||||
// Insert suffix after the type name, but before type parameters.
|
||||
return name[:idx] + suffix + name[idx:]
|
||||
}
|
||||
return name + suffix
|
||||
}
|
||||
|
||||
func viewTypeForValueType(typ types.Type) types.Type {
|
||||
if ptr, ok := typ.(*types.Pointer); ok {
|
||||
return viewTypeForValueType(ptr.Elem())
|
||||
}
|
||||
viewMethod := codegen.LookupMethod(typ, "View")
|
||||
if viewMethod == nil {
|
||||
return nil
|
||||
}
|
||||
sig, ok := viewMethod.Type().(*types.Signature)
|
||||
if !ok || sig.Results().Len() != 1 {
|
||||
return nil
|
||||
}
|
||||
return sig.Results().At(0).Type()
|
||||
}
|
||||
|
||||
func viewTypeForContainerType(typ types.Type) (*types.Named, *types.Func) {
|
||||
// The container type should be an instantiated generic type,
|
||||
// with its first type parameter specifying the element type.
|
||||
containerType, ok := typ.(*types.Named)
|
||||
if !ok || containerType.TypeArgs().Len() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Look up the view type for the container type.
|
||||
// It must include an additional type parameter specifying the element's view type.
|
||||
// For example, Container[T] => ContainerView[T, V].
|
||||
containerViewTypeName := containerType.Obj().Name() + "View"
|
||||
containerViewTypeObj, ok := containerType.Obj().Pkg().Scope().Lookup(containerViewTypeName).(*types.TypeName)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
containerViewGenericType, ok := containerViewTypeObj.Type().(*types.Named)
|
||||
if !ok || containerViewGenericType.TypeParams().Len() != containerType.TypeArgs().Len()+1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create a list of type arguments for instantiating the container view type.
|
||||
// Include all type arguments specified for the container type...
|
||||
containerViewTypeArgs := make([]types.Type, containerViewGenericType.TypeParams().Len())
|
||||
for i := range containerType.TypeArgs().Len() {
|
||||
containerViewTypeArgs[i] = containerType.TypeArgs().At(i)
|
||||
}
|
||||
// ...and add the element view type.
|
||||
// For that, we need to first determine the named elem type...
|
||||
elemType, ok := baseType(containerType.TypeArgs().At(0)).(*types.Named)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
// ...then infer the view type from it.
|
||||
var elemViewType *types.Named
|
||||
elemTypeName := elemType.Obj().Name()
|
||||
elemViewTypeBaseName := elemType.Obj().Name() + "View"
|
||||
if elemViewTypeName, ok := elemType.Obj().Pkg().Scope().Lookup(elemViewTypeBaseName).(*types.TypeName); ok {
|
||||
// The elem's view type is already defined in the same package as the elem type.
|
||||
elemViewType = elemViewTypeName.Type().(*types.Named)
|
||||
} else if slices.Contains(typeNames, elemTypeName) {
|
||||
// The elem's view type has not been generated yet, but we can define
|
||||
// and use a blank type with the expected view type name.
|
||||
elemViewTypeName = types.NewTypeName(0, elemType.Obj().Pkg(), elemViewTypeBaseName, nil)
|
||||
elemViewType = types.NewNamed(elemViewTypeName, types.NewStruct(nil, nil), nil)
|
||||
if elemTypeParams := elemType.TypeParams(); elemTypeParams != nil {
|
||||
elemViewType.SetTypeParams(collectTypeParams(elemTypeParams))
|
||||
}
|
||||
} else {
|
||||
// The elem view type does not exist and won't be generated.
|
||||
return nil, nil
|
||||
}
|
||||
// If elemType is an instantiated generic type, instantiate the elemViewType as well.
|
||||
if elemTypeArgs := elemType.TypeArgs(); elemTypeArgs != nil {
|
||||
elemViewType = must.Get(types.Instantiate(nil, elemViewType, collectTypes(elemTypeArgs), false)).(*types.Named)
|
||||
}
|
||||
// And finally set the elemViewType as the last type argument.
|
||||
containerViewTypeArgs[len(containerViewTypeArgs)-1] = elemViewType
|
||||
|
||||
// Instantiate the container view type with the specified type arguments.
|
||||
containerViewType := must.Get(types.Instantiate(nil, containerViewGenericType, containerViewTypeArgs, false))
|
||||
// Look up a function to create a view of a container.
|
||||
// It should be in the same package as the container type, named {ViewType}Of,
|
||||
// and have a signature like {ViewType}Of(c *Container[T]) ContainerView[T, V].
|
||||
makeContainerView, ok := containerType.Obj().Pkg().Scope().Lookup(containerViewTypeName + "Of").(*types.Func)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return containerViewType.(*types.Named), makeContainerView
|
||||
}
|
||||
|
||||
func baseType(typ types.Type) types.Type {
|
||||
if ptr, ok := typ.(*types.Pointer); ok {
|
||||
return ptr.Elem()
|
||||
}
|
||||
return typ
|
||||
}
|
||||
|
||||
func collectTypes(list *types.TypeList) []types.Type {
|
||||
// TODO(nickkhyl): use slices.Collect in Go 1.23?
|
||||
if list.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]types.Type, list.Len())
|
||||
for i := range res {
|
||||
res[i] = list.At(i)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func collectTypeParams(list *types.TypeParamList) []*types.TypeParam {
|
||||
if list.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]*types.TypeParam, list.Len())
|
||||
for i := range res {
|
||||
p := list.At(i)
|
||||
res[i] = types.NewTypeParam(p.Obj(), p.Constraint())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -327,6 +527,8 @@ var (
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
|
||||
flagCloneOnlyTypes = flag.String("clone-only-type", "", "comma-separated list of types (a subset of --type) that should only generate a go:generate clone line and not actual views")
|
||||
|
||||
typeNames []string
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -337,7 +539,7 @@ func main() {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
typeNames := strings.Split(*flagTypes, ",")
|
||||
typeNames = strings.Split(*flagTypes, ",")
|
||||
|
||||
var flagArgs []string
|
||||
flagArgs = append(flagArgs, fmt.Sprintf("-clonefunc=%v", *flagCloneFunc))
|
||||
@@ -381,7 +583,11 @@ func main() {
|
||||
}
|
||||
genView(buf, it, typ, pkg.Types)
|
||||
}
|
||||
out := pkg.Name + "_view.go"
|
||||
out := pkg.Name + "_view"
|
||||
if *flagBuildTags == "test" {
|
||||
out += "_test"
|
||||
}
|
||||
out += ".go"
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Command xdpderper runs the XDP STUN server.
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -15,11 +16,12 @@ import (
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"tailscale.com/derp/xdp"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
flagDevice = flag.String("device", "", "target device name")
|
||||
flagDevice = flag.String("device", "", "target device name (default: autodetect)")
|
||||
flagPort = flag.Int("dst-port", 0, "destination UDP port to serve")
|
||||
flagVerbose = flag.Bool("verbose", false, "verbose output including verifier errors")
|
||||
flagMode = flag.String("mode", "xdp", "XDP mode; valid modes: [xdp, xdpgeneric, xdpdrv, xdpoffload]")
|
||||
@@ -41,8 +43,18 @@ func main() {
|
||||
default:
|
||||
log.Fatal("invalid mode")
|
||||
}
|
||||
deviceName := *flagDevice
|
||||
if deviceName == "" {
|
||||
var err error
|
||||
deviceName, _, err = netutil.DefaultInterfacePortable()
|
||||
if err != nil || deviceName == "" {
|
||||
log.Fatalf("failed to detect default route interface: %v", err)
|
||||
}
|
||||
}
|
||||
log.Printf("binding to device: %s", deviceName)
|
||||
|
||||
server, err := xdp.NewSTUNServer(&xdp.STUNServerConfig{
|
||||
DeviceName: *flagDevice,
|
||||
DeviceName: deviceName,
|
||||
DstPort: *flagPort,
|
||||
AttachFlags: attachFlags,
|
||||
FullVerifierErr: *flagVerbose,
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -335,6 +333,9 @@ func (c *Direct) Close() error {
|
||||
}
|
||||
}
|
||||
c.noiseClient = nil
|
||||
if tr, ok := c.httpc.Transport.(*http.Transport); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -491,7 +492,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverLegacyKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf)
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
|
||||
@@ -588,18 +589,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
// We were given a wrapped pre-auth key, which means that in addition
|
||||
// to being a regular pre-auth key there was a suffix with information to
|
||||
// generate a tailnet-lock signature.
|
||||
nk, err := tryingNewKey.Public().MarshalBinary()
|
||||
nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())
|
||||
if err != nil {
|
||||
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
return false, "", nil, err
|
||||
}
|
||||
sig := &tka.NodeKeySignature{
|
||||
SigKind: tka.SigRotation,
|
||||
Pubkey: nk,
|
||||
Nested: wrappedSig,
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
|
||||
nodeKeySignature = sig.Serialize()
|
||||
}
|
||||
|
||||
if backendLogID == "" {
|
||||
@@ -1644,43 +1637,6 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
|
||||
// In all cases the authkey is returned, sans wrapping information if any.
|
||||
//
|
||||
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
|
||||
// and private key.
|
||||
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
|
||||
authKey, suffix, found := strings.Cut(key, "--TL")
|
||||
if !found {
|
||||
return key, false, nil, nil
|
||||
}
|
||||
sigBytes, privBytes, found := strings.Cut(suffix, "-")
|
||||
if !found {
|
||||
logf("decoding wrapped auth-key: did not find delimiter")
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: signature decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: priv decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
sig = new(tka.NodeKeySignature)
|
||||
if err := sig.Unserialize([]byte(rawSig)); err != nil {
|
||||
logf("decoding wrapped auth-key: signature: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
priv = ed25519.PrivateKey(rawPriv)
|
||||
|
||||
return authKey, true, sig, priv
|
||||
}
|
||||
|
||||
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
|
||||
if !nodeKey.IsZero() {
|
||||
req.Header.Add(tailcfg.LBHeader, nodeKey.String())
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -147,42 +146,3 @@ func TestTsmpPing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeWrappedAuthkey(t *testing.T) {
|
||||
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
|
||||
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
||||
}
|
||||
if sig != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
||||
}
|
||||
if priv != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
|
||||
}
|
||||
|
||||
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
|
||||
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if !isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
||||
}
|
||||
|
||||
if sig == nil {
|
||||
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
|
||||
t.Error("signature failed to verify")
|
||||
}
|
||||
|
||||
// Make sure the private is correct by using it.
|
||||
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
|
||||
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
|
||||
t.Error("failed to use priv")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/multierr"
|
||||
@@ -497,11 +498,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
// transport. We make exactly 1 request using this transport, so
|
||||
// there will be exactly 1 GotConn call. Additionally, the
|
||||
// transport handles 101 Switching Protocols correctly, such that
|
||||
// the Conn will not be reused or kept alive by the transport once
|
||||
// the response has been handed back from RoundTrip.
|
||||
// transport. The transport handles 101 Switching Protocols correctly,
|
||||
// such that the Conn will not be reused or kept alive by the transport
|
||||
// once the response has been handed back from RoundTrip.
|
||||
//
|
||||
// In theory, the machinery of net/http should make it such that
|
||||
// the trace callback happens-before we get the response, but
|
||||
@@ -517,10 +516,16 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
||||
// unexpected EOFs...), and we're bound to forget someday and
|
||||
// introduce a protocol optimization at a higher level that starts
|
||||
// eagerly transmitting from the server.
|
||||
connCh := make(chan net.Conn, 1)
|
||||
var lastConn syncs.AtomicValue[net.Conn]
|
||||
trace := httptrace.ClientTrace{
|
||||
// Even though we only make a single HTTP request which should
|
||||
// require a single connection, the context (with the attached
|
||||
// trace configuration) might be used by our custom dialer to
|
||||
// make other HTTP requests (e.g. BootstrapDNS). We only care
|
||||
// about the last connection made, which should be the one to
|
||||
// the control server.
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
connCh <- info.Conn
|
||||
lastConn.Store(info.Conn)
|
||||
},
|
||||
}
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
@@ -548,11 +553,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
||||
// is still a read buffer attached to it within resp.Body. So, we
|
||||
// must direct I/O through resp.Body, but we can still use the
|
||||
// underlying net.Conn for stuff like deadlines.
|
||||
var switchedConn net.Conn
|
||||
select {
|
||||
case switchedConn = <-connCh:
|
||||
default:
|
||||
}
|
||||
switchedConn := lastConn.Load()
|
||||
if switchedConn == nil {
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("httptrace didn't provide a connection")
|
||||
|
||||
@@ -11,10 +11,12 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -41,6 +43,8 @@ type httpTestParam struct {
|
||||
makeHTTPHangAfterUpgrade bool
|
||||
|
||||
doEarlyWrite bool
|
||||
|
||||
httpInDial bool
|
||||
}
|
||||
|
||||
func TestControlHTTP(t *testing.T) {
|
||||
@@ -120,6 +124,12 @@ func TestControlHTTP(t *testing.T) {
|
||||
name: "early_write",
|
||||
doEarlyWrite: true,
|
||||
},
|
||||
// Dialer needed to make another HTTP request along the way (e.g. to
|
||||
// resolve the hostname via BootstrapDNS).
|
||||
{
|
||||
name: "http_request_in_dial",
|
||||
httpInDial: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -217,6 +227,29 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
Clock: clock,
|
||||
}
|
||||
|
||||
if param.httpInDial {
|
||||
// Spin up a separate server to get a different port on localhost.
|
||||
secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return }))
|
||||
defer secondServer.Close()
|
||||
|
||||
prev := a.Dialer
|
||||
a.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second)
|
||||
defer cancel()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", secondServer.URL, nil)
|
||||
if err != nil {
|
||||
t.Errorf("http.NewRequest: %v", err)
|
||||
}
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Errorf("http.Get: %v", err)
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
return prev(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
if proxy != nil {
|
||||
proxyEnv := proxy.Start(t)
|
||||
defer proxy.Close()
|
||||
@@ -238,6 +271,7 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
si := <-sch
|
||||
if si.conn != nil {
|
||||
defer si.conn.Close()
|
||||
@@ -266,6 +300,19 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
t.Errorf("early write = %q; want %q", buf, earlyWriteMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// When no proxy is used, the RemoteAddr of the returned connection should match
|
||||
// one of the listeners of the test server.
|
||||
if proxy == nil {
|
||||
var expectedAddrs []string
|
||||
for _, ln := range []net.Listener{httpLn, httpsLn} {
|
||||
expectedAddrs = append(expectedAddrs, fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port))
|
||||
expectedAddrs = append(expectedAddrs, fmt.Sprintf("[::1]:%d", ln.Addr().(*net.TCPAddr).Port))
|
||||
}
|
||||
if !slices.Contains(expectedAddrs, conn.RemoteAddr().String()) {
|
||||
t.Errorf("unexpected remote addr: %s, want %s", conn.RemoteAddr(), expectedAddrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type serverResult struct {
|
||||
|
||||
@@ -19,10 +19,6 @@ type Knobs struct {
|
||||
// DisableUPnP indicates whether to attempt UPnP mapping.
|
||||
DisableUPnP atomic.Bool
|
||||
|
||||
// DisableDRPO is whether control says to disable the
|
||||
// DERP route optimization (Issue 150).
|
||||
DisableDRPO atomic.Bool
|
||||
|
||||
// KeepFullWGConfig is whether we should disable the lazy wireguard
|
||||
// programming and instead give WireGuard the full netmap always, even for
|
||||
// idle peers.
|
||||
@@ -99,6 +95,14 @@ type Knobs struct {
|
||||
// We began creating this rule on 2024-06-14, and this knob
|
||||
// allows us to disable the new behavior remotely if needed.
|
||||
DisableLocalDNSOverrideViaNRPT atomic.Bool
|
||||
|
||||
// DisableCryptorouting indicates that the node should not use the
|
||||
// magicsock crypto routing feature.
|
||||
DisableCryptorouting atomic.Bool
|
||||
|
||||
// DisableCaptivePortalDetection is whether the node should not perform captive portal detection
|
||||
// automatically when the network state changes.
|
||||
DisableCaptivePortalDetection atomic.Bool
|
||||
}
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
@@ -110,7 +114,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
has := capMap.Contains
|
||||
var (
|
||||
keepFullWG = has(tailcfg.NodeAttrDebugDisableWGTrim)
|
||||
disableDRPO = has(tailcfg.NodeAttrDebugDisableDRPO)
|
||||
disableUPnP = has(tailcfg.NodeAttrDisableUPnP)
|
||||
randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort)
|
||||
disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates)
|
||||
@@ -127,6 +130,8 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
||||
disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers)
|
||||
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
|
||||
disableCryptorouting = has(tailcfg.NodeAttrDisableMagicSockCryptoRouting)
|
||||
disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection)
|
||||
)
|
||||
|
||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||
@@ -136,7 +141,6 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
}
|
||||
|
||||
k.KeepFullWGConfig.Store(keepFullWG)
|
||||
k.DisableDRPO.Store(disableDRPO)
|
||||
k.DisableUPnP.Store(disableUPnP)
|
||||
k.RandomizeClientPort.Store(randomizeClientPort)
|
||||
k.OneCGNAT.Store(oneCGNAT)
|
||||
@@ -153,6 +157,8 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
||||
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
|
||||
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
|
||||
k.DisableCryptorouting.Store(disableCryptorouting)
|
||||
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
@@ -163,7 +169,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
}
|
||||
return map[string]any{
|
||||
"DisableUPnP": k.DisableUPnP.Load(),
|
||||
"DisableDRPO": k.DisableDRPO.Load(),
|
||||
"KeepFullWGConfig": k.KeepFullWGConfig.Load(),
|
||||
"RandomizeClientPort": k.RandomizeClientPort.Load(),
|
||||
"OneCGNAT": k.OneCGNAT.Load(),
|
||||
@@ -180,5 +185,7 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
||||
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
|
||||
"DisableLocalDNSOverrideViaNRPT": k.DisableLocalDNSOverrideViaNRPT.Load(),
|
||||
"DisableCryptorouting": k.DisableCryptorouting.Load(),
|
||||
"DisableCaptivePortalDetection": k.DisableCaptivePortalDetection.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
31
derp/derp.go
31
derp/derp.go
@@ -83,9 +83,16 @@ const (
|
||||
// a bug).
|
||||
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
|
||||
|
||||
// framePeerPresent is like framePeerGone, but for other
|
||||
// members of the DERP region when they're meshed up together.
|
||||
framePeerPresent = frameType(0x09) // 32B pub key of peer that's connected + optional 18B ip:port (16 byte IP + 2 byte BE uint16 port)
|
||||
// framePeerPresent is like framePeerGone, but for other members of the DERP
|
||||
// region when they're meshed up together.
|
||||
//
|
||||
// The message is at least 32 bytes (the public key of the peer that's
|
||||
// connected). If there are at least 18 bytes remaining after that, it's the
|
||||
// 16 byte IP + 2 byte BE uint16 port of the client. If there's another byte
|
||||
// remaining after that, it's a PeerPresentFlags byte.
|
||||
// While current servers send 41 bytes, old servers will send fewer, and newer
|
||||
// servers might send more.
|
||||
framePeerPresent = frameType(0x09)
|
||||
|
||||
// frameWatchConns is how one DERP node in a regional mesh
|
||||
// subscribes to the others in the region.
|
||||
@@ -124,8 +131,22 @@ const (
|
||||
type PeerGoneReasonType byte
|
||||
|
||||
const (
|
||||
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
|
||||
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
|
||||
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
|
||||
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
|
||||
PeerGoneReasonMeshConnBroke = PeerGoneReasonType(0xf0) // invented by Client.RunWatchConnectionLoop on disconnect; not sent on the wire
|
||||
)
|
||||
|
||||
// PeerPresentFlags is an optional byte of bit flags sent after a framePeerPresent message.
|
||||
//
|
||||
// For a modern server, the value should always be non-zero. If the value is zero,
|
||||
// that means the server doesn't support this field.
|
||||
type PeerPresentFlags byte
|
||||
|
||||
// PeerPresentFlags bits.
|
||||
const (
|
||||
PeerPresentIsRegular = 1 << 0
|
||||
PeerPresentIsMeshPeer = 1 << 1
|
||||
PeerPresentIsProber = 1 << 2
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -368,6 +368,8 @@ type PeerPresentMessage struct {
|
||||
Key key.NodePublic
|
||||
// IPPort is the remote IP and port of the client.
|
||||
IPPort netip.AddrPort
|
||||
// Flags is a bitmask of info about the client.
|
||||
Flags PeerPresentFlags
|
||||
}
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
@@ -547,18 +549,33 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
if n < keyLen {
|
||||
remain := b
|
||||
chunk, remain, ok := cutLeadingN(remain, keyLen)
|
||||
if !ok {
|
||||
c.logf("[unexpected] dropping short peerPresent frame from DERP server")
|
||||
continue
|
||||
}
|
||||
var msg PeerPresentMessage
|
||||
msg.Key = key.NodePublicFromRaw32(mem.B(b[:keyLen]))
|
||||
if n >= keyLen+16+2 {
|
||||
msg.IPPort = netip.AddrPortFrom(
|
||||
netip.AddrFrom16([16]byte(b[keyLen:keyLen+16])).Unmap(),
|
||||
binary.BigEndian.Uint16(b[keyLen+16:keyLen+16+2]),
|
||||
)
|
||||
msg.Key = key.NodePublicFromRaw32(mem.B(chunk))
|
||||
|
||||
const ipLen = 16
|
||||
const portLen = 2
|
||||
chunk, remain, ok = cutLeadingN(remain, ipLen+portLen)
|
||||
if !ok {
|
||||
// Older server which didn't send the IP.
|
||||
return msg, nil
|
||||
}
|
||||
msg.IPPort = netip.AddrPortFrom(
|
||||
netip.AddrFrom16([16]byte(chunk[:ipLen])).Unmap(),
|
||||
binary.BigEndian.Uint16(chunk[ipLen:]),
|
||||
)
|
||||
|
||||
chunk, _, ok = cutLeadingN(remain, 1)
|
||||
if !ok {
|
||||
// Older server which doesn't send PeerPresentFlags.
|
||||
return msg, nil
|
||||
}
|
||||
msg.Flags = PeerPresentFlags(chunk[0])
|
||||
return msg, nil
|
||||
|
||||
case frameRecvPacket:
|
||||
@@ -636,3 +653,10 @@ func (c *Client) LocalAddr() (netip.AddrPort, error) {
|
||||
}
|
||||
return netip.ParseAddrPort(a.String())
|
||||
}
|
||||
|
||||
func cutLeadingN(b []byte, n int) (chunk, remain []byte, ok bool) {
|
||||
if len(b) >= n {
|
||||
return b[:n], b[n:], true
|
||||
}
|
||||
return nil, b, false
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ type Server struct {
|
||||
removePktForwardOther expvar.Int
|
||||
avgQueueDuration *uint64 // In milliseconds; accessed atomically
|
||||
tcpRtt metrics.LabelMap // histogram
|
||||
meshUpdateBatchSize *metrics.Histogram
|
||||
meshUpdateLoopCount *metrics.Histogram
|
||||
|
||||
// verifyClientsLocalTailscaled only accepts client connections to the DERP
|
||||
// server if the clientKey is a known peer in the network, as specified by a
|
||||
@@ -323,6 +325,8 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
sentTo: map[key.NodePublic]map[key.NodePublic]int64{},
|
||||
avgQueueDuration: new(uint64),
|
||||
tcpRtt: metrics.LabelMap{Label: "le"},
|
||||
meshUpdateBatchSize: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000}),
|
||||
meshUpdateLoopCount: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100}),
|
||||
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
|
||||
clock: tstime.StdClock{},
|
||||
}
|
||||
@@ -566,7 +570,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
}
|
||||
s.keyOfAddr[c.remoteIPPort] = c.key
|
||||
s.curClients.Add(1)
|
||||
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, true)
|
||||
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, c.presentFlags(), true)
|
||||
}
|
||||
|
||||
// broadcastPeerStateChangeLocked enqueues a message to all watchers
|
||||
@@ -574,12 +578,13 @@ func (s *Server) registerClient(c *sclient) {
|
||||
// presence changed.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, ipPort netip.AddrPort, present bool) {
|
||||
func (s *Server) broadcastPeerStateChangeLocked(peer key.NodePublic, ipPort netip.AddrPort, flags PeerPresentFlags, present bool) {
|
||||
for w := range s.watchers {
|
||||
w.peerStateChange = append(w.peerStateChange, peerConnState{
|
||||
peer: peer,
|
||||
present: present,
|
||||
ipPort: ipPort,
|
||||
flags: flags,
|
||||
})
|
||||
go w.requestMeshUpdate()
|
||||
}
|
||||
@@ -601,7 +606,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
delete(s.clientsMesh, c.key)
|
||||
s.notePeerGoneFromRegionLocked(c.key)
|
||||
}
|
||||
s.broadcastPeerStateChangeLocked(c.key, netip.AddrPort{}, false)
|
||||
s.broadcastPeerStateChangeLocked(c.key, netip.AddrPort{}, 0, false)
|
||||
case *dupClientSet:
|
||||
c.debugLogf("removed duplicate client")
|
||||
if set.removeClient(c) {
|
||||
@@ -700,6 +705,7 @@ func (s *Server) addWatcher(c *sclient) {
|
||||
peer: peer,
|
||||
present: true,
|
||||
ipPort: ac.remoteIPPort,
|
||||
flags: ac.presentFlags(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -756,7 +762,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
c.meshUpdate = make(chan struct{})
|
||||
c.meshUpdate = make(chan struct{}, 1) // must be buffered; >1 is fine but wasteful
|
||||
}
|
||||
if clientInfo != nil {
|
||||
c.info = *clientInfo
|
||||
@@ -1141,13 +1147,18 @@ func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReaso
|
||||
}
|
||||
}
|
||||
|
||||
// requestMeshUpdate notes that a c's peerStateChange has been appended to and
|
||||
// should now be written.
|
||||
//
|
||||
// It does not block. If a meshUpdate is already pending for this client, it
|
||||
// does nothing.
|
||||
func (c *sclient) requestMeshUpdate() {
|
||||
if !c.canMesh {
|
||||
panic("unexpected requestMeshUpdate")
|
||||
}
|
||||
select {
|
||||
case c.meshUpdate <- struct{}{}:
|
||||
case <-c.done:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1176,6 +1187,10 @@ func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, inf
|
||||
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
|
||||
}
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "invalid 'addr' parameter") {
|
||||
// Issue 12617
|
||||
return errors.New("tailscaled version is too old (out of sync with derper binary)")
|
||||
}
|
||||
return fmt.Errorf("failed to query local tailscaled status for %v: %w", clientKey, err)
|
||||
}
|
||||
}
|
||||
@@ -1435,11 +1450,26 @@ type sclient struct {
|
||||
peerGoneLim *rate.Limiter
|
||||
}
|
||||
|
||||
func (c *sclient) presentFlags() PeerPresentFlags {
|
||||
var f PeerPresentFlags
|
||||
if c.info.IsProber {
|
||||
f |= PeerPresentIsProber
|
||||
}
|
||||
if c.canMesh {
|
||||
f |= PeerPresentIsMeshPeer
|
||||
}
|
||||
if f == 0 {
|
||||
return PeerPresentIsRegular
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// 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
|
||||
flags PeerPresentFlags
|
||||
present bool
|
||||
}
|
||||
|
||||
@@ -1613,6 +1643,11 @@ func (c *sclient) sendPong(data [8]byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
peerGoneFrameLen = keyLen + 1
|
||||
peerPresentFrameLen = keyLen + 16 + 2 + 1 // 16 byte IP + 2 byte port + 1 byte flags
|
||||
)
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) error {
|
||||
switch reason {
|
||||
@@ -1622,7 +1657,7 @@ func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) e
|
||||
c.s.peerGoneNotHereFrames.Add(1)
|
||||
}
|
||||
c.setWriteDeadline()
|
||||
data := make([]byte, 0, keyLen+1)
|
||||
data := make([]byte, 0, peerGoneFrameLen)
|
||||
data = peer.AppendTo(data)
|
||||
data = append(data, byte(reason))
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, uint32(len(data))); err != nil {
|
||||
@@ -1634,73 +1669,62 @@ func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) e
|
||||
}
|
||||
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic, ipPort netip.AddrPort) error {
|
||||
func (c *sclient) sendPeerPresent(peer key.NodePublic, ipPort netip.AddrPort, flags PeerPresentFlags) error {
|
||||
c.setWriteDeadline()
|
||||
const frameLen = keyLen + 16 + 2
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, frameLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, peerPresentFrameLen); err != nil {
|
||||
return err
|
||||
}
|
||||
payload := make([]byte, frameLen)
|
||||
payload := make([]byte, peerPresentFrameLen)
|
||||
_ = peer.AppendTo(payload[:0])
|
||||
a16 := ipPort.Addr().As16()
|
||||
copy(payload[keyLen:], a16[:])
|
||||
binary.BigEndian.PutUint16(payload[keyLen+16:], ipPort.Port())
|
||||
payload[keyLen+18] = byte(flags)
|
||||
_, err := c.bw.Write(payload)
|
||||
return err
|
||||
}
|
||||
|
||||
// sendMeshUpdates drains as many mesh peerStateChange entries as
|
||||
// possible into the write buffer WITHOUT flushing or otherwise
|
||||
// blocking (as it holds c.s.mu while working). If it can't drain them
|
||||
// all, it schedules itself to be called again in the future.
|
||||
// sendMeshUpdates drains all mesh peerStateChange entries into the write buffer
|
||||
// without flushing.
|
||||
func (c *sclient) sendMeshUpdates() error {
|
||||
c.s.mu.Lock()
|
||||
defer c.s.mu.Unlock()
|
||||
var lastBatch []peerConnState // memory to best effort reuse
|
||||
|
||||
// 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
|
||||
// takeAll returns c.peerStateChange and empties it.
|
||||
takeAll := func() []peerConnState {
|
||||
c.s.mu.Lock()
|
||||
defer c.s.mu.Unlock()
|
||||
if len(c.peerStateChange) == 0 {
|
||||
return nil
|
||||
}
|
||||
batch := c.peerStateChange
|
||||
if cap(lastBatch) > 16 {
|
||||
lastBatch = nil
|
||||
}
|
||||
c.peerStateChange = lastBatch[:0]
|
||||
return batch
|
||||
}
|
||||
|
||||
writes := 0
|
||||
for _, pcs := range c.peerStateChange {
|
||||
if c.bw.Available() <= frameHeaderLen+keyLen {
|
||||
break
|
||||
for loops := 0; ; loops++ {
|
||||
batch := takeAll()
|
||||
if len(batch) == 0 {
|
||||
c.s.meshUpdateLoopCount.Observe(float64(loops))
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if pcs.present {
|
||||
err = c.sendPeerPresent(pcs.peer, pcs.ipPort)
|
||||
} else {
|
||||
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
|
||||
}
|
||||
if err != nil {
|
||||
// Shouldn't happen, though, as we're writing
|
||||
// into available buffer space, not the
|
||||
// network.
|
||||
return err
|
||||
}
|
||||
writes++
|
||||
}
|
||||
c.s.meshUpdateBatchSize.Observe(float64(len(batch)))
|
||||
|
||||
remain := copy(c.peerStateChange, c.peerStateChange[writes:])
|
||||
c.peerStateChange = c.peerStateChange[:remain]
|
||||
|
||||
// Did we manage to write them all into the bufio buffer without flushing?
|
||||
if len(c.peerStateChange) == 0 {
|
||||
if cap(c.peerStateChange) > 16 {
|
||||
c.peerStateChange = nil
|
||||
for _, pcs := range batch {
|
||||
var err error
|
||||
if pcs.present {
|
||||
err = c.sendPeerPresent(pcs.peer, pcs.ipPort, pcs.flags)
|
||||
} else {
|
||||
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Didn't finish in the buffer space provided; schedule a future run.
|
||||
go c.requestMeshUpdate()
|
||||
lastBatch = batch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendPacket writes contents to the client in a RecvPacket frame. If
|
||||
@@ -1929,6 +1953,8 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
|
||||
}))
|
||||
m.Set("counter_tcp_rtt", &s.tcpRtt)
|
||||
m.Set("counter_mesh_update_batch_size", s.meshUpdateBatchSize)
|
||||
m.Set("counter_mesh_update_loop_count", s.meshUpdateLoopCount)
|
||||
var expvarVersion expvar.String
|
||||
expvarVersion.Set(version.Long())
|
||||
m.Set("version", &expvarVersion)
|
||||
|
||||
@@ -623,7 +623,13 @@ func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) {
|
||||
}
|
||||
}))
|
||||
}
|
||||
t.Logf("got present with IP %v", m.IPPort)
|
||||
t.Logf("got present with IP %v, flags=%v", m.IPPort, m.Flags)
|
||||
switch m.Flags {
|
||||
case PeerPresentIsMeshPeer, PeerPresentIsRegular:
|
||||
// Okay
|
||||
default:
|
||||
t.Errorf("unexpected PeerPresentIsMeshPeer flags %v", m.Flags)
|
||||
}
|
||||
delete(want, got)
|
||||
if len(want) == 0 {
|
||||
return
|
||||
|
||||
@@ -381,6 +381,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}()
|
||||
|
||||
var node *tailcfg.DERPNode // nil when using c.url to dial
|
||||
var idealNodeInRegion bool
|
||||
switch {
|
||||
case useWebsockets():
|
||||
var urlStr string
|
||||
@@ -421,6 +422,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
default:
|
||||
c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
|
||||
tcpConn, node, err = c.dialRegion(ctx, reg)
|
||||
idealNodeInRegion = err == nil && reg.Nodes[0] == node
|
||||
}
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -494,6 +496,18 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}
|
||||
req.Header.Set("Upgrade", "DERP")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
if !idealNodeInRegion && reg != nil {
|
||||
// This is purely informative for now (2024-07-06) for stats:
|
||||
req.Header.Set("Ideal-Node", reg.Nodes[0].Name)
|
||||
// TODO(bradfitz,raggi): start a time.AfterFunc for 30m-1h or so to
|
||||
// dialNode(reg.Nodes[0]) and see if we can even TCP connect to it. If
|
||||
// so, TLS handshake it as well (which is mixed up in this massive
|
||||
// connect method) and then if it all appears good, grab the mutex, bump
|
||||
// connGen, finish the Upgrade, close the old one, and set a new field
|
||||
// on Client that's like "here's the connect result and connGen for the
|
||||
// next connect that comes in"). Tracking bug for all this is:
|
||||
// https://github.com/tailscale/tailscale/issues/12724
|
||||
}
|
||||
|
||||
if !serverPub.IsZero() && serverProtoVersion != 0 {
|
||||
// parseMetaCert found the server's public key (no TLS
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -299,13 +298,13 @@ func TestBreakWatcherConnRecv(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var peers int
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) {
|
||||
t.Logf("add: %v", k.ShortString())
|
||||
add := func(m derp.PeerPresentMessage) {
|
||||
t.Logf("add: %v", m.Key.ShortString())
|
||||
peers++
|
||||
// Signal that the watcher has run
|
||||
watcherChan <- peers
|
||||
}
|
||||
remove := func(k key.NodePublic) { t.Logf("remove: %v", k.ShortString()); peers-- }
|
||||
remove := func(m derp.PeerGoneMessage) { t.Logf("remove: %v", m.Peer.ShortString()); peers-- }
|
||||
|
||||
watcher1.RunWatchConnectionLoop(ctx, serverPrivateKey1.Public(), t.Logf, add, remove)
|
||||
}()
|
||||
@@ -370,15 +369,15 @@ func TestBreakWatcherConn(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var peers int
|
||||
add := func(k key.NodePublic, _ netip.AddrPort) {
|
||||
t.Logf("add: %v", k.ShortString())
|
||||
add := func(m derp.PeerPresentMessage) {
|
||||
t.Logf("add: %v", m.Key.ShortString())
|
||||
peers++
|
||||
// Signal that the watcher has run
|
||||
watcherChan <- peers
|
||||
// Wait for breaker to run
|
||||
<-breakerChan
|
||||
}
|
||||
remove := func(k key.NodePublic) { t.Logf("remove: %v", k.ShortString()); peers-- }
|
||||
remove := func(m derp.PeerGoneMessage) { t.Logf("remove: %v", m.Peer.ShortString()); peers-- }
|
||||
|
||||
watcher1.RunWatchConnectionLoop(ctx, serverPrivateKey1.Public(), t.Logf, add, remove)
|
||||
}()
|
||||
@@ -407,8 +406,8 @@ func TestBreakWatcherConn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func noopAdd(key.NodePublic, netip.AddrPort) {}
|
||||
func noopRemove(key.NodePublic) {}
|
||||
func noopAdd(derp.PeerPresentMessage) {}
|
||||
func noopRemove(derp.PeerGoneMessage) {}
|
||||
|
||||
func TestRunWatchConnectionLoopServeConnect(t *testing.T) {
|
||||
defer func() { testHookWatchLookConnectResult = nil }()
|
||||
|
||||
@@ -5,7 +5,6 @@ package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -35,9 +34,14 @@ var testHookWatchLookConnectResult func(connectError error, wasSelfConnect bool)
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to be
|
||||
// closed, and c itself needs to be closed.
|
||||
//
|
||||
// It is a fatal error to call this on an already-started Client withoutq having
|
||||
// It is a fatal error to call this on an already-started Client without having
|
||||
// initialized Client.WatchConnectionChanges to true.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(key.NodePublic, netip.AddrPort), remove func(key.NodePublic)) {
|
||||
//
|
||||
// If the DERP connection breaks and reconnects, remove will be called for all
|
||||
// previously seen peers, with Reason type PeerGoneReasonSynthetic. Those
|
||||
// clients are likely still connected and their add message will appear after
|
||||
// reconnect.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.NodePublic, infoLogf logger.Logf, add func(derp.PeerPresentMessage), remove func(derp.PeerGoneMessage)) {
|
||||
if !c.WatchConnectionChanges {
|
||||
if c.isStarted() {
|
||||
panic("invalid use of RunWatchConnectionLoop on already-started Client without setting Client.RunWatchConnectionLoop")
|
||||
@@ -62,7 +66,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
logf("reconnected; clearing %d forwarding mappings", len(present))
|
||||
for k := range present {
|
||||
remove(k)
|
||||
remove(derp.PeerGoneMessage{Peer: k, Reason: derp.PeerGoneReasonMeshConnBroke})
|
||||
}
|
||||
present = map[key.NodePublic]bool{}
|
||||
}
|
||||
@@ -84,13 +88,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
updatePeer := func(k key.NodePublic, ipPort netip.AddrPort, isPresent bool) {
|
||||
if isPresent {
|
||||
add(k, ipPort)
|
||||
} else {
|
||||
remove(k)
|
||||
}
|
||||
|
||||
updatePeer := func(k key.NodePublic, isPresent bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if isPresent {
|
||||
@@ -148,7 +146,8 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(m.Key, m.IPPort, true)
|
||||
add(m)
|
||||
updatePeer(m.Key, true)
|
||||
case derp.PeerGoneMessage:
|
||||
switch m.Reason {
|
||||
case derp.PeerGoneReasonDisconnected:
|
||||
@@ -160,7 +159,8 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
logf("Recv: peer %s not at server %s for unknown reason %v",
|
||||
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
|
||||
}
|
||||
updatePeer(key.NodePublic(m.Peer), netip.AddrPort{}, false)
|
||||
remove(m)
|
||||
updatePeer(m.Peer, false)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The update program fetches the libbpf headers from the libbpf GitHub repository
|
||||
// and writes them to disk.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package xdp contains the XDP STUN program.
|
||||
package xdp
|
||||
|
||||
// XDPAttachFlags represents how XDP program will be attached to interface. This
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/cilium/ebpf"
|
||||
"github.com/cilium/ebpf/link"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -type config -type counters_key -type counter_key_af -type counter_key_packets_bytes_action -type counter_key_prog_end bpf xdp.c -- -I headers
|
||||
@@ -27,6 +28,7 @@ type STUNServer struct {
|
||||
metrics *stunServerMetrics
|
||||
dstPort int
|
||||
dropSTUN bool
|
||||
link link.Link
|
||||
}
|
||||
|
||||
//lint:ignore U1000 used in xdp_linux_test.go, which has a build tag
|
||||
@@ -87,7 +89,7 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding device: %w", err)
|
||||
}
|
||||
_, err = link.AttachXDP(link.XDPOptions{
|
||||
link, err := link.AttachXDP(link.XDPOptions{
|
||||
Program: objs.XdpProgFunc,
|
||||
Interface: iface.Index,
|
||||
Flags: link.XDPAttachFlags(config.AttachFlags),
|
||||
@@ -95,6 +97,7 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error attaching XDP program to dev: %w", err)
|
||||
}
|
||||
server.link = link
|
||||
return server, nil
|
||||
}
|
||||
|
||||
@@ -102,7 +105,12 @@ func NewSTUNServer(config *STUNServerConfig, opts ...STUNServerOption) (*STUNSer
|
||||
func (s *STUNServer) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.objs.Close()
|
||||
var errs []error
|
||||
if s.link != nil {
|
||||
errs = append(errs, s.link.Close())
|
||||
}
|
||||
errs = append(errs, s.objs.Close())
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
type stunServerMetrics struct {
|
||||
|
||||
@@ -36,13 +36,19 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
regStr = map[string]*string{}
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
mu sync.Mutex
|
||||
// +checklocks:mu
|
||||
set = map[string]string{}
|
||||
// +checklocks:mu
|
||||
regStr = map[string]*string{}
|
||||
// +checklocks:mu
|
||||
regBool = map[string]*bool{}
|
||||
// +checklocks:mu
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
// +checklocks:mu
|
||||
regDuration = map[string]*time.Duration{}
|
||||
regInt = map[string]*int{}
|
||||
// +checklocks:mu
|
||||
regInt = map[string]*int{}
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
@@ -51,6 +57,7 @@ func noteEnv(k, v string) {
|
||||
noteEnvLocked(k, v)
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func noteEnvLocked(k, v string) {
|
||||
if v != "" {
|
||||
set[k] = v
|
||||
@@ -202,6 +209,7 @@ func RegisterInt(envVar string) func() int {
|
||||
return func() int { return *p }
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setBoolLocked(p *bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -215,6 +223,7 @@ func setBoolLocked(p *bool, envVar, val string) {
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -228,6 +237,7 @@ func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
p.Set(b)
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -241,6 +251,7 @@ func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
}
|
||||
}
|
||||
|
||||
// +checklocks:mu
|
||||
func setIntLocked(p *int, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
|
||||
@@ -120,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
|
||||
# nix-direnv cache busting line: sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=
|
||||
|
||||
85
go.mod
85
go.mod
@@ -4,8 +4,6 @@ go 1.22.0
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
fybrik.io/crdoc v0.6.3
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||
github.com/andybalholm/brotli v1.1.0
|
||||
@@ -21,17 +19,17 @@ require (
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
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-20240119213807-a09d6be7affa
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/djherbis/times v1.6.0
|
||||
github.com/dsnet/try v0.0.3
|
||||
github.com/elastic/crd-ref-docs v0.0.12
|
||||
github.com/evanw/esbuild v0.19.11
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fxamacker/cbor/v2 v2.6.0
|
||||
github.com/gaissmai/bart v0.4.1
|
||||
github.com/gaissmai/bart v0.11.1
|
||||
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
|
||||
@@ -45,7 +43,6 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/goreleaser/nfpm/v2 v2.33.1
|
||||
github.com/hdevalence/ed25519consensus v0.2.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/illarion/gonotify v1.0.1
|
||||
github.com/inetaf/tcpproxy v0.0.0-20240214030015-3ce58045626c
|
||||
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
|
||||
@@ -78,42 +75,41 @@ require (
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4
|
||||
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-20240102155253-bf50773ba734
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e
|
||||
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.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
|
||||
go.uber.org/zap v1.27.0
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/crypto v0.25.0
|
||||
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
|
||||
golang.org/x/mod v0.18.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/mod v0.19.0
|
||||
golang.org/x/net v0.27.0
|
||||
golang.org/x/oauth2 v0.16.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/term v0.21.0
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/time v0.5.0
|
||||
golang.org/x/tools v0.22.0
|
||||
golang.org/x/tools v0.23.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-20240306221502-ee1e1f6070e3
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987
|
||||
honnef.co/go/tools v0.4.6
|
||||
k8s.io/api v0.30.1
|
||||
k8s.io/apimachinery v0.30.1
|
||||
k8s.io/apiserver v0.30.1
|
||||
k8s.io/client-go v0.30.1
|
||||
modernc.org/sqlite v1.29.10
|
||||
k8s.io/api v0.30.3
|
||||
k8s.io/apimachinery v0.30.3
|
||||
k8s.io/apiserver v0.30.3
|
||||
k8s.io/client-go v0.30.3
|
||||
nhooyr.io/websocket v1.8.10
|
||||
sigs.k8s.io/controller-runtime v0.18.4
|
||||
sigs.k8s.io/controller-tools v0.15.1-0.20240618033008-7824932b0cab
|
||||
@@ -122,26 +118,27 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.13.0 // 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/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gobuffalo/flect v1.0.2 // indirect
|
||||
github.com/goccy/go-yaml v1.12.0 // indirect
|
||||
github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.49.3 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
|
||||
go.opentelemetry.io/otel v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.22.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.22.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -201,7 +198,7 @@ require (
|
||||
github.com/denis-tingaikin/go-header v0.4.3 // 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.5+incompatible // indirect
|
||||
github.com/docker/docker v26.1.4+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
|
||||
@@ -209,7 +206,7 @@ require (
|
||||
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.9.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/firefart/nonamedreturns v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
@@ -218,7 +215,7 @@ require (
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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-logr/logr v1.4.2 // 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
|
||||
@@ -260,7 +257,7 @@ require (
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/huandu/xstrings v1.5.0 // indirect
|
||||
github.com/imdario/mergo v0.3.16 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
@@ -345,14 +342,14 @@ require (
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
github.com/spf13/afero v1.9.5 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // 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
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/stretchr/testify v1.9.0
|
||||
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
|
||||
@@ -376,7 +373,7 @@ require (
|
||||
gitlab.com/digitalxero/go-conventional-commit v1.0.7 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/image v0.18.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
@@ -387,10 +384,10 @@ require (
|
||||
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.30.1 // indirect
|
||||
k8s.io/klog/v2 v2.120.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.30.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
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
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-ye8puuEDd/CRSy/AHrtLdKVxVASJAdpt6bW3jU2OUvw=
|
||||
sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=
|
||||
|
||||
219
go.sum
219
go.sum
@@ -46,8 +46,6 @@ 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=
|
||||
fybrik.io/crdoc v0.6.3 h1:jNNAVINu8up5vrLa0jrV7z7HSlyHF/6lNOrAtrXwYlI=
|
||||
fybrik.io/crdoc v0.6.3/go.mod h1:kvZRt7VAzOyrmDpIqREtcKAVFSJYEBoAyniYebsJGtQ=
|
||||
github.com/Abirdcfly/dupword v0.0.11 h1:z6v8rMETchZXUIuHxYNmlUAuKuB21PeaSymTed16wgU=
|
||||
github.com/Abirdcfly/dupword v0.0.11/go.mod h1:wH8mVGuf3CP5fsBTkfWwwwKTjDnVVCxtU8d8rgeVYXA=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
@@ -56,6 +54,8 @@ github.com/Antonboom/errname v0.1.9 h1:BZDX4r3l4TBZxZ2o2LNrlGxSHran4d1u4veZdoORT
|
||||
github.com/Antonboom/errname v0.1.9/go.mod h1:nLTcJzevREuAsgTbG85UsuiWpMpAqbKD1HNZ29OzE58=
|
||||
github.com/Antonboom/nilnil v0.1.4 h1:yWIfwbCRDpJiJvs7Quz55dzeXCgORQyAG29N9/J5H2Q=
|
||||
github.com/Antonboom/nilnil v0.1.4/go.mod h1:iOov/7gRcXkeEU+EMGpBu2ORih3iyVEiWjeste1SJm8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
@@ -73,10 +73,10 @@ github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
@@ -194,6 +194,9 @@ github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8
|
||||
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
|
||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
|
||||
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -215,13 +218,15 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS
|
||||
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/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
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.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
|
||||
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
@@ -237,8 +242,6 @@ github.com/dave/brenda v1.1.0 h1:Sl1LlwXnbw7xMhq3y2x11McFu43AjDcwkllxxgZ3EZw=
|
||||
github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM=
|
||||
github.com/dave/courtney v0.4.0 h1:Vb8hi+k3O0h5++BR96FIcX0x3NovRbnhGd/dRr8inBk=
|
||||
github.com/dave/courtney v0.4.0/go.mod h1:3WSU3yaloZXYAxRuWt8oRyVb9SaRiMBt5Kz/2J227tM=
|
||||
github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE=
|
||||
github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4kXpVnZ6TPxR2mZ9qVKjNNAs=
|
||||
github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -259,14 +262,18 @@ github.com/docker/cli v25.0.0+incompatible h1:zaimaQdnX7fYWFqzN88exE9LDEvRslexpF
|
||||
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.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
|
||||
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU=
|
||||
github.com/docker/docker v26.1.4+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/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
|
||||
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/elastic/crd-ref-docs v0.0.12 h1:F3seyncbzUz3rT3d+caeYWhumb5ojYQ6Bl0Z+zOp16M=
|
||||
github.com/elastic/crd-ref-docs v0.0.12/go.mod h1:X83mMBdJt05heJUYiS3T0yJ/JkCuliuhSUNav5Gjo/U=
|
||||
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=
|
||||
@@ -289,10 +296,12 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
|
||||
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
|
||||
github.com/evanw/esbuild v0.19.11 h1:mbPO1VJ/df//jjUd+p/nRLYCpizXxXb2w/zZMShxa2k=
|
||||
github.com/evanw/esbuild v0.19.11/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
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.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
@@ -303,8 +312,8 @@ github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1t
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls=
|
||||
github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
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=
|
||||
@@ -330,8 +339,11 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
|
||||
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.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
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=
|
||||
@@ -342,6 +354,12 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX
|
||||
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-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.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
@@ -371,6 +389,8 @@ 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/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM=
|
||||
github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU=
|
||||
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=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
@@ -512,6 +532,9 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW
|
||||
github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M=
|
||||
github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY=
|
||||
github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -522,8 +545,6 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
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.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
@@ -531,12 +552,10 @@ github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3s
|
||||
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/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||
github.com/huandu/xstrings v1.5.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=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio=
|
||||
@@ -623,14 +642,12 @@ github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoa
|
||||
github.com/kunwardeep/paralleltest v1.0.6/go.mod h1:Y0Y0XISdZM5IKm3TREQMZ6iteqn1YuwCsJO/0kL9Zes=
|
||||
github.com/kyoh86/exportloopref v0.1.11 h1:1Z0bcmTypkL3Q4k+IDHMWTcnCliEZcaPiIe0/ymEyhQ=
|
||||
github.com/kyoh86/exportloopref v0.1.11/go.mod h1:qkV4UF1zGl6EkF1ox8L5t9SwyeBAZ3qLMd6up458uqA=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUcJwlhA=
|
||||
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=
|
||||
@@ -682,6 +699,10 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA=
|
||||
github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -691,6 +712,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/moricho/tparallel v0.3.1 h1:fQKD4U1wRMAYNngDonW5XupoB/ZGJHdpzrWqgyg9krA=
|
||||
github.com/moricho/tparallel v0.3.1/go.mod h1:leENX2cUv7Sv2qDgdi0D0fCftN8fRC67Bcn8pqzeYNI=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
@@ -699,8 +722,6 @@ github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81
|
||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAVRfPlSo=
|
||||
@@ -791,8 +812,6 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
|
||||
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
@@ -854,8 +873,8 @@ github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
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/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
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=
|
||||
@@ -869,8 +888,9 @@ github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8L
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
@@ -882,8 +902,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/studio-b12/gowebdav v0.9.0 h1:1j1sc9gQnNxbXXM4M/CebPOX4aXYtr7MojAVcN4dHjU=
|
||||
github.com/studio-b12/gowebdav v0.9.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8=
|
||||
@@ -904,8 +924,8 @@ github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPx
|
||||
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-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKfFTvR00d0b0R0bzRKBW9yrj813fhI=
|
||||
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734/go.mod h1:6v53VHLmLKUaqWMpSGDeRWhltLSCEteMItYoiKLpdJk=
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba h1:uNo1VCm/xg4alMkIKo8RWTKNx5y1otfVOcKbp+irkL4=
|
||||
github.com/tailscale/mkctr v0.0.0-20240628074852-17ca944da6ba/go.mod h1:DxnqIXBplij66U2ZkL688xy07q97qQ83P+TVueLiHq4=
|
||||
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/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w=
|
||||
@@ -914,10 +934,10 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:t
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
|
||||
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
|
||||
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
@@ -988,12 +1008,28 @@ 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.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
|
||||
go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
|
||||
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY=
|
||||
go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
|
||||
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
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-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
@@ -1012,8 +1048,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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
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=
|
||||
@@ -1032,8 +1068,8 @@ golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTr
|
||||
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.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
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=
|
||||
@@ -1062,8 +1098,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1108,8 +1144,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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
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=
|
||||
@@ -1203,8 +1239,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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.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=
|
||||
@@ -1213,8 +1249,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.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
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=
|
||||
@@ -1309,12 +1345,14 @@ 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.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
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=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
@@ -1385,6 +1423,11 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20240102182953-50ed04b92917 h1:nz5NESFLZbJGPFxDT/HCn+V1mZ8JGNoY4nUpmW/Y2eg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac h1:OZkkudMUu9LVQMCoRUbI/1p5VCo9BOrlvkqMvWtqa6s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:B5xPO//w8qmBDjGReYLpR6UJPnkldGkCSMoH/2vxJeg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1401,6 +1444,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
|
||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1446,8 +1491,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-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8=
|
||||
gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
|
||||
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=
|
||||
@@ -1459,48 +1504,22 @@ honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8=
|
||||
honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY=
|
||||
k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM=
|
||||
k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws=
|
||||
k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4=
|
||||
k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U=
|
||||
k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/apiserver v0.30.1 h1:BEWEe8bzS12nMtDKXzCF5Q5ovp6LjjYkSp8qOPk8LZ8=
|
||||
k8s.io/apiserver v0.30.1/go.mod h1:i87ZnQ+/PGAmSbD/iEKM68bm1D5reX8fO4Ito4B01mo=
|
||||
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
||||
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
||||
k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw=
|
||||
k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ=
|
||||
k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04=
|
||||
k8s.io/apiextensions-apiserver v0.30.3 h1:oChu5li2vsZHx2IvnGP3ah8Nj3KyqG3kRSaKmijhB9U=
|
||||
k8s.io/apiextensions-apiserver v0.30.3/go.mod h1:uhXxYDkMAvl6CJw4lrDN4CPbONkF3+XL9cacCT44kV4=
|
||||
k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc=
|
||||
k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
|
||||
k8s.io/apiserver v0.30.3 h1:QZJndA9k2MjFqpnyYv/PH+9PE0SHhx3hBho4X0vE65g=
|
||||
k8s.io/apiserver v0.30.3/go.mod h1:6Oa88y1CZqnzetd2JdepO0UXzQX4ZnOekx2/PtEjrOg=
|
||||
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
||||
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ=
|
||||
k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk=
|
||||
modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
|
||||
modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA=
|
||||
modernc.org/ccgo/v4 v4.16.0/go.mod h1:dkNyWIjFrVIZ68DTo36vHK+6/ShBn4ysU61So6PIqCI=
|
||||
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
||||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
|
||||
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg=
|
||||
modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
|
||||
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
|
||||
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
|
||||
modernc.org/sqlite v1.29.10 h1:3u93dz83myFnMilBGCOLbr+HjklS6+5rJLx4q86RDAg=
|
||||
modernc.org/sqlite v1.29.10/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA=
|
||||
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/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=
|
||||
|
||||
@@ -1 +1 @@
|
||||
4d101c0f2d2a234b8902bfff5fadb16070201f0a
|
||||
2f152a4eff5875655a9a84fce8f8d329f8d9a321
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module gokrazy/build/tsapp
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require tailscale.com v1.66.4 // indirect
|
||||
@@ -1,86 +0,0 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
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/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
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/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/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
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/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||
github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
|
||||
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/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
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/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-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
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-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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
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/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
k8s.io/client-go v0.29.1 h1:19B/+2NGEwnFLzt0uB5kNJnfTsbV8w6TgQRz9l7ti7A=
|
||||
k8s.io/client-go v0.29.1/go.mod h1:TDG/psL9hdet0TI9mGyHJSgRkW3H9JZk2dNEUS7bRks=
|
||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
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=
|
||||
tailscale.com v1.66.4 h1:V0vTQah3xi2/zbsxJeOfl5QbO1WJPeD9TMlfL0daXqc=
|
||||
tailscale.com v1.66.4/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM=
|
||||
@@ -1,5 +0,0 @@
|
||||
module gokrazy/build/tsapp
|
||||
|
||||
go 1.22.2
|
||||
|
||||
require tailscale.com v1.66.4 // indirect
|
||||
9
gokrazy/tsapp/builddir/tailscale.com/go.mod
Normal file
9
gokrazy/tsapp/builddir/tailscale.com/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module gokrazy/build/tsapp
|
||||
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
replace tailscale.com => ../../../..
|
||||
|
||||
require tailscale.com v0.0.0-00010101000000-000000000000 // indirect
|
||||
@@ -40,10 +40,10 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez
|
||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
|
||||
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
|
||||
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/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls=
|
||||
github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
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/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg=
|
||||
@@ -54,8 +54,8 @@ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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=
|
||||
@@ -74,12 +74,18 @@ github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0
|
||||
github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ=
|
||||
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
|
||||
@@ -92,14 +98,18 @@ 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/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
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.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||
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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ=
|
||||
github.com/tailscale/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=
|
||||
@@ -110,12 +120,14 @@ github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVL
|
||||
github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g=
|
||||
github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1 h1:ycpNCSYwzZ7x4G4ioPNtKQmIY0G/3o4pVf8wCZq6blY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20240705152531-2f5d148bcfe1/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA=
|
||||
github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
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/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=
|
||||
@@ -130,25 +142,41 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H
|
||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
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/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
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/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
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.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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=
|
||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM=
|
||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0=
|
||||
k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q=
|
||||
k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc=
|
||||
k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k=
|
||||
k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U=
|
||||
nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q=
|
||||
nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
|
||||
tailscale.com v1.66.4 h1:V0vTQah3xi2/zbsxJeOfl5QbO1WJPeD9TMlfL0daXqc=
|
||||
tailscale.com v1.66.4/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM=
|
||||
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=
|
||||
@@ -32,4 +32,8 @@ const (
|
||||
|
||||
// ArgServerName provides a Warnable with the hostname of a server involved in the unhealthy state.
|
||||
ArgServerName Arg = "server-name"
|
||||
|
||||
// ArgServerName provides a Warnable with comma delimited list of the hostname of the servers involved in the unhealthy state.
|
||||
// If no nameservers were available to query, this will be an empty string.
|
||||
ArgDNSServers Arg = "dns-servers"
|
||||
)
|
||||
|
||||
162
health/health.go
162
health/health.go
@@ -6,6 +6,7 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
@@ -69,6 +70,9 @@ type Tracker struct {
|
||||
|
||||
warnables []*Warnable // keys ever set
|
||||
warnableVal map[*Warnable]*warningState
|
||||
// pendingVisibleTimers contains timers for Warnables that are unhealthy, but are
|
||||
// not visible to the user yet, because they haven't been unhealthy for TimeToVisible
|
||||
pendingVisibleTimers map[*Warnable]*time.Timer
|
||||
|
||||
// sysErr maps subsystems to their current error (or nil if the subsystem is healthy)
|
||||
// Deprecated: using Warnables should be preferred
|
||||
@@ -78,6 +82,7 @@ type Tracker struct {
|
||||
|
||||
latestVersion *tailcfg.ClientVersion // or nil
|
||||
checkForUpdates bool
|
||||
applyUpdates opt.Bool
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
@@ -92,7 +97,8 @@ type Tracker struct {
|
||||
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
|
||||
ipnState string
|
||||
ipnWantRunning bool
|
||||
anyInterfaceUp opt.Bool // empty means unknown (assume true)
|
||||
ipnWantRunningLastTrue time.Time // when ipnWantRunning last changed false -> true
|
||||
anyInterfaceUp opt.Bool // empty means unknown (assume true)
|
||||
udp4Unbound bool
|
||||
controlHealth []string
|
||||
lastLoginErr error
|
||||
@@ -160,6 +166,7 @@ func Register(w *Warnable) *Warnable {
|
||||
if registeredWarnables[w.Code] != nil {
|
||||
panic(fmt.Sprintf("health: a Warnable with code %q was already registered", w.Code))
|
||||
}
|
||||
|
||||
mak.Set(®isteredWarnables, w.Code, w)
|
||||
return w
|
||||
}
|
||||
@@ -211,9 +218,16 @@ type Warnable struct {
|
||||
// Deprecated: this is only used in one case, and will be removed in a future PR
|
||||
MapDebugFlag string
|
||||
|
||||
// If true, this warnable is related to configuration of networking stack
|
||||
// on the machine that impacts connectivity.
|
||||
// ImpactsConnectivity is whether this Warnable in an unhealthy state will impact the user's
|
||||
// ability to connect to the Internet or other nodes on the tailnet. On platforms where
|
||||
// the client GUI supports a tray icon, the client will display an exclamation mark
|
||||
// on the tray icon when ImpactsConnectivity is set to true and the Warnable is unhealthy.
|
||||
ImpactsConnectivity bool
|
||||
|
||||
// TimeToVisible is the Duration that the Warnable has to be in an unhealthy state before it
|
||||
// should be surfaced as unhealthy to the user. This is used to prevent transient errors from being
|
||||
// displayed to the user.
|
||||
TimeToVisible time.Duration
|
||||
}
|
||||
|
||||
// StaticMessage returns a function that always returns the input string, to be used in
|
||||
@@ -250,9 +264,16 @@ func (t *Tracker) nil() bool {
|
||||
type Severity string
|
||||
|
||||
const (
|
||||
SeverityHigh Severity = "high"
|
||||
// SeverityHigh is the highest severity level, used for critical errors that need immediate attention.
|
||||
// On platforms where the client GUI can deliver notifications, a SeverityHigh Warnable will trigger
|
||||
// a modal notification.
|
||||
SeverityHigh Severity = "high"
|
||||
// SeverityMedium is used for errors that are important but not critical. This won't trigger a modal
|
||||
// notification, however it will be displayed in a more visible way than a SeverityLow Warnable.
|
||||
SeverityMedium Severity = "medium"
|
||||
SeverityLow Severity = "low"
|
||||
// SeverityLow is used for less important notices that don't need immediate attention. The user will
|
||||
// have to go to a Settings window, or another "hidden" GUI location to see these messages.
|
||||
SeverityLow Severity = "low"
|
||||
)
|
||||
|
||||
// Args is a map of Args to string values that can be used to provide parameters regarding
|
||||
@@ -280,6 +301,15 @@ func (ws *warningState) Equal(other *warningState) bool {
|
||||
return ws.BrokenSince.Equal(other.BrokenSince) && maps.Equal(ws.Args, other.Args)
|
||||
}
|
||||
|
||||
// IsVisible returns whether the Warnable should be visible to the user, based on the TimeToVisible
|
||||
// field of the Warnable and the BrokenSince time when the Warnable became unhealthy.
|
||||
func (w *Warnable) IsVisible(ws *warningState) bool {
|
||||
if ws == nil || w.TimeToVisible == 0 {
|
||||
return true
|
||||
}
|
||||
return time.Since(ws.BrokenSince) >= w.TimeToVisible
|
||||
}
|
||||
|
||||
// SetUnhealthy sets a warningState for the given Warnable with the provided Args, and should be
|
||||
// called when a Warnable becomes unhealthy, or its unhealthy status needs to be updated.
|
||||
// SetUnhealthy takes ownership of args. The args can be nil if no additional information is
|
||||
@@ -316,7 +346,27 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
|
||||
mak.Set(&t.warnableVal, w, ws)
|
||||
if !ws.Equal(prevWs) {
|
||||
for _, cb := range t.watchers {
|
||||
go cb(w, w.unhealthyState(ws))
|
||||
// If the Warnable has been unhealthy for more than its TimeToVisible, the callback should be
|
||||
// executed immediately. Otherwise, the callback should be enqueued to run once the Warnable
|
||||
// becomes visible.
|
||||
if w.IsVisible(ws) {
|
||||
go cb(w, w.unhealthyState(ws))
|
||||
continue
|
||||
}
|
||||
|
||||
// The time remaining until the Warnable will be visible to the user is the TimeToVisible
|
||||
// minus the time that has already passed since the Warnable became unhealthy.
|
||||
visibleIn := w.TimeToVisible - time.Since(brokenSince)
|
||||
mak.Set(&t.pendingVisibleTimers, w, time.AfterFunc(visibleIn, func() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
// Check if the Warnable is still unhealthy, as it could have become healthy between the time
|
||||
// the timer was set for and the time it was executed.
|
||||
if t.warnableVal[w] != nil {
|
||||
go cb(w, w.unhealthyState(ws))
|
||||
delete(t.pendingVisibleTimers, w)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,6 +388,13 @@ func (t *Tracker) setHealthyLocked(w *Warnable) {
|
||||
}
|
||||
|
||||
delete(t.warnableVal, w)
|
||||
|
||||
// Stop any pending visiblity timers for this Warnable
|
||||
if canc, ok := t.pendingVisibleTimers[w]; ok {
|
||||
canc.Stop()
|
||||
delete(t.pendingVisibleTimers, w)
|
||||
}
|
||||
|
||||
for _, cb := range t.watchers {
|
||||
go cb(w, nil)
|
||||
}
|
||||
@@ -705,7 +762,29 @@ func (t *Tracker) SetIPNState(state string, wantRunning bool) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.ipnState = state
|
||||
prevWantRunning := t.ipnWantRunning
|
||||
t.ipnWantRunning = wantRunning
|
||||
|
||||
if state == "Running" {
|
||||
// Any time we are told the backend is Running (control+DERP are connected), the Warnable
|
||||
// should be set to healthy, no matter if 5 seconds have passed or not.
|
||||
t.setHealthyLocked(warmingUpWarnable)
|
||||
} else if wantRunning && !prevWantRunning && t.ipnWantRunningLastTrue.IsZero() {
|
||||
// The first time we see wantRunning=true and it used to be false, it means the user requested
|
||||
// the backend to start. We store this timestamp and use it to silence some warnings that are
|
||||
// expected during startup.
|
||||
t.ipnWantRunningLastTrue = time.Now()
|
||||
t.setUnhealthyLocked(warmingUpWarnable, nil)
|
||||
time.AfterFunc(warmingUpWarnableDuration, func() {
|
||||
t.mu.Lock()
|
||||
t.updateWarmingUpWarnableLocked()
|
||||
t.mu.Unlock()
|
||||
})
|
||||
} else if !wantRunning {
|
||||
// Reset the timer when the user decides to stop the backend.
|
||||
t.ipnWantRunningLastTrue = time.Time{}
|
||||
}
|
||||
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
@@ -759,17 +838,20 @@ func (t *Tracker) SetLatestVersion(v *tailcfg.ClientVersion) {
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetCheckForUpdates sets whether the client wants to check for updates.
|
||||
func (t *Tracker) SetCheckForUpdates(v bool) {
|
||||
// SetAutoUpdatePrefs sets the client auto-update preferences. The arguments
|
||||
// match the fields of ipn.AutoUpdatePrefs, but we cannot pass that struct
|
||||
// directly due to a circular import.
|
||||
func (t *Tracker) SetAutoUpdatePrefs(check bool, apply opt.Bool) {
|
||||
if t.nil() {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.checkForUpdates == v {
|
||||
if t.checkForUpdates == check && t.applyUpdates == apply {
|
||||
return
|
||||
}
|
||||
t.checkForUpdates = v
|
||||
t.checkForUpdates = check
|
||||
t.applyUpdates = apply
|
||||
t.selfCheckLocked()
|
||||
}
|
||||
|
||||
@@ -825,6 +907,10 @@ func (t *Tracker) Strings() []string {
|
||||
func (t *Tracker) stringsLocked() []string {
|
||||
result := []string{}
|
||||
for w, ws := range t.warnableVal {
|
||||
if !w.IsVisible(ws) {
|
||||
// Do not append invisible warnings.
|
||||
continue
|
||||
}
|
||||
if ws.Args == nil {
|
||||
result = append(result, w.Text(Args{}))
|
||||
} else {
|
||||
@@ -858,20 +944,16 @@ var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
// updateBuiltinWarnablesLocked performs a number of checks on the state of the backend,
|
||||
// and adds/removes Warnings from the Tracker as needed.
|
||||
func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
if t.checkForUpdates {
|
||||
if cv := t.latestVersion; cv != nil && !cv.RunningLatest && cv.LatestVersion != "" {
|
||||
if cv.UrgentSecurityUpdate {
|
||||
t.setUnhealthyLocked(securityUpdateAvailableWarnable, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: cv.LatestVersion,
|
||||
})
|
||||
} else {
|
||||
t.setUnhealthyLocked(updateAvailableWarnable, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: cv.LatestVersion,
|
||||
})
|
||||
}
|
||||
}
|
||||
t.updateWarmingUpWarnableLocked()
|
||||
|
||||
if w, show := t.showUpdateWarnable(); show {
|
||||
t.setUnhealthyLocked(w, Args{
|
||||
ArgCurrentVersion: version.Short(),
|
||||
ArgAvailableVersion: t.latestVersion.LatestVersion,
|
||||
})
|
||||
} else {
|
||||
t.setHealthyLocked(updateAvailableWarnable)
|
||||
t.setHealthyLocked(securityUpdateAvailableWarnable)
|
||||
}
|
||||
|
||||
if version.IsUnstableBuild() {
|
||||
@@ -906,8 +988,12 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
}
|
||||
|
||||
if t.lastLoginErr != nil {
|
||||
var errMsg string
|
||||
if !errors.Is(t.lastLoginErr, context.Canceled) {
|
||||
errMsg = t.lastLoginErr.Error()
|
||||
}
|
||||
t.setUnhealthyLocked(LoginStateWarnable, Args{
|
||||
ArgError: t.lastLoginErr.Error(),
|
||||
ArgError: errMsg,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
@@ -1037,6 +1123,32 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
// updateWarmingUpWarnableLocked ensures the warmingUpWarnable is healthy if wantRunning has been set to true
|
||||
// for more than warmingUpWarnableDuration.
|
||||
func (t *Tracker) updateWarmingUpWarnableLocked() {
|
||||
if !t.ipnWantRunningLastTrue.IsZero() && time.Now().After(t.ipnWantRunningLastTrue.Add(warmingUpWarnableDuration)) {
|
||||
t.setHealthyLocked(warmingUpWarnable)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) showUpdateWarnable() (*Warnable, bool) {
|
||||
if !t.checkForUpdates {
|
||||
return nil, false
|
||||
}
|
||||
cv := t.latestVersion
|
||||
if cv == nil || cv.RunningLatest || cv.LatestVersion == "" {
|
||||
return nil, false
|
||||
}
|
||||
if cv.UrgentSecurityUpdate {
|
||||
return securityUpdateAvailableWarnable, true
|
||||
}
|
||||
// Only show update warning when auto-updates are off
|
||||
if !t.applyUpdates.EqualBool(true) {
|
||||
return updateAvailableWarnable, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
|
||||
@@ -6,8 +6,12 @@ package health
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
func TestAppendWarnableDebugFlags(t *testing.T) {
|
||||
@@ -158,6 +162,51 @@ func TestWatcher(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWatcherWithTimeToVisible tests that a registered watcher function gets called with the correct
|
||||
// Warnable and non-nil/nil UnhealthyState upon setting a Warnable to unhealthy/healthy, but the Warnable
|
||||
// has a TimeToVisible set, which means that a watcher should only be notified of an unhealthy state after
|
||||
// the TimeToVisible duration has passed.
|
||||
func TestSetUnhealthyWithTimeToVisible(t *testing.T) {
|
||||
ht := Tracker{}
|
||||
mw := Register(&Warnable{
|
||||
Code: "test-warnable-3-secs-to-visible",
|
||||
Title: "Test Warnable with 3 seconds to visible",
|
||||
Text: StaticMessage("Hello world"),
|
||||
TimeToVisible: 2 * time.Second,
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
defer unregister(mw)
|
||||
|
||||
becameUnhealthy := make(chan struct{})
|
||||
becameHealthy := make(chan struct{})
|
||||
|
||||
watchFunc := func(w *Warnable, us *UnhealthyState) {
|
||||
if w != mw {
|
||||
t.Fatalf("watcherFunc was called, but with an unexpected Warnable: %v, want: %v", w, w)
|
||||
}
|
||||
|
||||
if us != nil {
|
||||
becameUnhealthy <- struct{}{}
|
||||
} else {
|
||||
becameHealthy <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
ht.RegisterWatcher(watchFunc)
|
||||
ht.SetUnhealthy(mw, Args{ArgError: "Hello world"})
|
||||
|
||||
select {
|
||||
case <-becameUnhealthy:
|
||||
// Test failed because the watcher got notified of an unhealthy state
|
||||
t.Fatalf("watcherFunc was called with an unhealthy state")
|
||||
case <-becameHealthy:
|
||||
// Test failed because the watcher got of a healthy state
|
||||
t.Fatalf("watcherFunc was called with a healthy state")
|
||||
case <-time.After(1 * time.Second):
|
||||
// As expected, watcherFunc still had not been called after 1 second
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterWarnablePanicsWithDuplicate(t *testing.T) {
|
||||
w := &Warnable{
|
||||
Code: "test-warnable-1",
|
||||
@@ -199,15 +248,103 @@ func TestCheckDependsOnAppearsInUnhealthyState(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("Expected an UnhealthyState for w1, got nothing")
|
||||
}
|
||||
if len(us1.DependsOn) != 0 {
|
||||
t.Fatalf("Expected no DependsOn in the unhealthy state, got: %v", us1.DependsOn)
|
||||
wantDependsOn := []WarnableCode{warmingUpWarnable.Code}
|
||||
if !reflect.DeepEqual(us1.DependsOn, wantDependsOn) {
|
||||
t.Fatalf("Expected DependsOn = %v in the unhealthy state, got: %v", wantDependsOn, us1.DependsOn)
|
||||
}
|
||||
ht.SetUnhealthy(w2, Args{ArgError: "w2 is also unhealthy now"})
|
||||
us2, ok := ht.CurrentState().Warnings[w2.Code]
|
||||
if !ok {
|
||||
t.Fatalf("Expected an UnhealthyState for w2, got nothing")
|
||||
}
|
||||
if !reflect.DeepEqual(us2.DependsOn, []WarnableCode{w1.Code}) {
|
||||
t.Fatalf("Expected DependsOn = [w1.Code] in the unhealthy state, got: %v", us2.DependsOn)
|
||||
wantDependsOn = slices.Concat([]WarnableCode{w1.Code}, wantDependsOn)
|
||||
if !reflect.DeepEqual(us2.DependsOn, wantDependsOn) {
|
||||
t.Fatalf("Expected DependsOn = %v in the unhealthy state, got: %v", wantDependsOn, us2.DependsOn)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowUpdateWarnable(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
check bool
|
||||
apply opt.Bool
|
||||
cv *tailcfg.ClientVersion
|
||||
wantWarnable *Warnable
|
||||
wantShow bool
|
||||
}{
|
||||
{
|
||||
desc: "nil CientVersion",
|
||||
check: true,
|
||||
cv: nil,
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "RunningLatest",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: true},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "no LatestVersion",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: ""},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "show regular update",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: updateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
{
|
||||
desc: "show security update",
|
||||
check: true,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3", UrgentSecurityUpdate: true},
|
||||
wantWarnable: securityUpdateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
{
|
||||
desc: "update check disabled",
|
||||
check: false,
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "hide update with auto-updates",
|
||||
check: true,
|
||||
apply: opt.NewBool(true),
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3"},
|
||||
wantWarnable: nil,
|
||||
wantShow: false,
|
||||
},
|
||||
{
|
||||
desc: "show security update with auto-updates",
|
||||
check: true,
|
||||
apply: opt.NewBool(true),
|
||||
cv: &tailcfg.ClientVersion{RunningLatest: false, LatestVersion: "1.2.3", UrgentSecurityUpdate: true},
|
||||
wantWarnable: securityUpdateAvailableWarnable,
|
||||
wantShow: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
tr := &Tracker{
|
||||
checkForUpdates: tt.check,
|
||||
applyUpdates: tt.apply,
|
||||
latestVersion: tt.cv,
|
||||
}
|
||||
gotWarnable, gotShow := tr.showUpdateWarnable()
|
||||
if gotWarnable != tt.wantWarnable {
|
||||
t.Errorf("got warnable: %v, want: %v", gotWarnable, tt.wantWarnable)
|
||||
}
|
||||
if gotShow != tt.wantShow {
|
||||
t.Errorf("got show: %v, want: %v", gotShow, tt.wantShow)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,16 +20,17 @@ type State struct {
|
||||
Warnings map[WarnableCode]UnhealthyState
|
||||
}
|
||||
|
||||
// Representation contains information to be shown to the user to inform them
|
||||
// UnhealthyState contains information to be shown to the user to inform them
|
||||
// that a Warnable is currently unhealthy.
|
||||
type UnhealthyState struct {
|
||||
WarnableCode WarnableCode
|
||||
Severity Severity
|
||||
Title string
|
||||
Text string
|
||||
BrokenSince *time.Time `json:",omitempty"`
|
||||
Args Args `json:",omitempty"`
|
||||
DependsOn []WarnableCode `json:",omitempty"`
|
||||
WarnableCode WarnableCode
|
||||
Severity Severity
|
||||
Title string
|
||||
Text string
|
||||
BrokenSince *time.Time `json:",omitempty"`
|
||||
Args Args `json:",omitempty"`
|
||||
DependsOn []WarnableCode `json:",omitempty"`
|
||||
ImpactsConnectivity bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
|
||||
@@ -41,19 +42,27 @@ func (w *Warnable) unhealthyState(ws *warningState) *UnhealthyState {
|
||||
text = w.Text(Args{})
|
||||
}
|
||||
|
||||
dependsOnWarnableCodes := make([]WarnableCode, len(w.DependsOn))
|
||||
dependsOnWarnableCodes := make([]WarnableCode, len(w.DependsOn), len(w.DependsOn)+1)
|
||||
for i, d := range w.DependsOn {
|
||||
dependsOnWarnableCodes[i] = d.Code
|
||||
}
|
||||
|
||||
if w != warmingUpWarnable {
|
||||
// Here we tell the frontend that all Warnables depend on warmingUpWarnable. GUIs will silence all warnings until all
|
||||
// their dependencies are healthy. This is a special case to prevent the GUI from showing a bunch of warnings when
|
||||
// the backend is still warming up.
|
||||
dependsOnWarnableCodes = append(dependsOnWarnableCodes, warmingUpWarnable.Code)
|
||||
}
|
||||
|
||||
return &UnhealthyState{
|
||||
WarnableCode: w.Code,
|
||||
Severity: w.Severity,
|
||||
Title: w.Title,
|
||||
Text: text,
|
||||
BrokenSince: &ws.BrokenSince,
|
||||
Args: ws.Args,
|
||||
DependsOn: dependsOnWarnableCodes,
|
||||
WarnableCode: w.Code,
|
||||
Severity: w.Severity,
|
||||
Title: w.Title,
|
||||
Text: text,
|
||||
BrokenSince: &ws.BrokenSince,
|
||||
Args: ws.Args,
|
||||
DependsOn: dependsOnWarnableCodes,
|
||||
ImpactsConnectivity: w.ImpactsConnectivity,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +86,10 @@ func (t *Tracker) CurrentState() *State {
|
||||
wm := map[WarnableCode]UnhealthyState{}
|
||||
|
||||
for w, ws := range t.warnableVal {
|
||||
if !w.IsVisible(ws) {
|
||||
// Skip invisible Warnables.
|
||||
continue
|
||||
}
|
||||
wm[w.Code] = *w.unhealthyState(ws)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,10 @@ package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -17,7 +21,11 @@ var updateAvailableWarnable = Register(&Warnable{
|
||||
Title: "Update available",
|
||||
Severity: SeverityLow,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("An update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -25,9 +33,13 @@ var updateAvailableWarnable = Register(&Warnable{
|
||||
var securityUpdateAvailableWarnable = Register(&Warnable{
|
||||
Code: "security-update-available",
|
||||
Title: "Security update available",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("An urgent security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
if version.IsMacAppStore() || version.IsAppleTV() || version.IsMacSys() || version.IsWindowsGUI() || runtime.GOOS == "android" {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
} else {
|
||||
return fmt.Sprintf("A security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -37,16 +49,17 @@ var unstableWarnable = Register(&Warnable{
|
||||
Code: "is-using-unstable-version",
|
||||
Title: "Using an unstable version",
|
||||
Severity: SeverityLow,
|
||||
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes: please report any bugs to Tailscale."),
|
||||
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes. Please report any issues to Tailscale."),
|
||||
})
|
||||
|
||||
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
|
||||
var NetworkStatusWarnable = Register(&Warnable{
|
||||
Code: "network-status",
|
||||
Title: "Network down",
|
||||
Severity: SeverityHigh,
|
||||
Text: StaticMessage("Tailscale cannot connect because the network is down. (No network interface is up.)"),
|
||||
Severity: SeverityMedium,
|
||||
Text: StaticMessage("Tailscale cannot connect because the network is down. Check your Internet connection."),
|
||||
ImpactsConnectivity: true,
|
||||
TimeToVisible: 5 * time.Second,
|
||||
})
|
||||
|
||||
// IPNStateWarnable is a Warnable that warns the user that Tailscale is stopped.
|
||||
@@ -82,29 +95,33 @@ var LoginStateWarnable = Register(&Warnable{
|
||||
},
|
||||
})
|
||||
|
||||
// notInMapPollWarnable is a Warnable that warns the user that they cannot connect to the control server.
|
||||
// notInMapPollWarnable is a Warnable that warns the user that we are using a stale network map.
|
||||
var notInMapPollWarnable = Register(&Warnable{
|
||||
Code: "not-in-map-poll",
|
||||
Title: "Cannot connect to control server",
|
||||
Title: "Out of sync",
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Cannot connect to the control server (not in map poll). Check your Internet connection."),
|
||||
Text: StaticMessage("Unable to connect to the Tailscale coordination server to synchronize the state of your tailnet. Peer reachability might degrade over time."),
|
||||
// 8 minutes reflects a maximum maintenance window for the coordination server.
|
||||
TimeToVisible: 8 * time.Minute,
|
||||
})
|
||||
|
||||
// noDERPHomeWarnable is a Warnable that warns the user that Tailscale doesn't have a home DERP.
|
||||
var noDERPHomeWarnable = Register(&Warnable{
|
||||
Code: "no-derp-home",
|
||||
Title: "No home relay server",
|
||||
Severity: SeverityHigh,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
|
||||
Code: "no-derp-home",
|
||||
Title: "No home relay server",
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
|
||||
ImpactsConnectivity: true,
|
||||
TimeToVisible: 10 * time.Second,
|
||||
})
|
||||
|
||||
// noDERPConnectionWarnable is a Warnable that warns the user that Tailscale couldn't connect to a specific DERP server.
|
||||
var noDERPConnectionWarnable = Register(&Warnable{
|
||||
Code: "no-derp-connection",
|
||||
Title: "Relay server unavailable",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: func(args Args) string {
|
||||
if n := args[ArgDERPRegionName]; n != "" {
|
||||
@@ -113,6 +130,8 @@ var noDERPConnectionWarnable = Register(&Warnable{
|
||||
return fmt.Sprintf("Tailscale could not connect to the relay server with ID '%s'. Your Internet connection might be down, or the server might be temporarily unavailable.", args[ArgDERPRegionID])
|
||||
}
|
||||
},
|
||||
ImpactsConnectivity: true,
|
||||
TimeToVisible: 10 * time.Second,
|
||||
})
|
||||
|
||||
// derpTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't heard from the home DERP region for a while.
|
||||
@@ -134,7 +153,7 @@ var derpTimeoutWarnable = Register(&Warnable{
|
||||
var derpRegionErrorWarnable = Register(&Warnable{
|
||||
Code: "derp-region-error",
|
||||
Title: "Relay server error",
|
||||
Severity: SeverityMedium,
|
||||
Severity: SeverityLow,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: func(args Args) string {
|
||||
return fmt.Sprintf("The relay server #%v is reporting an issue: %v", args[ArgDERPRegionID], args[ArgError])
|
||||
@@ -145,7 +164,7 @@ var derpRegionErrorWarnable = Register(&Warnable{
|
||||
var noUDP4BindWarnable = Register(&Warnable{
|
||||
Code: "no-udp4-bind",
|
||||
Title: "Incoming connections may fail",
|
||||
Severity: SeverityHigh,
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
Text: StaticMessage("Tailscale couldn't listen for incoming UDP connections."),
|
||||
ImpactsConnectivity: true,
|
||||
@@ -212,3 +231,17 @@ var controlHealthWarnable = Register(&Warnable{
|
||||
return fmt.Sprintf("The coordination server is reporting an health issue: %v", args[ArgError])
|
||||
},
|
||||
})
|
||||
|
||||
// warmingUpWarnableDuration is the duration for which the warmingUpWarnable is reported by the backend after the user
|
||||
// has changed ipnWantRunning to true from false.
|
||||
const warmingUpWarnableDuration = 5 * time.Second
|
||||
|
||||
// warmingUpWarnable is a Warnable that is reported by the backend when it is starting up, for a maximum time of
|
||||
// warmingUpWarnableDuration. The GUIs use the presence of this Warnable to prevent showing any other warnings until
|
||||
// the backend is fully started.
|
||||
var warmingUpWarnable = Register(&Warnable{
|
||||
Code: "warming-up",
|
||||
Title: "Tailscale is starting",
|
||||
Severity: SeverityLow,
|
||||
Text: StaticMessage("Tailscale is starting. Please wait."),
|
||||
})
|
||||
|
||||
@@ -159,7 +159,14 @@ func linuxVersionMeta() (meta versionMeta) {
|
||||
return
|
||||
}
|
||||
|
||||
// linuxBuildTagPackageType is set by packagetype_*.go
|
||||
// build tag guarded files.
|
||||
var linuxBuildTagPackageType string
|
||||
|
||||
func packageTypeLinux() string {
|
||||
if v := linuxBuildTagPackageType; v != "" {
|
||||
return v
|
||||
}
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
|
||||
10
hostinfo/packagetype_container.go
Normal file
10
hostinfo/packagetype_container.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux && ts_package_container
|
||||
|
||||
package hostinfo
|
||||
|
||||
func init() {
|
||||
linuxBuildTagPackageType = "container"
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
//go:build for_go_mod_tidy_only
|
||||
|
||||
// Package tooldeps contains dependencies for tools used in the Tailscale repository,
|
||||
// so they're not removed by "go mod tidy".
|
||||
package tooldeps
|
||||
|
||||
import (
|
||||
|
||||
@@ -42,6 +42,10 @@ type ConfigVAlpha struct {
|
||||
AutoUpdate *AutoUpdatePrefs `json:",omitempty"`
|
||||
ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this
|
||||
|
||||
// StaticEndpoints are additional, user-defined endpoints that this node
|
||||
// should advertise amongst its wireguard endpoints.
|
||||
StaticEndpoints []netip.AddrPort `json:",omitempty"`
|
||||
|
||||
// TODO(bradfitz,maisem): future something like:
|
||||
// Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
@@ -29,7 +30,11 @@ func (src *Prefs) Clone() *Prefs {
|
||||
if src.DriveShares != nil {
|
||||
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
|
||||
for i := range dst.DriveShares {
|
||||
dst.DriveShares[i] = src.DriveShares[i].Clone()
|
||||
if src.DriveShares[i] == nil {
|
||||
dst.DriveShares[i] = nil
|
||||
} else {
|
||||
dst.DriveShares[i] = src.DriveShares[i].Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.Persist = src.Persist.Clone()
|
||||
@@ -81,20 +86,32 @@ func (src *ServeConfig) Clone() *ServeConfig {
|
||||
if dst.TCP != nil {
|
||||
dst.TCP = map[uint16]*TCPPortHandler{}
|
||||
for k, v := range src.TCP {
|
||||
dst.TCP[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.TCP[k] = nil
|
||||
} else {
|
||||
dst.TCP[k] = ptr.To(*v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if dst.Web != nil {
|
||||
dst.Web = map[HostPort]*WebServerConfig{}
|
||||
for k, v := range src.Web {
|
||||
dst.Web[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.Web[k] = nil
|
||||
} else {
|
||||
dst.Web[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.AllowFunnel = maps.Clone(src.AllowFunnel)
|
||||
if dst.Foreground != nil {
|
||||
dst.Foreground = map[string]*ServeConfig{}
|
||||
for k, v := range src.Foreground {
|
||||
dst.Foreground[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.Foreground[k] = nil
|
||||
} else {
|
||||
dst.Foreground[k] = v.Clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
@@ -157,7 +174,11 @@ func (src *WebServerConfig) Clone() *WebServerConfig {
|
||||
if dst.Handlers != nil {
|
||||
dst.Handlers = map[string]*HTTPHandler{}
|
||||
for k, v := range src.Handlers {
|
||||
dst.Handlers[k] = v.Clone()
|
||||
if v == nil {
|
||||
dst.Handlers[k] = nil
|
||||
} else {
|
||||
dst.Handlers[k] = ptr.To(*v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) stopOfflineAutoUpdate() {
|
||||
@@ -30,6 +31,10 @@ func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return
|
||||
}
|
||||
// On macsys, auto-updates are managed by Sparkle.
|
||||
if version.IsMacSysExt() {
|
||||
return
|
||||
}
|
||||
|
||||
if b.offlineAutoUpdateCancel != nil {
|
||||
// Already running.
|
||||
|
||||
@@ -318,7 +318,7 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
|
||||
res := tailcfg.C2NPostureIdentityResponse{}
|
||||
|
||||
// Only collect serial numbers if enabled on the client,
|
||||
// Only collect posture identity if enabled on the client,
|
||||
// this will first check syspolicy, MDM settings like Registry
|
||||
// on Windows or defaults on macOS. If they are not set, it falls
|
||||
// back to the cli-flag, `--posture-checking`.
|
||||
@@ -337,8 +337,17 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.SerialNumbers = sns
|
||||
|
||||
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
|
||||
// and looks good in client metrics, remove this parameter and always report MAC
|
||||
// addresses.
|
||||
if r.FormValue("hwaddrs") == "true" {
|
||||
res.IfaceHardwareAddrs, err = posture.GetHardwareAddrs()
|
||||
if err != nil {
|
||||
b.logf("c2n: GetHardwareAddrs returned error: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
@@ -355,7 +364,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
prefs := b.Prefs().AutoUpdate()
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
|
||||
Supported: clientupdate.CanAutoUpdate(),
|
||||
Supported: clientupdate.CanAutoUpdate() && !version.IsMacSysExt(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,9 +450,13 @@ func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
|
||||
// tailscaled is restarted during the update, systemd won't kill this
|
||||
// temporary update unit, which could cause unexpected breakage.
|
||||
//
|
||||
// We want to use the --wait flag for systemd-run, to block the update
|
||||
// command until completion and collect output. But this flag was added in
|
||||
// systemd 232, so we need to check the version first.
|
||||
// We want to use a few optional flags:
|
||||
// * --wait, to block the update command until completion (added in systemd 232)
|
||||
// * --pipe, to collect stdout/stderr (added in systemd 235)
|
||||
// * --collect, to clean up failed runs from memory (added in systemd 236)
|
||||
//
|
||||
// We need to check the version of systemd to figure out if those flags are
|
||||
// available.
|
||||
//
|
||||
// The output will look like:
|
||||
//
|
||||
@@ -461,10 +474,14 @@ func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
|
||||
if err != nil {
|
||||
return defaultCmd
|
||||
}
|
||||
if systemdVer < 232 {
|
||||
return exec.Command("systemd-run", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
} else {
|
||||
if systemdVer >= 236 {
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
} else if systemdVer >= 235 {
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", cmdTS, "update", "--yes")
|
||||
} else if systemdVer >= 232 {
|
||||
return exec.Command("systemd-run", "--wait", cmdTS, "update", "--yes")
|
||||
} else {
|
||||
return exec.Command("systemd-run", cmdTS, "update", "--yes")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,17 @@ var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
// If a cert is expired, it will be renewed synchronously otherwise it will be
|
||||
// renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
return b.GetCertPEMWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// GetCertPEMWithValidity gets the TLSCertKeyPair for domain, either from cache
|
||||
// or via the ACME process. ACME process is used for new domain certs, existing
|
||||
// expired certs or existing certs that should get renewed sooner than
|
||||
// minValidity.
|
||||
//
|
||||
// If a cert is expired, or expires sooner than minValidity, it will be renewed
|
||||
// synchronously. Otherwise it will be renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
@@ -109,17 +120,28 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
if shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair); err != nil {
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if shouldRenew {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
|
||||
// Renewal check failed, but the current cert is valid and not
|
||||
// expired, so it's safe to return.
|
||||
return pair, nil
|
||||
}
|
||||
return pair, nil
|
||||
if !shouldRenew {
|
||||
return pair, nil
|
||||
}
|
||||
if minValidity == 0 {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background, return current valid cert.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
|
||||
return pair, nil
|
||||
}
|
||||
// If the caller requested a specific validity duration, fall through
|
||||
// to synchronous renewal to fulfill that.
|
||||
logf("starting sync renewal")
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
@@ -129,7 +151,14 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
|
||||
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
|
||||
// based on the current time, the cert's expiry, and the ARI check.
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair, minValidity time.Duration) (bool, error) {
|
||||
if minValidity != 0 {
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
return cert.NotAfter.Sub(now) < minValidity, nil
|
||||
}
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
if renewAt, ok := renewCertAt[domain]; ok {
|
||||
@@ -157,11 +186,7 @@ func (b *LocalBackend) domainRenewed(domain string) {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) domainRenewalTimeByExpiry(pair *TLSCertKeyPair) (time.Time, error) {
|
||||
block, _ := pem.Decode(pair.CertPEM)
|
||||
if block == nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
@@ -366,6 +391,17 @@ type TLSCertKeyPair struct {
|
||||
Cached bool // whether result came from cache
|
||||
}
|
||||
|
||||
func (kp TLSCertKeyPair) parseCertificate() (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode(kp.CertPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("error parsing certificate PEM")
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("PEM block is %q, not a CERTIFICATE", block.Type)
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
@@ -383,7 +419,7 @@ func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKey
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
@@ -393,7 +429,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
if p, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// shouldStartDomainRenewal caches its result so it's OK to call this
|
||||
// frequently.
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p)
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if !shouldRenew {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ipnlocal is the heart of the Tailscale node agent that controls
|
||||
// all the other misc pieces of the Tailscale node.
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
@@ -22,8 +24,8 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -58,6 +60,7 @@ import (
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/log/sockstatlog"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/captivedetection"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
@@ -96,6 +99,7 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/util/rands"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
@@ -338,6 +342,24 @@ type LocalBackend struct {
|
||||
// lastSuggestedExitNode stores the last suggested exit node suggestion to
|
||||
// avoid unnecessary churn between multiple equally-good options.
|
||||
lastSuggestedExitNode tailcfg.StableNodeID
|
||||
|
||||
// refreshAutoExitNode indicates if the exit node should be recomputed when the next netcheck report is available.
|
||||
refreshAutoExitNode bool
|
||||
|
||||
// captiveCtx and captiveCancel are used to control captive portal
|
||||
// detection. They are protected by 'mu' and can be changed during the
|
||||
// lifetime of a LocalBackend.
|
||||
//
|
||||
// captiveCtx will always be non-nil, though it might be a canceled
|
||||
// context. captiveCancel is non-nil if checkCaptivePortalLoop is
|
||||
// running, and is set to nil after being canceled.
|
||||
captiveCtx context.Context
|
||||
captiveCancel context.CancelFunc
|
||||
// needsCaptiveDetection is a channel that is used to signal either
|
||||
// that captive portal detection is required (sending true) or that the
|
||||
// backend is healthy and captive portal detection is not required
|
||||
// (sending false).
|
||||
needsCaptiveDetection chan bool
|
||||
}
|
||||
|
||||
// HealthTracker returns the health tracker for the backend.
|
||||
@@ -386,49 +408,50 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
sds.SetDialer(dialer.SystemDial)
|
||||
}
|
||||
|
||||
if sys.InitialConfig != nil {
|
||||
p := pm.CurrentPrefs().AsStruct()
|
||||
mp, err := sys.InitialConfig.Parsed.ToPrefs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.ApplyEdits(&mp)
|
||||
if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
envknob.LogCurrent(logf)
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
clock := tstime.StdClock{}
|
||||
|
||||
// Until we transition to a Running state, use a canceled context for
|
||||
// our captive portal detection.
|
||||
captiveCtx, captiveCancel := context.WithCancel(ctx)
|
||||
captiveCancel()
|
||||
|
||||
b := &LocalBackend{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logf: logf,
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
sys: sys,
|
||||
health: sys.HealthTracker(),
|
||||
conf: sys.InitialConfig,
|
||||
e: e,
|
||||
dialer: dialer,
|
||||
store: store,
|
||||
pm: pm,
|
||||
backendLogID: logID,
|
||||
state: ipn.NoState,
|
||||
portpoll: new(portlist.Poller),
|
||||
em: newExpiryManager(logf),
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
clock: clock,
|
||||
selfUpdateProgress: make([]ipnstate.UpdateProgress, 0),
|
||||
lastSelfUpdateState: ipnstate.UpdateFinished,
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logf: logf,
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
sys: sys,
|
||||
health: sys.HealthTracker(),
|
||||
e: e,
|
||||
dialer: dialer,
|
||||
store: store,
|
||||
pm: pm,
|
||||
backendLogID: logID,
|
||||
state: ipn.NoState,
|
||||
portpoll: new(portlist.Poller),
|
||||
em: newExpiryManager(logf),
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
clock: clock,
|
||||
selfUpdateProgress: make([]ipnstate.UpdateProgress, 0),
|
||||
lastSelfUpdateState: ipnstate.UpdateFinished,
|
||||
captiveCtx: captiveCtx,
|
||||
captiveCancel: nil, // so that we start checkCaptivePortalLoop when Running
|
||||
needsCaptiveDetection: make(chan bool),
|
||||
}
|
||||
mConn.SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
if sys.InitialConfig != nil {
|
||||
if err := b.setConfigLocked(sys.InitialConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
netMon := sys.NetMon.Get()
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon, sys.HealthTracker())
|
||||
if err != nil {
|
||||
@@ -611,11 +634,50 @@ func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
b.conf = conf
|
||||
// TODO(bradfitz): apply things
|
||||
if err := b.setConfigLocked(conf); err != nil {
|
||||
return false, fmt.Errorf("error setting config: %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error {
|
||||
|
||||
// TODO(irbekrm): notify the relevant components to consume any prefs
|
||||
// updates. Currently only initial configfile settings are applied
|
||||
// immediately.
|
||||
p := b.pm.CurrentPrefs().AsStruct()
|
||||
mp, err := conf.Parsed.ToPrefs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing config to prefs: %w", err)
|
||||
}
|
||||
p.ApplyEdits(&mp)
|
||||
if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
b.conf = conf
|
||||
}()
|
||||
|
||||
if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ensure that magicsock conn has the up to date static wireguard
|
||||
// endpoints. Setting the endpoints here triggers an asynchronous update
|
||||
// of the node's advertised endpoints.
|
||||
if b.conf == nil && len(conf.Parsed.StaticEndpoints) != 0 || !reflect.DeepEqual(conf.Parsed.StaticEndpoints, b.conf.Parsed.StaticEndpoints) {
|
||||
ms, ok := b.sys.MagicSock.GetOK()
|
||||
if !ok {
|
||||
b.logf("[unexpected] ReloadConfig: MagicSock not set")
|
||||
} else {
|
||||
ms.SetStaticEndpoints(views.SliceOf(conf.Parsed.StaticEndpoints))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var assumeNetworkUpdateForTest = envknob.RegisterBool("TS_ASSUME_NETWORK_UP_FOR_TEST")
|
||||
|
||||
// pauseOrResumeControlClientLocked pauses b.cc if there is no network available
|
||||
@@ -631,6 +693,10 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
|
||||
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
|
||||
}
|
||||
|
||||
// captivePortalDetectionInterval is the duration to wait in an unhealthy state with connectivity broken
|
||||
// before running captive portal detection.
|
||||
const captivePortalDetectionInterval = 2 * time.Second
|
||||
|
||||
// linkChange is our network monitor callback, called whenever the network changes.
|
||||
func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
||||
b.mu.Lock()
|
||||
@@ -640,7 +706,9 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
||||
hadPAC := b.prevIfState.HasPAC()
|
||||
b.prevIfState = ifst
|
||||
b.pauseOrResumeControlClientLocked()
|
||||
|
||||
if delta.Major && shouldAutoExitNode() {
|
||||
b.refreshAutoExitNode = true
|
||||
}
|
||||
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
||||
// add/remove subnets.
|
||||
if hadPAC != ifst.HasPAC() {
|
||||
@@ -679,6 +747,47 @@ func (b *LocalBackend) onHealthChange(w *health.Warnable, us *health.UnhealthySt
|
||||
b.send(ipn.Notify{
|
||||
Health: state,
|
||||
})
|
||||
|
||||
isConnectivityImpacted := false
|
||||
for _, w := range state.Warnings {
|
||||
// Ignore the captive portal warnable itself.
|
||||
if w.ImpactsConnectivity && w.WarnableCode != captivePortalWarnable.Code {
|
||||
isConnectivityImpacted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// captiveCtx can be changed, and is protected with 'mu'; grab that
|
||||
// before we start our select, below.
|
||||
//
|
||||
// It is guaranteed to be non-nil.
|
||||
b.mu.Lock()
|
||||
ctx := b.captiveCtx
|
||||
b.mu.Unlock()
|
||||
|
||||
// If the context is canceled, we don't need to do anything.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if isConnectivityImpacted {
|
||||
b.logf("health: connectivity impacted; triggering captive portal detection")
|
||||
|
||||
// Ensure that we select on captiveCtx so that we can time out
|
||||
// triggering captive portal detection if the backend is shutdown.
|
||||
select {
|
||||
case b.needsCaptiveDetection <- true:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
} else {
|
||||
// If connectivity is not impacted, we know for sure we're not behind a captive portal,
|
||||
// so drop any warning, and signal that we don't need captive portal detection.
|
||||
b.health.SetHealthy(captivePortalWarnable)
|
||||
select {
|
||||
case b.needsCaptiveDetection <- false:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown halts the backend and all its sub-components. The backend
|
||||
@@ -691,6 +800,11 @@ func (b *LocalBackend) Shutdown() {
|
||||
}
|
||||
b.shutdownCalled = true
|
||||
|
||||
if b.captiveCancel != nil {
|
||||
b.logf("canceling captive portal context")
|
||||
b.captiveCancel()
|
||||
}
|
||||
|
||||
if b.loginFlags&controlclient.LoginEphemeral != 0 {
|
||||
b.mu.Unlock()
|
||||
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
||||
@@ -723,7 +837,9 @@ func (b *LocalBackend) Shutdown() {
|
||||
b.webClientShutdown()
|
||||
|
||||
if b.sockstatLogger != nil {
|
||||
b.sockstatLogger.Shutdown()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
b.sockstatLogger.Shutdown(ctx)
|
||||
}
|
||||
if b.peerAPIServer != nil {
|
||||
b.peerAPIServer.taildrop.Shutdown()
|
||||
@@ -1184,7 +1300,13 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
|
||||
prefsChanged := false
|
||||
prefs := b.pm.CurrentPrefs().AsStruct()
|
||||
netMap := b.netMap
|
||||
oldNetMap := b.netMap
|
||||
curNetMap := st.NetMap
|
||||
if curNetMap == nil {
|
||||
// The status didn't include a netmap update, so the old one is still
|
||||
// current.
|
||||
curNetMap = oldNetMap
|
||||
}
|
||||
|
||||
if prefs.ControlURL == "" {
|
||||
// Once we get a message from the control plane, set
|
||||
@@ -1215,7 +1337,14 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
prefs.WantRunning = true
|
||||
prefs.LoggedOut = false
|
||||
}
|
||||
if setExitNodeID(prefs, st.NetMap) {
|
||||
if shouldAutoExitNode() {
|
||||
// Re-evaluate exit node suggestion in case circumstances have changed.
|
||||
_, err := b.suggestExitNodeLocked(curNetMap)
|
||||
if err != nil && !errors.Is(err, ErrNoPreferredDERP) {
|
||||
b.logf("SetControlClientStatus failed to select auto exit node: %v", err)
|
||||
}
|
||||
}
|
||||
if setExitNodeID(prefs, curNetMap, b.lastSuggestedExitNode) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if applySysPolicy(prefs) {
|
||||
@@ -1232,8 +1361,8 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
if prefsChanged {
|
||||
// Prefs will be written out if stale; this is not safe unless locked or cloned.
|
||||
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
|
||||
MagicDNSName: st.NetMap.MagicDNSSuffix(),
|
||||
DomainName: st.NetMap.DomainName(),
|
||||
MagicDNSName: curNetMap.MagicDNSSuffix(),
|
||||
DomainName: curNetMap.DomainName(),
|
||||
}); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
@@ -1300,8 +1429,8 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
b.send(ipn.Notify{ErrMessage: &msg, Prefs: &p})
|
||||
return
|
||||
}
|
||||
if netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||
if oldNetMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(oldNetMap)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
b.logf("[v1] netmap diff: (none)")
|
||||
} else {
|
||||
@@ -1418,9 +1547,8 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
b.send(*notify)
|
||||
}
|
||||
}()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
if !b.updateNetmapDeltaLocked(muts) {
|
||||
return false
|
||||
}
|
||||
@@ -1428,8 +1556,14 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo
|
||||
if b.netMap != nil && mutationsAreWorthyOfTellingIPNBus(muts) {
|
||||
nm := ptr.To(*b.netMap) // shallow clone
|
||||
nm.Peers = make([]tailcfg.NodeView, 0, len(b.peers))
|
||||
shouldAutoExitNode := shouldAutoExitNode()
|
||||
for _, p := range b.peers {
|
||||
nm.Peers = append(nm.Peers, p)
|
||||
// If the auto exit node currently set goes offline, find another auto exit node.
|
||||
if shouldAutoExitNode && b.pm.prefs.ExitNodeID() == p.StableID() && p.Online() != nil && !*p.Online() {
|
||||
b.setAutoExitNodeIDLockedOnEntry(unlock)
|
||||
return false
|
||||
}
|
||||
}
|
||||
slices.SortFunc(nm.Peers, func(a, b tailcfg.NodeView) int {
|
||||
return cmp.Compare(a.ID(), b.ID())
|
||||
@@ -1491,9 +1625,14 @@ func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (hand
|
||||
|
||||
// setExitNodeID updates prefs to reference an exit node by ID, rather
|
||||
// than by IP. It returns whether prefs was mutated.
|
||||
func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap, lastSuggestedExitNode tailcfg.StableNodeID) (prefsChanged bool) {
|
||||
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
|
||||
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
|
||||
if shouldAutoExitNode() && lastSuggestedExitNode != "" {
|
||||
exitNodeID = lastSuggestedExitNode
|
||||
}
|
||||
// Note: when exitNodeIDStr == "auto" && lastSuggestedExitNode == "", then exitNodeID is now "auto" which will never match a peer's node ID.
|
||||
// When there is no a peer matching the node ID, traffic will blackhole, preventing accidental non-exit-node usage when a policy is in effect that requires an exit node.
|
||||
changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
|
||||
prefs.ExitNodeID = exitNodeID
|
||||
prefs.ExitNodeIP = netip.Addr{}
|
||||
@@ -2032,6 +2171,122 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
}
|
||||
}
|
||||
|
||||
// captivePortalWarnable is a Warnable which is set to an unhealthy state when a captive portal is detected.
|
||||
var captivePortalWarnable = health.Register(&health.Warnable{
|
||||
Code: "captive-portal-detected",
|
||||
Title: "Captive portal detected",
|
||||
// High severity, because captive portals block all traffic and require user intervention.
|
||||
Severity: health.SeverityHigh,
|
||||
Text: health.StaticMessage("This network requires you to log in using your web browser."),
|
||||
ImpactsConnectivity: true,
|
||||
})
|
||||
|
||||
func (b *LocalBackend) checkCaptivePortalLoop(ctx context.Context) {
|
||||
var tmr *time.Timer
|
||||
|
||||
maybeStartTimer := func() {
|
||||
// If there's an existing timer, nothing to do; just continue
|
||||
// waiting for it to expire. Otherwise, create a new timer.
|
||||
if tmr == nil {
|
||||
tmr = time.NewTimer(captivePortalDetectionInterval)
|
||||
}
|
||||
}
|
||||
maybeStopTimer := func() {
|
||||
if tmr == nil {
|
||||
return
|
||||
}
|
||||
if !tmr.Stop() {
|
||||
<-tmr.C
|
||||
}
|
||||
tmr = nil
|
||||
}
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
maybeStopTimer()
|
||||
return
|
||||
}
|
||||
|
||||
// First, see if we have a signal on our "healthy" channel, which
|
||||
// takes priority over an existing timer. Because a select is
|
||||
// nondeterministic, we explicitly check this channel before
|
||||
// entering the main select below, so that we're guaranteed to
|
||||
// stop the timer before starting captive portal detection.
|
||||
select {
|
||||
case needsCaptiveDetection := <-b.needsCaptiveDetection:
|
||||
if needsCaptiveDetection {
|
||||
maybeStartTimer()
|
||||
} else {
|
||||
maybeStopTimer()
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
var timerChan <-chan time.Time
|
||||
if tmr != nil {
|
||||
timerChan = tmr.C
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// All done; stop the timer and then exit.
|
||||
maybeStopTimer()
|
||||
return
|
||||
case <-timerChan:
|
||||
// Kick off captive portal check
|
||||
b.performCaptiveDetection()
|
||||
// nil out timer to force recreation
|
||||
tmr = nil
|
||||
case needsCaptiveDetection := <-b.needsCaptiveDetection:
|
||||
if needsCaptiveDetection {
|
||||
maybeStartTimer()
|
||||
} else {
|
||||
// Healthy; cancel any existing timer
|
||||
maybeStopTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// performCaptiveDetection checks if captive portal detection is enabled via controlknob. If so, it runs
|
||||
// the detection and updates the Warnable accordingly.
|
||||
func (b *LocalBackend) performCaptiveDetection() {
|
||||
if !b.shouldRunCaptivePortalDetection() {
|
||||
return
|
||||
}
|
||||
|
||||
d := captivedetection.NewDetector(b.logf)
|
||||
var dm *tailcfg.DERPMap
|
||||
b.mu.Lock()
|
||||
if b.netMap != nil {
|
||||
dm = b.netMap.DERPMap
|
||||
}
|
||||
preferredDERP := 0
|
||||
if b.hostinfo != nil {
|
||||
if b.hostinfo.NetInfo != nil {
|
||||
preferredDERP = b.hostinfo.NetInfo.PreferredDERP
|
||||
}
|
||||
}
|
||||
ctx := b.ctx
|
||||
netMon := b.NetMon()
|
||||
b.mu.Unlock()
|
||||
found := d.Detect(ctx, netMon, dm, preferredDERP)
|
||||
if found {
|
||||
b.health.SetUnhealthy(captivePortalWarnable, health.Args{})
|
||||
} else {
|
||||
b.health.SetHealthy(captivePortalWarnable)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldRunCaptivePortalDetection reports whether captive portal detection
|
||||
// should be run. It is enabled by default, but can be disabled via a control
|
||||
// knob. It is also only run when the user explicitly wants the backend to be
|
||||
// running.
|
||||
func (b *LocalBackend) shouldRunCaptivePortalDetection() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return !b.ControlKnobs().DisableCaptivePortalDetection.Load() && b.pm.prefs.WantRunning()
|
||||
}
|
||||
|
||||
// packetFilterPermitsUnlockedNodes reports any peer in peers with the
|
||||
// UnsignedPeerAPIOnly bool set true has any of its allowed IPs in the packet
|
||||
// filter.
|
||||
@@ -3357,7 +3612,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
||||
// setExitNodeID returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
setExitNodeID(newp, netMap)
|
||||
setExitNodeID(newp, netMap, b.lastSuggestedExitNode)
|
||||
// applySysPolicy does likewise so we can also ignore its return value.
|
||||
applySysPolicy(newp)
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
@@ -4425,9 +4680,27 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
|
||||
if newState == ipn.Running {
|
||||
b.authURL = ""
|
||||
b.authURLTime = time.Time{}
|
||||
|
||||
// Start a captive portal detection loop if none has been
|
||||
// started. Create a new context if none is present, since it
|
||||
// can be shut down if we transition away from Running.
|
||||
if b.captiveCancel == nil {
|
||||
b.captiveCtx, b.captiveCancel = context.WithCancel(b.ctx)
|
||||
go b.checkCaptivePortalLoop(b.captiveCtx)
|
||||
}
|
||||
} else if oldState == ipn.Running {
|
||||
// Transitioning away from running.
|
||||
b.closePeerAPIListenersLocked()
|
||||
|
||||
// Stop any existing captive portal detection loop.
|
||||
if b.captiveCancel != nil {
|
||||
b.captiveCancel()
|
||||
b.captiveCancel = nil
|
||||
|
||||
// NOTE: don't set captiveCtx to nil here, to ensure
|
||||
// that we always have a (canceled) context to wait on
|
||||
// in onHealthChange.
|
||||
}
|
||||
}
|
||||
b.pauseOrResumeControlClientLocked()
|
||||
|
||||
@@ -4850,12 +5123,68 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
var refresh bool
|
||||
if b.MagicConn().DERPs() > 0 || testenv.InTest() {
|
||||
// When b.refreshAutoExitNode is set, we recently observed a link change
|
||||
// that indicates we have switched networks. After switching networks,
|
||||
// the previously selected automatic exit node is no longer as likely
|
||||
// to be a good choice and connectivity will already be broken due to
|
||||
// the network switch. Therefore, it is a good time to switch to a new
|
||||
// exit node because the network is already disrupted.
|
||||
//
|
||||
// Unfortunately, at the time of the link change, no information is
|
||||
// known about the new network's latency or location, so the necessary
|
||||
// details are not available to make a new choice. Instead, it sets
|
||||
// b.refreshAutoExitNode to signal that a new decision should be made
|
||||
// when we have an updated netcheck report. ni is that updated report.
|
||||
//
|
||||
// However, during testing we observed that often the first ni is
|
||||
// inconclusive because it was running during the link change or the
|
||||
// link was otherwise not stable yet. b.MagicConn().updateEndpoints()
|
||||
// can detect when the netcheck failed and trigger a rebind, but the
|
||||
// required information is not available here, and moderate additional
|
||||
// plumbing is required to pass that in. Instead, checking for an active
|
||||
// DERP link offers an easy approximation. We will continue to refine
|
||||
// this over time.
|
||||
refresh = b.refreshAutoExitNode
|
||||
b.refreshAutoExitNode = false
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if cc == nil {
|
||||
return
|
||||
}
|
||||
cc.SetNetInfo(ni)
|
||||
if refresh {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
b.setAutoExitNodeIDLockedOnEntry(unlock)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setAutoExitNodeIDLockedOnEntry(unlock unlockOnce) {
|
||||
defer unlock()
|
||||
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
if !prefs.Valid() {
|
||||
b.logf("[unexpected]: received tailnet exit node ID pref change callback but current prefs are nil")
|
||||
return
|
||||
}
|
||||
prefsClone := prefs.AsStruct()
|
||||
newSuggestion, err := b.suggestExitNodeLocked(nil)
|
||||
if err != nil {
|
||||
b.logf("setAutoExitNodeID: %v", err)
|
||||
return
|
||||
}
|
||||
prefsClone.ExitNodeID = newSuggestion.ID
|
||||
_, err = b.editPrefsLockedOnEntry(&ipn.MaskedPrefs{
|
||||
Prefs: *prefsClone,
|
||||
ExitNodeIDSet: true,
|
||||
}, unlock)
|
||||
if err != nil {
|
||||
b.logf("setAutoExitNodeID: failed to apply exit node ID preference: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// setNetMapLocked updates the LocalBackend state to reflect the newly
|
||||
@@ -5290,7 +5619,7 @@ func (b *LocalBackend) OperatorUserID() string {
|
||||
if opUserName == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := user.Lookup(opUserName)
|
||||
u, err := osuser.LookupByUsername(opUserName)
|
||||
if err != nil {
|
||||
b.logf("error looking up operator %q uid: %v", opUserName, err)
|
||||
return ""
|
||||
@@ -6524,32 +6853,41 @@ func mayDeref[T any](p *T) (v T) {
|
||||
}
|
||||
|
||||
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
||||
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
||||
|
||||
// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// suggestExitNodeLocked computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
||||
// eligible for consideration, the peer must have NodeAttrSuggestExitNode in its CapMap.
|
||||
//
|
||||
// Currently, peers with a DERP home are preferred over those without (typically this means Mullvad).
|
||||
// Peers are selected based on having a DERP home that is the lowest latency to this device. For peers
|
||||
// without a DERP home, we look for geographic proximity to this device's DERP home.
|
||||
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
b.mu.Lock()
|
||||
//
|
||||
// netMap is an optional netmap to use that overrides b.netMap (needed for SetControlClientStatus before b.netMap is updated).
|
||||
// If netMap is nil, then b.netMap is used.
|
||||
//
|
||||
// b.mu.lock() must be held.
|
||||
func (b *LocalBackend) suggestExitNodeLocked(netMap *netmap.NetworkMap) (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
// netMap is an optional netmap to use that overrides b.netMap (needed for SetControlClientStatus before b.netMap is updated). If netMap is nil, then b.netMap is used.
|
||||
if netMap == nil {
|
||||
netMap = b.netMap
|
||||
}
|
||||
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
||||
netMap := b.netMap
|
||||
prevSuggestion := b.lastSuggestedExitNode
|
||||
b.mu.Unlock()
|
||||
|
||||
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, getAllowedSuggestions())
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.lastSuggestedExitNode = res.ID
|
||||
b.mu.Unlock()
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.suggestExitNodeLocked(nil)
|
||||
}
|
||||
|
||||
// selectRegionFunc returns a DERP region from the slice of candidate regions.
|
||||
// The value is returned, not the slice index.
|
||||
type selectRegionFunc func(views.Slice[int]) int
|
||||
@@ -6578,7 +6916,7 @@ func fillAllowedSuggestions() set.Set[tailcfg.StableNodeID] {
|
||||
}
|
||||
|
||||
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
if report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
if report == nil || report.PreferredDERP == 0 || netMap == nil || netMap.DERPMap == nil {
|
||||
return res, ErrNoPreferredDERP
|
||||
}
|
||||
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
|
||||
@@ -6788,6 +7126,12 @@ func longLatDistance(fromLat, fromLong, toLat, toLong float64) float64 {
|
||||
return earthRadiusMeters * c
|
||||
}
|
||||
|
||||
// shouldAutoExitNode checks for the auto exit node MDM policy.
|
||||
func shouldAutoExitNode() bool {
|
||||
exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, "")
|
||||
return exitNodeIDStr == "auto:any"
|
||||
}
|
||||
|
||||
// startAutoUpdate triggers an auto-update attempt. The actual update happens
|
||||
// asynchronously. If another update is in progress, an error is returned.
|
||||
func (b *LocalBackend) startAutoUpdate(logPrefix string) (retErr error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user