Compare commits
100 Commits
dsnet/sync
...
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 |
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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
|
||||
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
|
||||
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+
|
||||
@@ -82,7 +81,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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
|
||||
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
|
||||
💣 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+
|
||||
@@ -113,7 +111,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
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+
|
||||
@@ -161,7 +159,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
github.com/klauspost/compress/zstd from tailscale.com/util/zstdframe
|
||||
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
|
||||
github.com/kortschak/wol from tailscale.com/ipn/ipnlocal
|
||||
LD github.com/kr/fs from github.com/pkg/sftp
|
||||
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
|
||||
@@ -183,8 +180,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
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
|
||||
@@ -207,7 +202,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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 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
|
||||
@@ -230,7 +225,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
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
|
||||
@@ -307,7 +301,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
@@ -317,6 +310,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
@@ -660,7 +654,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/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
|
||||
LD tailscale.com/cmd/tailscaled/childproc from tailscale.com/ssh/tailssh
|
||||
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
|
||||
@@ -692,6 +685,10 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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
|
||||
@@ -701,6 +698,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
@@ -743,16 +741,15 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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/ssh/tailssh from tailscale.com/cmd/k8s-operator
|
||||
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+
|
||||
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+
|
||||
tailscale.com/tsd from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/tsnet from tailscale.com/cmd/k8s-operator
|
||||
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+
|
||||
@@ -837,7 +834,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
golang.org/x/crypto/argon2 from tailscale.com/tka
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/argon2+
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
LD golang.org/x/crypto/blowfish from github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf+
|
||||
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+
|
||||
@@ -848,7 +845,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
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+
|
||||
LD golang.org/x/crypto/ssh from github.com/pkg/sftp+
|
||||
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+
|
||||
@@ -953,7 +949,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
log/internal from log+
|
||||
log/slog from github.com/go-logr/logr+
|
||||
log/slog/internal from log/slog
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
maps from sigs.k8s.io/controller-runtime/pkg/predicate+
|
||||
math from archive/tar+
|
||||
math/big from crypto/dsa+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,8 +22,9 @@ import (
|
||||
"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/ssh/tailssh"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -33,7 +34,10 @@ 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
|
||||
|
||||
@@ -223,6 +227,7 @@ func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
@@ -242,18 +247,7 @@ func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, msg, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
spdyH := &spdyHijacker{
|
||||
ts: ap.ts,
|
||||
req: r,
|
||||
who: who,
|
||||
ResponseWriter: w,
|
||||
log: ap.log,
|
||||
pod: r.PathValue("pod"),
|
||||
ns: r.PathValue("namespace"),
|
||||
addrs: addrs,
|
||||
failOpen: failOpen,
|
||||
connectToRecorder: tailssh.ConnectToRecorder,
|
||||
}
|
||||
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)))
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -156,8 +156,7 @@ func runExitNodeSuggest(ctx context.Context, args []string) error {
|
||||
fmt.Println("No exit node suggestion is available.")
|
||||
return nil
|
||||
}
|
||||
hostname := strings.TrimSuffix(res.Name, ".")
|
||||
fmt.Printf("Suggested exit node: %v\nTo accept this suggestion, use `tailscale set --exit-node=%v`.\n", hostname, shellquote.Join(hostname))
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,9 +100,10 @@ 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
|
||||
|
||||
@@ -212,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+
|
||||
@@ -222,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+
|
||||
@@ -288,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+
|
||||
@@ -329,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+
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -333,6 +333,9 @@ func (c *Direct) Close() error {
|
||||
}
|
||||
}
|
||||
c.noiseClient = nil
|
||||
if tr, ok := c.httpc.Transport.(*http.Transport); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -95,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
|
||||
@@ -122,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) {
|
||||
@@ -147,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
|
||||
@@ -173,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,4 +120,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-CRzwQpi//TuLU8P66Dh4IdmM96f1YF10XyFfFBF4pQA=
|
||||
# nix-direnv cache busting line: sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=
|
||||
|
||||
56
go.mod
56
go.mod
@@ -4,7 +4,6 @@ go 1.22.0
|
||||
|
||||
require (
|
||||
filippo.io/mkcert v1.4.4
|
||||
fybrik.io/crdoc v0.6.3
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||
github.com/andybalholm/brotli v1.1.0
|
||||
@@ -20,13 +19,13 @@ 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
|
||||
@@ -44,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
|
||||
@@ -82,36 +80,36 @@ require (
|
||||
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-20240705152531-2f5d148bcfe1
|
||||
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
|
||||
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
|
||||
@@ -120,6 +118,7 @@ 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
|
||||
@@ -130,13 +129,16 @@ require (
|
||||
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/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 (
|
||||
@@ -196,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
|
||||
@@ -204,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
|
||||
@@ -213,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
|
||||
@@ -255,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
|
||||
@@ -340,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
|
||||
@@ -382,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-CRzwQpi//TuLU8P66Dh4IdmM96f1YF10XyFfFBF4pQA=
|
||||
sha256-1hekcJr1jEJFu4ZnapNkbAAv+8phTQuMloULIZ0f018=
|
||||
|
||||
121
go.sum
121
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=
|
||||
@@ -75,6 +73,8 @@ 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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
@@ -226,7 +226,7 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6
|
||||
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=
|
||||
@@ -242,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=
|
||||
@@ -264,8 +262,8 @@ 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=
|
||||
@@ -274,6 +272,8 @@ 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/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=
|
||||
@@ -296,8 +296,8 @@ 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=
|
||||
@@ -340,8 +340,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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.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=
|
||||
@@ -354,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=
|
||||
@@ -383,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=
|
||||
@@ -544,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=
|
||||
@@ -640,6 +646,8 @@ github.com/ldez/gomoddirectives v0.2.3 h1:y7MBaisZVDYmKvt9/l1mjNCiSA1BVn34U0ObUc
|
||||
github.com/ldez/gomoddirectives v0.2.3/go.mod h1:cpgBogWITnCfRq2qGoDkKMEVSaarhdBr6g8G04uz6d0=
|
||||
github.com/ldez/tagliatelle v0.5.0 h1:epgfuYt9v0CG3fms0pEgIMNPuFf/LpPIfjk4kyqSioo=
|
||||
github.com/ldez/tagliatelle v0.5.0/go.mod h1:rj1HmWiL1MiKQuOONhd09iySTEkUuE/8+5jtPYz9xa4=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leonklingele/grouper v1.1.1 h1:suWXRU57D4/Enn6pXR0QVqqWWrnJ9Osrz+5rjt8ivzU=
|
||||
github.com/leonklingele/grouper v1.1.1/go.mod h1:uk3I3uDfi9B6PeUjsCKi6ndcf63Uy7snXgR4yDYQVDY=
|
||||
github.com/lufeee/execinquery v1.2.1 h1:hf0Ems4SHcUGBxpGN7Jz78z1ppVkP/837ZlETPCEtOM=
|
||||
@@ -691,6 +699,8 @@ 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=
|
||||
@@ -863,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=
|
||||
@@ -878,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=
|
||||
@@ -891,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=
|
||||
@@ -923,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-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/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=
|
||||
@@ -1017,8 +1028,8 @@ 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=
|
||||
@@ -1037,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=
|
||||
@@ -1087,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=
|
||||
@@ -1133,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=
|
||||
@@ -1228,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=
|
||||
@@ -1238,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=
|
||||
@@ -1334,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=
|
||||
@@ -1478,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=
|
||||
@@ -1491,22 +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=
|
||||
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,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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -162,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
|
||||
}
|
||||
@@ -218,6 +223,11 @@ type Warnable struct {
|
||||
// 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
|
||||
@@ -291,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
|
||||
@@ -327,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)
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,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)
|
||||
}
|
||||
@@ -861,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 {
|
||||
@@ -938,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 {
|
||||
|
||||
@@ -162,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",
|
||||
|
||||
@@ -20,7 +20,7 @@ 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
|
||||
@@ -86,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ var NetworkStatusWarnable = Register(&Warnable{
|
||||
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.
|
||||
@@ -101,6 +102,8 @@ var notInMapPollWarnable = Register(&Warnable{
|
||||
Severity: SeverityMedium,
|
||||
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||
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.
|
||||
@@ -111,6 +114,7 @@ var noDERPHomeWarnable = Register(&Warnable{
|
||||
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.
|
||||
@@ -127,6 +131,7 @@ var noDERPConnectionWarnable = Register(&Warnable{
|
||||
}
|
||||
},
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -57,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"
|
||||
@@ -341,6 +345,21 @@ type LocalBackend struct {
|
||||
|
||||
// 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.
|
||||
@@ -389,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 {
|
||||
@@ -614,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
|
||||
@@ -634,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()
|
||||
@@ -684,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
|
||||
@@ -696,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)
|
||||
@@ -728,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()
|
||||
@@ -1189,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
|
||||
@@ -1220,7 +1337,14 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
prefs.WantRunning = true
|
||||
prefs.LoggedOut = false
|
||||
}
|
||||
if setExitNodeID(prefs, st.NetMap, b.lastSuggestedExitNode) {
|
||||
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) {
|
||||
@@ -1237,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)
|
||||
}
|
||||
@@ -1305,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 {
|
||||
@@ -2047,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.
|
||||
@@ -4440,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()
|
||||
|
||||
@@ -4865,8 +5123,32 @@ func (b *LocalBackend) Logout(ctx context.Context) error {
|
||||
func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
b.mu.Lock()
|
||||
cc := b.cc
|
||||
refresh := b.refreshAutoExitNode
|
||||
b.refreshAutoExitNode = false
|
||||
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 {
|
||||
@@ -4889,7 +5171,7 @@ func (b *LocalBackend) setAutoExitNodeIDLockedOnEntry(unlock unlockOnce) {
|
||||
return
|
||||
}
|
||||
prefsClone := prefs.AsStruct()
|
||||
newSuggestion, err := b.suggestExitNodeLocked()
|
||||
newSuggestion, err := b.suggestExitNodeLocked(nil)
|
||||
if err != nil {
|
||||
b.logf("setAutoExitNodeID: %v", err)
|
||||
return
|
||||
@@ -6571,7 +6853,6 @@ 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")
|
||||
|
||||
// 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
|
||||
@@ -6580,10 +6861,17 @@ var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try a
|
||||
// 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.
|
||||
//
|
||||
// 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() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
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
|
||||
|
||||
res, err := suggestExitNode(lastReport, netMap, prevSuggestion, randomRegion, randomNode, getAllowedSuggestions())
|
||||
@@ -6597,7 +6885,7 @@ func (b *LocalBackend) suggestExitNodeLocked() (response apitype.ExitNodeSuggest
|
||||
func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionResponse, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.suggestExitNodeLocked()
|
||||
return b.suggestExitNodeLocked(nil)
|
||||
}
|
||||
|
||||
// selectRegionFunc returns a DERP region from the slice of candidate regions.
|
||||
|
||||
@@ -2119,6 +2119,72 @@ func TestAutoExitNodeSetNetInfoCallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetControlClientStatusAutoExitNode(t *testing.T) {
|
||||
peer1 := makePeer(1, withCap(26), withSuggest(), withExitRoutes(), withNodeKey())
|
||||
peer2 := makePeer(2, withCap(26), withSuggest(), withExitRoutes(), withNodeKey())
|
||||
derpMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t1",
|
||||
RegionID: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t2",
|
||||
RegionID: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
report := &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 5 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
}
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: []tailcfg.NodeView{
|
||||
peer1,
|
||||
peer2,
|
||||
},
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
b := newTestLocalBackend(t)
|
||||
msh := &mockSyspolicyHandler{
|
||||
t: t,
|
||||
stringPolicies: map[syspolicy.Key]*string{
|
||||
syspolicy.ExitNodeID: ptr.To("auto:any"),
|
||||
},
|
||||
}
|
||||
syspolicy.SetHandlerForTest(t, msh)
|
||||
b.netMap = nm
|
||||
b.lastSuggestedExitNode = peer1.StableID()
|
||||
b.sys.MagicSock.Get().SetLastNetcheckReportForTest(b.ctx, report)
|
||||
b.SetPrefsForTest(b.pm.CurrentPrefs().AsStruct())
|
||||
firstExitNode := b.Prefs().ExitNodeID()
|
||||
newPeer1 := makePeer(1, withCap(26), withSuggest(), withExitRoutes(), withOnline(false), withNodeKey())
|
||||
updatedNetmap := &netmap.NetworkMap{
|
||||
Peers: []tailcfg.NodeView{
|
||||
newPeer1,
|
||||
peer2,
|
||||
},
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
b.SetControlClientStatus(b.cc, controlclient.Status{NetMap: updatedNetmap})
|
||||
lastExitNode := b.Prefs().ExitNodeID()
|
||||
if firstExitNode == lastExitNode {
|
||||
t.Errorf("did not switch exit nodes despite auto exit node going offline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplySysPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -3036,6 +3102,18 @@ func withCap(version tailcfg.CapabilityVersion) peerOptFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func withOnline(isOnline bool) peerOptFunc {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Online = &isOnline
|
||||
}
|
||||
}
|
||||
|
||||
func withNodeKey() peerOptFunc {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Key = key.NewNode().Public()
|
||||
}
|
||||
}
|
||||
|
||||
func deterministicRegionForTest(t testing.TB, want views.Slice[int], use int) selectRegionFunc {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ipnserver runs the LocalAPI HTTP server that communicates
|
||||
// with the LocalBackend.
|
||||
package ipnserver
|
||||
|
||||
import (
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
@@ -23,7 +24,16 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
pair, err := h.b.GetCertPEM(r.Context(), domain)
|
||||
var minValidity time.Duration
|
||||
if minValidityStr := r.URL.Query().Get("min_validity"); minValidityStr != "" {
|
||||
var err error
|
||||
minValidity, err = time.ParseDuration(minValidityStr)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid validity parameter: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
pair, err := h.b.GetCertPEMWithValidity(r.Context(), domain, minValidity)
|
||||
if err != nil {
|
||||
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
|
||||
// GetCertPEM (and everywhere) should carry info info to get whether
|
||||
|
||||
@@ -810,7 +810,7 @@ func exitNodeIPOfArg(s string, st *ipnstate.Status) (ip netip.Addr, err error) {
|
||||
match := 0
|
||||
for _, ps := range st.Peer {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if !strings.EqualFold(s, baseName) {
|
||||
if !strings.EqualFold(s, baseName) && !strings.EqualFold(s, ps.DNSName) {
|
||||
continue
|
||||
}
|
||||
match++
|
||||
|
||||
@@ -914,6 +914,21 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
||||
},
|
||||
want: mustIP("1.0.0.2"),
|
||||
},
|
||||
{
|
||||
name: "name_fqdn",
|
||||
arg: "skippy.foo.",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: mustIP("1.0.0.2"),
|
||||
},
|
||||
{
|
||||
name: "name_not_exit",
|
||||
arg: "skippy",
|
||||
@@ -928,6 +943,20 @@ func TestExitNodeIPOfArg(t *testing.T) {
|
||||
},
|
||||
wantErr: `node "skippy" is not advertising an exit node`,
|
||||
},
|
||||
{
|
||||
name: "name_wrong_fqdn",
|
||||
arg: "skippy.bar.",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netip.Addr{mustIP("1.0.0.2")},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `invalid value "skippy.bar." for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "ambiguous",
|
||||
arg: "skippy",
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package kubestore contains an ipn.StateStore implementation using Kubernetes Secrets.
|
||||
|
||||
package kubestore
|
||||
|
||||
import (
|
||||
|
||||
6
k8s-operator/api-docs-config.yaml
Normal file
6
k8s-operator/api-docs-config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
processor: {}
|
||||
render:
|
||||
kubernetesVersion: 1.30
|
||||
4747
k8s-operator/api.md
4747
k8s-operator/api.md
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// Package apis contains a constant to name the Tailscale Kubernetes Operator's schema group.
|
||||
package apis
|
||||
|
||||
const GroupName = "tailscale.com"
|
||||
|
||||
20
k8s-operator/sessionrecording/conn/conn.go
Normal file
20
k8s-operator/sessionrecording/conn/conn.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// Package conn contains shared interface for the hijacked
|
||||
// connection of a 'kubectl exec' session that is being recorded.
|
||||
package conn
|
||||
|
||||
import "net"
|
||||
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
// Fail can be called to set connection state to failed. By default any
|
||||
// bytes left over in write buffer are forwarded to the intended
|
||||
// destination when the connection is being closed except for when the
|
||||
// connection state is failed- so set the state to failed when erroring
|
||||
// out and failure policy is to fail closed.
|
||||
Fail()
|
||||
}
|
||||
118
k8s-operator/sessionrecording/fakes/fakes.go
Normal file
118
k8s-operator/sessionrecording/fakes/fakes.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// Package fakes contains mocks used for testing 'kubectl exec' session
|
||||
// recording functionality.
|
||||
package fakes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
func New(conn net.Conn, wb bytes.Buffer, rb bytes.Buffer, closed bool) net.Conn {
|
||||
return &TestConn{
|
||||
Conn: conn,
|
||||
writeBuf: wb,
|
||||
readBuf: rb,
|
||||
closed: closed,
|
||||
}
|
||||
}
|
||||
|
||||
type TestConn struct {
|
||||
net.Conn
|
||||
// writeBuf contains whatever was send to the conn via Write.
|
||||
writeBuf bytes.Buffer
|
||||
// readBuf contains whatever was sent to the conn via Read.
|
||||
readBuf bytes.Buffer
|
||||
sync.RWMutex // protects the following
|
||||
closed bool
|
||||
}
|
||||
|
||||
var _ net.Conn = &TestConn{}
|
||||
|
||||
func (tc *TestConn) Read(b []byte) (int, error) {
|
||||
return tc.readBuf.Read(b)
|
||||
}
|
||||
|
||||
func (tc *TestConn) Write(b []byte) (int, error) {
|
||||
return tc.writeBuf.Write(b)
|
||||
}
|
||||
|
||||
func (tc *TestConn) Close() error {
|
||||
tc.Lock()
|
||||
defer tc.Unlock()
|
||||
tc.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TestConn) IsClosed() bool {
|
||||
tc.Lock()
|
||||
defer tc.Unlock()
|
||||
return tc.closed
|
||||
}
|
||||
|
||||
func (tc *TestConn) WriteBufBytes() []byte {
|
||||
return tc.writeBuf.Bytes()
|
||||
}
|
||||
|
||||
func (tc *TestConn) ResetReadBuf() {
|
||||
tc.readBuf.Reset()
|
||||
}
|
||||
|
||||
func (tc *TestConn) WriteReadBufBytes(b []byte) error {
|
||||
_, err := tc.readBuf.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
type TestSessionRecorder struct {
|
||||
// buf holds data that was sent to the session recorder.
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *TestSessionRecorder) Write(b []byte) (int, error) {
|
||||
return t.buf.Write(b)
|
||||
}
|
||||
|
||||
func (t *TestSessionRecorder) Close() error {
|
||||
t.buf.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TestSessionRecorder) Bytes() []byte {
|
||||
return t.buf.Bytes()
|
||||
}
|
||||
|
||||
func CastLine(t *testing.T, p []byte, clock tstime.Clock) []byte {
|
||||
t.Helper()
|
||||
j, err := json.Marshal([]any{
|
||||
clock.Now().Sub(clock.Now()).Seconds(),
|
||||
"o",
|
||||
string(p),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling cast line: %v", err)
|
||||
}
|
||||
return append(j, '\n')
|
||||
}
|
||||
|
||||
func AsciinemaResizeMsg(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
ch := sessionrecording.CastHeader{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
bs, err := json.Marshal(ch)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling CastHeader: %v", err)
|
||||
}
|
||||
return append(bs, '\n')
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
// Package sessionrecording contains functionality for recording Kubernetes API
|
||||
// server proxy 'kubectl exec' sessions.
|
||||
package sessionrecording
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -19,17 +21,51 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/k8s-operator/sessionrecording/spdy"
|
||||
"tailscale.com/k8s-operator/sessionrecording/tsrecorder"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// spdyHijacker implements [net/http.Hijacker] interface.
|
||||
const SPDYProtocol protocol = "SPDY"
|
||||
|
||||
// protocol is the streaming protocol of the hijacked session. Supported
|
||||
// protocols are SPDY.
|
||||
type protocol string
|
||||
|
||||
var (
|
||||
// CounterSessionRecordingsAttempted counts the number of session recording attempts.
|
||||
CounterSessionRecordingsAttempted = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_attempted")
|
||||
|
||||
// counterSessionRecordingsUploaded counts the number of successfully uploaded session recordings.
|
||||
counterSessionRecordingsUploaded = clientmetric.NewCounter("k8s_auth_proxy_session_recordings_uploaded")
|
||||
)
|
||||
|
||||
func New(ts *tsnet.Server, req *http.Request, who *apitype.WhoIsResponse, w http.ResponseWriter, pod, ns string, proto protocol, addrs []netip.AddrPort, failOpen bool, connFunc RecorderDialFn, log *zap.SugaredLogger) *Hijacker {
|
||||
return &Hijacker{
|
||||
ts: ts,
|
||||
req: req,
|
||||
who: who,
|
||||
ResponseWriter: w,
|
||||
pod: pod,
|
||||
ns: ns,
|
||||
addrs: addrs,
|
||||
failOpen: failOpen,
|
||||
connectToRecorder: connFunc,
|
||||
proto: proto,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Hijacker implements [net/http.Hijacker] interface.
|
||||
// It must be configured with an http request for a 'kubectl exec' session that
|
||||
// needs to be recorded. It knows how to hijack the connection and configure for
|
||||
// the session contents to be sent to a tsrecorder instance.
|
||||
type spdyHijacker struct {
|
||||
type Hijacker struct {
|
||||
http.ResponseWriter
|
||||
ts *tsnet.Server
|
||||
req *http.Request
|
||||
@@ -40,6 +76,7 @@ type spdyHijacker struct {
|
||||
addrs []netip.AddrPort // tsrecorder addresses
|
||||
failOpen bool // whether to fail open if recording fails
|
||||
connectToRecorder RecorderDialFn
|
||||
proto protocol // streaming protocol
|
||||
}
|
||||
|
||||
// RecorderDialFn dials the specified netip.AddrPorts that should be tsrecorder
|
||||
@@ -51,7 +88,7 @@ type RecorderDialFn func(context.Context, []netip.AddrPort, func(context.Context
|
||||
|
||||
// Hijack hijacks a 'kubectl exec' session and configures for the session
|
||||
// contents to be sent to a recorder.
|
||||
func (h *spdyHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
func (h *Hijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
h.log.Infof("recorder addrs: %v, failOpen: %v", h.addrs, h.failOpen)
|
||||
reqConn, brw, err := h.ResponseWriter.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
@@ -69,7 +106,7 @@ func (h *spdyHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
// spdyHijacker.addrs. Returns conn from provided opts, wrapped in recording
|
||||
// logic. If connecting to the recorder fails or an error is received during the
|
||||
// session and spdyHijacker.failOpen is false, connection will be closed.
|
||||
func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
||||
func (h *Hijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.Conn, error) {
|
||||
const (
|
||||
// https://docs.asciinema.org/manual/asciicast/v2/
|
||||
asciicastv2 = 2
|
||||
@@ -96,27 +133,18 @@ func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.C
|
||||
h.log.Info("successfully connected to a session recorder")
|
||||
wc = rw
|
||||
cl := tstime.DefaultClock{}
|
||||
lc := &spdyRemoteConnRecorder{
|
||||
log: h.log,
|
||||
Conn: conn,
|
||||
rec: &recorder{
|
||||
start: cl.Now(),
|
||||
clock: cl,
|
||||
failOpen: h.failOpen,
|
||||
conn: wc,
|
||||
},
|
||||
}
|
||||
|
||||
rec := tsrecorder.New(wc, cl, cl.Now(), h.failOpen)
|
||||
qp := h.req.URL.Query()
|
||||
ch := CastHeader{
|
||||
ch := sessionrecording.CastHeader{
|
||||
Version: asciicastv2,
|
||||
Timestamp: lc.rec.start.Unix(),
|
||||
Timestamp: cl.Now().Unix(),
|
||||
Command: strings.Join(qp["command"], " "),
|
||||
SrcNode: strings.TrimSuffix(h.who.Node.Name, "."),
|
||||
SrcNodeID: h.who.Node.StableID,
|
||||
Kubernetes: &Kubernetes{
|
||||
Kubernetes: &sessionrecording.Kubernetes{
|
||||
PodName: h.pod,
|
||||
Namespace: h.ns,
|
||||
Container: strings.Join(qp["container"], " "),
|
||||
},
|
||||
}
|
||||
if !h.who.Node.IsTagged() {
|
||||
@@ -125,7 +153,7 @@ func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.C
|
||||
} else {
|
||||
ch.SrcNodeTags = h.who.Node.Tags
|
||||
}
|
||||
lc.ch = ch
|
||||
lc := spdy.New(conn, rec, ch, h.log)
|
||||
go func() {
|
||||
var err error
|
||||
select {
|
||||
@@ -134,6 +162,7 @@ func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.C
|
||||
case err = <-errChan:
|
||||
}
|
||||
if err == nil {
|
||||
counterSessionRecordingsUploaded.Add(1)
|
||||
h.log.Info("finished uploading the recording")
|
||||
return
|
||||
}
|
||||
@@ -145,7 +174,7 @@ func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.C
|
||||
}
|
||||
msg += "; failure mode set to 'fail closed'; closing connection"
|
||||
h.log.Error(msg)
|
||||
lc.failed = true
|
||||
lc.Fail()
|
||||
// TODO (irbekrm): write a message to the client
|
||||
if err := lc.Close(); err != nil {
|
||||
h.log.Infof("error closing recorder connections: %v", err)
|
||||
@@ -155,51 +184,6 @@ func (h *spdyHijacker) setUpRecording(ctx context.Context, conn net.Conn) (net.C
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
// CastHeader is the asciicast header to be sent to the recorder at the start of
|
||||
// the recording of a session.
|
||||
// https://docs.asciinema.org/manual/asciicast/v2/#header
|
||||
type CastHeader struct {
|
||||
// Version is the asciinema file format version.
|
||||
Version int `json:"version"`
|
||||
|
||||
// Width is the terminal width in characters.
|
||||
Width int `json:"width"`
|
||||
|
||||
// Height is the terminal height in characters.
|
||||
Height int `json:"height"`
|
||||
|
||||
// Timestamp is the unix timestamp of when the recording started.
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
|
||||
// Tailscale-specific fields: SrcNode is the full MagicDNS name of the
|
||||
// tailnet node originating the connection, without the trailing dot.
|
||||
SrcNode string `json:"srcNode"`
|
||||
|
||||
// SrcNodeID is the node ID of the tailnet node originating the connection.
|
||||
SrcNodeID tailcfg.StableNodeID `json:"srcNodeID"`
|
||||
|
||||
// SrcNodeTags is the list of tags on the node originating the connection (if any).
|
||||
SrcNodeTags []string `json:"srcNodeTags,omitempty"`
|
||||
|
||||
// SrcNodeUserID is the user ID of the node originating the connection (if not tagged).
|
||||
SrcNodeUserID tailcfg.UserID `json:"srcNodeUserID,omitempty"` // if not tagged
|
||||
|
||||
// SrcNodeUser is the LoginName of the node originating the connection (if not tagged).
|
||||
SrcNodeUser string `json:"srcNodeUser,omitempty"`
|
||||
|
||||
Command string
|
||||
|
||||
// Kubernetes-specific fields:
|
||||
Kubernetes *Kubernetes `json:"kubernetes,omitempty"`
|
||||
}
|
||||
|
||||
// Kubernetes contains 'kubectl exec' session specific information for
|
||||
// tsrecorder.
|
||||
type Kubernetes struct {
|
||||
PodName string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
func closeConnWithWarning(conn net.Conn, msg string) error {
|
||||
b := io.NopCloser(bytes.NewBuffer([]byte(msg)))
|
||||
resp := http.Response{Status: http.StatusText(http.StatusForbidden), StatusCode: http.StatusForbidden, Body: b}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
package sessionrecording
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -19,12 +19,13 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/k8s-operator/sessionrecording/fakes"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func Test_SPDYHijacker(t *testing.T) {
|
||||
func Test_Hijacker(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -64,9 +65,9 @@ func Test_SPDYHijacker(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tc := &testConn{}
|
||||
tc := &fakes.TestConn{}
|
||||
ch := make(chan error)
|
||||
h := &spdyHijacker{
|
||||
h := &Hijacker{
|
||||
connectToRecorder: func(context.Context, []netip.AddrPort, func(context.Context, string, string) (net.Conn, error)) (wc io.WriteCloser, rec []*tailcfg.SSHRecordingAttempt, _ <-chan error, err error) {
|
||||
if tt.failRecorderConnect {
|
||||
err = errors.New("test")
|
||||
@@ -98,8 +99,8 @@ func Test_SPDYHijacker(t *testing.T) {
|
||||
// (test that connection remains open over some period
|
||||
// of time).
|
||||
if err := tstest.WaitFor(timeout, func() (err error) {
|
||||
if tt.wantsConnClosed != tc.isClosed() {
|
||||
return fmt.Errorf("got connection state: %t, wants connection state: %t", tc.isClosed(), tt.wantsConnClosed)
|
||||
if tt.wantsConnClosed != tc.IsClosed() {
|
||||
return fmt.Errorf("got connection state: %t, wants connection state: %t", tc.IsClosed(), tt.wantsConnClosed)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
// Package spdy contains functionality for parsing SPDY streaming sessions. This
|
||||
// is used for 'kubectl exec' session recording.
|
||||
package spdy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -17,16 +19,29 @@ import (
|
||||
|
||||
"go.uber.org/zap"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
srconn "tailscale.com/k8s-operator/sessionrecording/conn"
|
||||
"tailscale.com/k8s-operator/sessionrecording/tsrecorder"
|
||||
"tailscale.com/sessionrecording"
|
||||
)
|
||||
|
||||
// spdyRemoteConnRecorder is a wrapper around net.Conn. It reads the bytestream
|
||||
// for a 'kubectl exec' session, sends session recording data to the configured
|
||||
// recorder and forwards the raw bytes to the original destination.
|
||||
type spdyRemoteConnRecorder struct {
|
||||
func New(nc net.Conn, rec *tsrecorder.Client, ch sessionrecording.CastHeader, log *zap.SugaredLogger) srconn.Conn {
|
||||
return &conn{
|
||||
Conn: nc,
|
||||
rec: rec,
|
||||
ch: ch,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// conn is a wrapper around net.Conn. It reads the bytestream for a 'kubectl
|
||||
// exec' session streamed using SPDY protocol, sends session recording data to
|
||||
// the configured recorder and forwards the raw bytes to the original
|
||||
// destination.
|
||||
type conn struct {
|
||||
net.Conn
|
||||
// rec knows how to send data written to it to a tsrecorder instance.
|
||||
rec *recorder
|
||||
ch CastHeader
|
||||
rec *tsrecorder.Client
|
||||
ch sessionrecording.CastHeader
|
||||
|
||||
stdoutStreamID atomic.Uint32
|
||||
stderrStreamID atomic.Uint32
|
||||
@@ -53,7 +68,7 @@ type spdyRemoteConnRecorder struct {
|
||||
// If the frame is a data frame for resize stream, sends resize message to the
|
||||
// recorder. If the frame is a SYN_STREAM control frame that starts stdout,
|
||||
// stderr or resize stream, store the stream ID.
|
||||
func (c *spdyRemoteConnRecorder) Read(b []byte) (int, error) {
|
||||
func (c *conn) Read(b []byte) (int, error) {
|
||||
c.rmu.Lock()
|
||||
defer c.rmu.Unlock()
|
||||
n, err := c.Conn.Read(b)
|
||||
@@ -103,7 +118,7 @@ func (c *spdyRemoteConnRecorder) Read(b []byte) (int, error) {
|
||||
// Write forwards the raw data of the latest parsed SPDY frame to the original
|
||||
// destination. If the frame is an SPDY data frame, it also sends the payload to
|
||||
// the connected session recorder.
|
||||
func (c *spdyRemoteConnRecorder) Write(b []byte) (int, error) {
|
||||
func (c *conn) Write(b []byte) (int, error) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
c.writeBuf.Write(b)
|
||||
@@ -133,7 +148,7 @@ func (c *spdyRemoteConnRecorder) Write(b []byte) (int, error) {
|
||||
return
|
||||
}
|
||||
j = append(j, '\n')
|
||||
err = c.rec.writeCastLine(j)
|
||||
err = c.rec.WriteCastLine(j)
|
||||
if err != nil {
|
||||
c.log.Errorf("received error from recorder: %v", err)
|
||||
}
|
||||
@@ -151,7 +166,7 @@ func (c *spdyRemoteConnRecorder) Write(b []byte) (int, error) {
|
||||
return len(b), err
|
||||
}
|
||||
|
||||
func (c *spdyRemoteConnRecorder) Close() error {
|
||||
func (c *conn) Close() error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if c.closed {
|
||||
@@ -167,13 +182,19 @@ func (c *spdyRemoteConnRecorder) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// parseSynStream parses SYN_STREAM SPDY control frame and updates
|
||||
func (s *conn) Fail() {
|
||||
s.wmu.Lock()
|
||||
s.failed = true
|
||||
s.wmu.Unlock()
|
||||
}
|
||||
|
||||
// storeStreamID parses SYN_STREAM SPDY control frame and updates
|
||||
// spdyRemoteConnRecorder to store the newly created stream's ID if it is one of
|
||||
// the stream types we care about. Storing stream_id:stream_type mapping allows
|
||||
// us to parse received data frames (that have stream IDs) differently depening
|
||||
// on which stream they belong to (i.e send data frame payload for stdout stream
|
||||
// to session recorder).
|
||||
func (c *spdyRemoteConnRecorder) storeStreamID(sf spdyFrame, header http.Header) {
|
||||
func (c *conn) storeStreamID(sf spdyFrame, header http.Header) {
|
||||
const (
|
||||
streamTypeHeaderKey = "Streamtype"
|
||||
)
|
||||
@@ -3,19 +3,18 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
package spdy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/k8s-operator/sessionrecording/fakes"
|
||||
"tailscale.com/k8s-operator/sessionrecording/tsrecorder"
|
||||
"tailscale.com/sessionrecording"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
// Test_Writes tests that 1 or more Write calls to spdyRemoteConnRecorder
|
||||
@@ -56,13 +55,13 @@ func Test_Writes(t *testing.T) {
|
||||
name: "single_write_stdout_data_frame_with_payload",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5}},
|
||||
wantForwarded: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5},
|
||||
wantRecorded: castLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
wantRecorded: fakes.CastLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
},
|
||||
{
|
||||
name: "single_write_stderr_data_frame_with_payload",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5}},
|
||||
wantForwarded: []byte{0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5},
|
||||
wantRecorded: castLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
wantRecorded: fakes.CastLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
},
|
||||
{
|
||||
name: "single_data_frame_unknow_stream_with_payload",
|
||||
@@ -73,13 +72,13 @@ func Test_Writes(t *testing.T) {
|
||||
name: "control_frame_and_data_frame_split_across_two_writes",
|
||||
inputs: [][]byte{{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}, {0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5}},
|
||||
wantForwarded: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5},
|
||||
wantRecorded: castLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
wantRecorded: fakes.CastLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl),
|
||||
},
|
||||
{
|
||||
name: "single_first_write_stdout_data_frame_with_payload",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5}},
|
||||
wantForwarded: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5},
|
||||
wantRecorded: append(asciinemaResizeMsg(t, 10, 20), castLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl)...),
|
||||
wantRecorded: append(fakes.AsciinemaResizeMsg(t, 10, 20), fakes.CastLine(t, []byte{0x1, 0x2, 0x3, 0x4, 0x5}, cl)...),
|
||||
width: 10,
|
||||
height: 20,
|
||||
firstWrite: true,
|
||||
@@ -87,19 +86,15 @@ func Test_Writes(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tc := &testConn{}
|
||||
sr := &testSessionRecorder{}
|
||||
rec := &recorder{
|
||||
conn: sr,
|
||||
clock: cl,
|
||||
start: cl.Now(),
|
||||
}
|
||||
tc := &fakes.TestConn{}
|
||||
sr := &fakes.TestSessionRecorder{}
|
||||
rec := tsrecorder.New(sr, cl, cl.Now(), true)
|
||||
|
||||
c := &spdyRemoteConnRecorder{
|
||||
c := &conn{
|
||||
Conn: tc,
|
||||
log: zl.Sugar(),
|
||||
rec: rec,
|
||||
ch: CastHeader{
|
||||
ch: sessionrecording.CastHeader{
|
||||
Width: tt.width,
|
||||
Height: tt.height,
|
||||
},
|
||||
@@ -118,13 +113,13 @@ func Test_Writes(t *testing.T) {
|
||||
}
|
||||
|
||||
// Assert that the expected bytes have been forwarded to the original destination.
|
||||
gotForwarded := tc.writeBuf.Bytes()
|
||||
gotForwarded := tc.WriteBufBytes()
|
||||
if !reflect.DeepEqual(gotForwarded, tt.wantForwarded) {
|
||||
t.Errorf("expected bytes not forwarded, wants\n%v\ngot\n%v", tt.wantForwarded, gotForwarded)
|
||||
}
|
||||
|
||||
// Assert that the expected bytes have been forwarded to the session recorder.
|
||||
gotRecorded := sr.buf.Bytes()
|
||||
gotRecorded := sr.Bytes()
|
||||
if !reflect.DeepEqual(gotRecorded, tt.wantRecorded) {
|
||||
t.Errorf("expected bytes not recorded, wants\n%v\ngot\n%v", tt.wantRecorded, gotRecorded)
|
||||
}
|
||||
@@ -197,14 +192,10 @@ func Test_Reads(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tc := &testConn{}
|
||||
sr := &testSessionRecorder{}
|
||||
rec := &recorder{
|
||||
conn: sr,
|
||||
clock: cl,
|
||||
start: cl.Now(),
|
||||
}
|
||||
c := &spdyRemoteConnRecorder{
|
||||
tc := &fakes.TestConn{}
|
||||
sr := &fakes.TestSessionRecorder{}
|
||||
rec := tsrecorder.New(sr, cl, cl.Now(), true)
|
||||
c := &conn{
|
||||
Conn: tc,
|
||||
log: zl.Sugar(),
|
||||
rec: rec,
|
||||
@@ -213,9 +204,8 @@ func Test_Reads(t *testing.T) {
|
||||
|
||||
for i, input := range tt.inputs {
|
||||
c.zlibReqReader = reader
|
||||
tc.readBuf.Reset()
|
||||
_, err := tc.readBuf.Write(input)
|
||||
if err != nil {
|
||||
tc.ResetReadBuf()
|
||||
if err := tc.WriteReadBufBytes(input); err != nil {
|
||||
t.Fatalf("writing bytes to test conn: %v", err)
|
||||
}
|
||||
_, err = c.Read(make([]byte, len(input)))
|
||||
@@ -244,19 +234,6 @@ func Test_Reads(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func castLine(t *testing.T, p []byte, clock tstime.Clock) []byte {
|
||||
t.Helper()
|
||||
j, err := json.Marshal([]any{
|
||||
clock.Now().Sub(clock.Now()).Seconds(),
|
||||
"o",
|
||||
string(p),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling cast line: %v", err)
|
||||
}
|
||||
return append(j, '\n')
|
||||
}
|
||||
|
||||
func resizeMsgBytes(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
bs, err := json.Marshal(spdyResizeMsg{Width: width, Height: height})
|
||||
@@ -265,62 +242,3 @@ func resizeMsgBytes(t *testing.T, width, height int) []byte {
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func asciinemaResizeMsg(t *testing.T, width, height int) []byte {
|
||||
t.Helper()
|
||||
ch := CastHeader{
|
||||
Width: width,
|
||||
Height: height,
|
||||
}
|
||||
bs, err := json.Marshal(ch)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling CastHeader: %v", err)
|
||||
}
|
||||
return append(bs, '\n')
|
||||
}
|
||||
|
||||
type testConn struct {
|
||||
net.Conn
|
||||
// writeBuf contains whatever was send to the conn via Write.
|
||||
writeBuf bytes.Buffer
|
||||
// readBuf contains whatever was sent to the conn via Read.
|
||||
readBuf bytes.Buffer
|
||||
sync.RWMutex // protects the following
|
||||
closed bool
|
||||
}
|
||||
|
||||
var _ net.Conn = &testConn{}
|
||||
|
||||
func (tc *testConn) Read(b []byte) (int, error) {
|
||||
return tc.readBuf.Read(b)
|
||||
}
|
||||
|
||||
func (tc *testConn) Write(b []byte) (int, error) {
|
||||
return tc.writeBuf.Write(b)
|
||||
}
|
||||
|
||||
func (tc *testConn) Close() error {
|
||||
tc.Lock()
|
||||
defer tc.Unlock()
|
||||
tc.closed = true
|
||||
return nil
|
||||
}
|
||||
func (tc *testConn) isClosed() bool {
|
||||
tc.Lock()
|
||||
defer tc.Unlock()
|
||||
return tc.closed
|
||||
}
|
||||
|
||||
type testSessionRecorder struct {
|
||||
// buf holds data that was sent to the session recorder.
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *testSessionRecorder) Write(b []byte) (int, error) {
|
||||
return t.buf.Write(b)
|
||||
}
|
||||
|
||||
func (t *testSessionRecorder) Close() error {
|
||||
t.buf.Reset()
|
||||
return nil
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
package spdy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
package spdy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
package spdy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
// Package tsrecorder contains functionality for connecting to a tsrecorder instance.
|
||||
package tsrecorder
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -16,9 +17,18 @@ import (
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
func New(conn io.WriteCloser, clock tstime.Clock, start time.Time, failOpen bool) *Client {
|
||||
return &Client{
|
||||
start: start,
|
||||
clock: clock,
|
||||
conn: conn,
|
||||
failOpen: failOpen,
|
||||
}
|
||||
}
|
||||
|
||||
// recorder knows how to send the provided bytes to the configured tsrecorder
|
||||
// instance in asciinema format.
|
||||
type recorder struct {
|
||||
type Client struct {
|
||||
start time.Time
|
||||
clock tstime.Clock
|
||||
|
||||
@@ -36,7 +46,7 @@ type recorder struct {
|
||||
|
||||
// Write appends timestamp to the provided bytes and sends them to the
|
||||
// configured tsrecorder.
|
||||
func (rec *recorder) Write(p []byte) (err error) {
|
||||
func (rec *Client) Write(p []byte) (err error) {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -52,7 +62,7 @@ func (rec *recorder) Write(p []byte) (err error) {
|
||||
return fmt.Errorf("error marhalling payload: %w", err)
|
||||
}
|
||||
j = append(j, '\n')
|
||||
if err := rec.writeCastLine(j); err != nil {
|
||||
if err := rec.WriteCastLine(j); err != nil {
|
||||
if !rec.failOpen {
|
||||
return fmt.Errorf("error writing payload to recorder: %w", err)
|
||||
}
|
||||
@@ -61,7 +71,7 @@ func (rec *recorder) Write(p []byte) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rec *recorder) Close() error {
|
||||
func (rec *Client) Close() error {
|
||||
rec.mu.Lock()
|
||||
defer rec.mu.Unlock()
|
||||
if rec.conn == nil {
|
||||
@@ -74,15 +84,20 @@ func (rec *recorder) Close() error {
|
||||
|
||||
// writeCastLine sends bytes to the tsrecorder. The bytes should be in
|
||||
// asciinema format.
|
||||
func (rec *recorder) writeCastLine(j []byte) error {
|
||||
rec.mu.Lock()
|
||||
defer rec.mu.Unlock()
|
||||
if rec.conn == nil {
|
||||
func (c *Client) WriteCastLine(j []byte) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.conn == nil {
|
||||
return errors.New("recorder closed")
|
||||
}
|
||||
_, err := rec.conn.Write(j)
|
||||
_, err := c.conn.Write(j)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recorder write error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ResizeMsg struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
// Package kube contains types and utilities for the Tailscale Kubernetes Operator.
|
||||
package kube
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package kube provides a client to interact with Kubernetes.
|
||||
// This package is Tailscale-internal and not meant for external consumption.
|
||||
// Further, the API should not be considered stable.
|
||||
package kube
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package kube provides a client to interact with Kubernetes.
|
||||
// This package is Tailscale-internal and not meant for external consumption.
|
||||
// Further, the API should not be considered stable.
|
||||
package kube
|
||||
|
||||
import "net/netip"
|
||||
|
||||
@@ -29,7 +29,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
@@ -60,7 +60,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android/libtailscale](https://pkg.go.dev/github.com/tailscale/tailscale-android/libtailscale) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cfa45674af86/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f5d148bcfe1/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
|
||||
|
||||
@@ -33,7 +33,7 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
@@ -47,9 +47,9 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.4.1/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.7/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.7/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.7/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.8/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.8/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.8/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
@@ -65,8 +65,8 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cfa45674af86/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/71393c576b98/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
@@ -74,15 +74,15 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.24.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.6.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.11.1/LICENSE))
|
||||
- [github.com/go-json-experiment/json](https://pkg.go.dev/github.com/go-json-experiment/json) ([BSD-3-Clause](https://github.com/go-json-experiment/json/blob/2e55bd4e08b0/LICENSE))
|
||||
- [github.com/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
|
||||
@@ -84,8 +84,8 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5db17b287bf1/LICENSE))
|
||||
- [github.com/tailscale/wf](https://pkg.go.dev/github.com/tailscale/wf) ([BSD-3-Clause](https://github.com/tailscale/wf/blob/6fbb0a674ee6/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/2f5d148bcfe1/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/71393c576b98/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/v0.12.0/LICENSE))
|
||||
@@ -95,19 +95,19 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.24.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
|
||||
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.16.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.1/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/64c016c92987/LICENSE))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.30.3/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([ISC](https://github.com/nhooyr/websocket/blob/v1.8.10/LICENSE.txt))
|
||||
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/LICENSE))
|
||||
- [sigs.k8s.io/yaml/goyaml.v2](https://pkg.go.dev/sigs.k8s.io/yaml/goyaml.v2) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.4.0/goyaml.v2/LICENSE))
|
||||
|
||||
@@ -44,9 +44,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.4.1/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.7/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.7/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.7/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.8/LICENSE))
|
||||
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.8/internal/snapref/LICENSE))
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.8/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.5.0/LICENSE.md))
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.58/LICENSE))
|
||||
@@ -57,23 +57,23 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/7601212d8e23/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/4327221bd339/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/6580b55d49ca/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/8497ac4dab2e/LICENSE))
|
||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
|
||||
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/ae6ca9944745/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.24.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.25.0:LICENSE))
|
||||
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.26.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.27.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.16.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
|
||||
@@ -242,12 +242,12 @@ func (l *Logger) Flush() {
|
||||
l.logger.StartFlush()
|
||||
}
|
||||
|
||||
func (l *Logger) Shutdown() {
|
||||
func (l *Logger) Shutdown(ctx context.Context) {
|
||||
if l.cancelFn != nil {
|
||||
l.cancelFn()
|
||||
}
|
||||
l.filch.Close()
|
||||
l.logger.Shutdown(context.Background())
|
||||
l.logger.Shutdown(ctx)
|
||||
|
||||
type closeIdler interface {
|
||||
CloseIdleConnections()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package sockstatlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -28,7 +29,7 @@ func TestResourceCleanup(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lg.Write([]byte("hello"))
|
||||
lg.Shutdown()
|
||||
lg.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Command logadopt is a CLI tool to adopt a machine into a logtail collection.
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -266,6 +266,7 @@ func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
case <-l.shutdownDone:
|
||||
}
|
||||
close(done)
|
||||
l.httpc.CloseIdleConnections()
|
||||
}()
|
||||
|
||||
l.shutdownStartMu.Lock()
|
||||
|
||||
223
net/captivedetection/captivedetection.go
Normal file
223
net/captivedetection/captivedetection.go
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package captivedetection provides a way to detect if the system is connected to a network that has
|
||||
// a captive portal. It does this by making HTTP requests to known captive portal detection endpoints
|
||||
// and checking if the HTTP responses indicate that a captive portal might be present.
|
||||
package captivedetection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Detector checks whether the system is behind a captive portal.
|
||||
type Detector struct {
|
||||
|
||||
// httpClient is the HTTP client that is used for captive portal detection. It is configured
|
||||
// to not follow redirects, have a short timeout and no keep-alive.
|
||||
httpClient *http.Client
|
||||
// currIfIndex is the index of the interface that is currently being used by the httpClient.
|
||||
currIfIndex int
|
||||
// mu guards currIfIndex.
|
||||
mu sync.Mutex
|
||||
// logf is the logger used for logging messages. If it is nil, log.Printf is used.
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
// NewDetector creates a new Detector instance for captive portal detection.
|
||||
func NewDetector(logf logger.Logf) *Detector {
|
||||
d := &Detector{logf: logf}
|
||||
d.httpClient = &http.Client{
|
||||
// No redirects allowed
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: d.dialContext,
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
Timeout: Timeout,
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// Timeout is the timeout for captive portal detection requests. Because the captive portal intercepting our requests
|
||||
// is usually located on the LAN, this is a relatively short timeout.
|
||||
const Timeout = 3 * time.Second
|
||||
|
||||
// Detect is the entry point to the API. It attempts to detect if the system is behind a captive portal
|
||||
// by making HTTP requests to known captive portal detection Endpoints. If any of the requests return a response code
|
||||
// or body that looks like a captive portal, Detect returns true. It returns false in all other cases, including when any
|
||||
// error occurs during a detection attempt.
|
||||
//
|
||||
// This function might take a while to return, as it will attempt to detect a captive portal on all available interfaces
|
||||
// by performing multiple HTTP requests. It should be called in a separate goroutine if you want to avoid blocking.
|
||||
func (d *Detector) Detect(ctx context.Context, netMon *netmon.Monitor, derpMap *tailcfg.DERPMap, preferredDERPRegionID int) (found bool) {
|
||||
return d.detectCaptivePortalWithGOOS(ctx, netMon, derpMap, preferredDERPRegionID, runtime.GOOS)
|
||||
}
|
||||
|
||||
func (d *Detector) detectCaptivePortalWithGOOS(ctx context.Context, netMon *netmon.Monitor, derpMap *tailcfg.DERPMap, preferredDERPRegionID int, goos string) (found bool) {
|
||||
ifState := netMon.InterfaceState()
|
||||
if !ifState.AnyInterfaceUp() {
|
||||
d.logf("[v2] DetectCaptivePortal: no interfaces up, returning false")
|
||||
return false
|
||||
}
|
||||
|
||||
endpoints := availableEndpoints(derpMap, preferredDERPRegionID, d.logf, goos)
|
||||
|
||||
// Here we try detecting a captive portal using *all* available interfaces on the system
|
||||
// that have a IPv4 address. We consider to have found a captive portal when any interface
|
||||
// reports one may exists. This is necessary because most systems have multiple interfaces,
|
||||
// and most importantly on macOS no default route interface is set until the user has accepted
|
||||
// the captive portal alert thrown by the system. If no default route interface is known,
|
||||
// we need to try with anything that might remotely resemble a Wi-Fi interface.
|
||||
for ifName, i := range ifState.Interface {
|
||||
if !i.IsUp() || i.IsLoopback() || interfaceNameDoesNotNeedCaptiveDetection(ifName, goos) {
|
||||
continue
|
||||
}
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
d.logf("[v1] DetectCaptivePortal: failed to get addresses for interface %s: %v", ifName, err)
|
||||
continue
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
d.logf("[v2] attempting to do captive portal detection on interface %s", ifName)
|
||||
res := d.detectOnInterface(ctx, i.Index, endpoints)
|
||||
if res {
|
||||
d.logf("DetectCaptivePortal(found=true,ifName=%s)", ifName)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
d.logf("DetectCaptivePortal(found=false)")
|
||||
return false
|
||||
}
|
||||
|
||||
// interfaceNameDoesNotNeedCaptiveDetection returns true if an interface does not require captive portal detection
|
||||
// based on its name. This is useful to avoid making unnecessary HTTP requests on interfaces that are known to not
|
||||
// require it. We also avoid making requests on the interface prefixes "pdp" and "rmnet", which are cellular data
|
||||
// interfaces on iOS and Android, respectively, and would be needlessly battery-draining.
|
||||
func interfaceNameDoesNotNeedCaptiveDetection(ifName string, goos string) bool {
|
||||
ifName = strings.ToLower(ifName)
|
||||
excludedPrefixes := []string{"tailscale", "tun", "tap", "docker", "kube", "wg"}
|
||||
if goos == "windows" {
|
||||
excludedPrefixes = append(excludedPrefixes, "loopback", "tunnel", "ppp", "isatap", "teredo", "6to4")
|
||||
} else if goos == "darwin" || goos == "ios" {
|
||||
excludedPrefixes = append(excludedPrefixes, "pdp", "awdl", "bridge", "ap", "utun", "tap", "llw", "anpi", "lo", "stf", "gif", "xhc", "pktap")
|
||||
} else if goos == "android" {
|
||||
excludedPrefixes = append(excludedPrefixes, "rmnet", "p2p", "dummy", "sit")
|
||||
}
|
||||
for _, prefix := range excludedPrefixes {
|
||||
if strings.HasPrefix(ifName, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// detectOnInterface reports whether or not we think the system is behind a
|
||||
// captive portal, detected by making a request to a URL that we know should
|
||||
// return a "204 No Content" response and checking if that's what we get.
|
||||
//
|
||||
// The boolean return is whether we think we have a captive portal.
|
||||
func (d *Detector) detectOnInterface(ctx context.Context, ifIndex int, endpoints []Endpoint) bool {
|
||||
defer d.httpClient.CloseIdleConnections()
|
||||
|
||||
d.logf("[v2] %d available captive portal detection endpoints: %v", len(endpoints), endpoints)
|
||||
|
||||
// We try to detect the captive portal more quickly by making requests to multiple endpoints concurrently.
|
||||
var wg sync.WaitGroup
|
||||
resultCh := make(chan bool, len(endpoints))
|
||||
|
||||
for i, e := range endpoints {
|
||||
if i >= 5 {
|
||||
// Try a maximum of 5 endpoints, break out (returning false) if we run of attempts.
|
||||
break
|
||||
}
|
||||
wg.Add(1)
|
||||
go func(endpoint Endpoint) {
|
||||
defer wg.Done()
|
||||
found, err := d.verifyCaptivePortalEndpoint(ctx, endpoint, ifIndex)
|
||||
if err != nil {
|
||||
d.logf("[v1] checkCaptivePortalEndpoint failed with endpoint %v: %v", endpoint, err)
|
||||
return
|
||||
}
|
||||
if found {
|
||||
resultCh <- true
|
||||
}
|
||||
}(e)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
}()
|
||||
|
||||
for result := range resultCh {
|
||||
if result {
|
||||
// If any of the endpoints seems to be a captive portal, we consider the system to be behind one.
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// verifyCaptivePortalEndpoint checks if the given Endpoint is a captive portal by making an HTTP request to the
|
||||
// given Endpoint URL using the interface with index ifIndex, and checking if the response looks like a captive portal.
|
||||
func (d *Detector) verifyCaptivePortalEndpoint(ctx context.Context, e Endpoint, ifIndex int) (found bool, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", e.URL.String(), nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Attach the Tailscale challenge header if the endpoint supports it. Not all captive portal detection endpoints
|
||||
// support this, so we only attach it if the endpoint does.
|
||||
if e.SupportsTailscaleChallenge {
|
||||
// Note: the set of valid characters in a challenge and the total
|
||||
// length is limited; see isChallengeChar in cmd/derper for more
|
||||
// details.
|
||||
chal := "ts_" + e.URL.Host
|
||||
req.Header.Set("X-Tailscale-Challenge", chal)
|
||||
}
|
||||
|
||||
d.mu.Lock()
|
||||
d.currIfIndex = ifIndex
|
||||
d.mu.Unlock()
|
||||
|
||||
// Make the actual request, and check if the response looks like a captive portal or not.
|
||||
r, err := d.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return e.responseLooksLikeCaptive(r, d.logf), nil
|
||||
}
|
||||
|
||||
func (d *Detector) dialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
ifIndex := d.currIfIndex
|
||||
|
||||
dl := net.Dialer{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
return setSocketInterfaceIndex(c, ifIndex, d.logf)
|
||||
},
|
||||
}
|
||||
|
||||
return dl.DialContext(ctx, network, addr)
|
||||
}
|
||||
60
net/captivedetection/captivedetection_test.go
Normal file
60
net/captivedetection/captivedetection_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package captivedetection
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
func TestAvailableEndpointsAlwaysAtLeastTwo(t *testing.T) {
|
||||
endpoints := availableEndpoints(nil, 0, t.Logf, runtime.GOOS)
|
||||
if len(endpoints) == 0 {
|
||||
t.Errorf("Expected non-empty AvailableEndpoints, got an empty slice instead")
|
||||
}
|
||||
if len(endpoints) == 1 {
|
||||
t.Errorf("Expected at least two AvailableEndpoints for redundancy, got only one instead")
|
||||
}
|
||||
for _, e := range endpoints {
|
||||
if e.URL.Scheme != "http" {
|
||||
t.Errorf("Expected HTTP URL in Endpoint, got HTTPS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectCaptivePortalReturnsFalse(t *testing.T) {
|
||||
d := NewDetector(t.Logf)
|
||||
found := d.Detect(context.Background(), netmon.NewStatic(), nil, 0)
|
||||
if found {
|
||||
t.Errorf("DetectCaptivePortal returned true, expected false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllEndpointsAreUpAndReturnExpectedResponse(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/13019")
|
||||
d := NewDetector(t.Logf)
|
||||
endpoints := availableEndpoints(nil, 0, t.Logf, runtime.GOOS)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, e := range endpoints {
|
||||
wg.Add(1)
|
||||
go func(endpoint Endpoint) {
|
||||
defer wg.Done()
|
||||
found, err := d.verifyCaptivePortalEndpoint(context.Background(), endpoint, 0)
|
||||
if err != nil {
|
||||
t.Errorf("verifyCaptivePortalEndpoint failed with endpoint %v: %v", endpoint, err)
|
||||
}
|
||||
if found {
|
||||
t.Errorf("verifyCaptivePortalEndpoint with endpoint %v says we're behind a captive portal, but we aren't", endpoint)
|
||||
}
|
||||
}(e)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
178
net/captivedetection/endpoints.go
Normal file
178
net/captivedetection/endpoints.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package captivedetection
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// EndpointProvider is an enum that represents the source of an Endpoint.
|
||||
type EndpointProvider int
|
||||
|
||||
const (
|
||||
// DERPMapPreferred is used for an endpoint that is a DERP node contained in the current preferred DERP region,
|
||||
// as provided by the DERPMap.
|
||||
DERPMapPreferred EndpointProvider = iota
|
||||
// DERPMapOther is used for an endpoint that is a DERP node, but not contained in the current preferred DERP region.
|
||||
DERPMapOther
|
||||
// Tailscale is used for endpoints that are the Tailscale coordination server or admin console.
|
||||
Tailscale
|
||||
)
|
||||
|
||||
func (p EndpointProvider) String() string {
|
||||
switch p {
|
||||
case DERPMapPreferred:
|
||||
return "DERPMapPreferred"
|
||||
case Tailscale:
|
||||
return "Tailscale"
|
||||
case DERPMapOther:
|
||||
return "DERPMapOther"
|
||||
default:
|
||||
return fmt.Sprintf("EndpointProvider(%d)", p)
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint represents a URL that can be used to detect a captive portal, along with the expected
|
||||
// result of the HTTP request.
|
||||
type Endpoint struct {
|
||||
// URL is the URL that we make an HTTP request to as part of the captive portal detection process.
|
||||
URL *url.URL
|
||||
// StatusCode is the expected HTTP status code that we expect to see in the response.
|
||||
StatusCode int
|
||||
// ExpectedContent is a string that we expect to see contained in the response body. If this is non-empty,
|
||||
// we will check that the response body contains this string. If it is empty, we will not check the response body
|
||||
// and only check the status code.
|
||||
ExpectedContent string
|
||||
// SupportsTailscaleChallenge is true if the endpoint will return the sent value of the X-Tailscale-Challenge
|
||||
// HTTP header in its HTTP response.
|
||||
SupportsTailscaleChallenge bool
|
||||
// Provider is the source of the endpoint. This is used to prioritize certain endpoints over others
|
||||
// (for example, a DERP node in the preferred region should always be used first).
|
||||
Provider EndpointProvider
|
||||
}
|
||||
|
||||
func (e Endpoint) String() string {
|
||||
return fmt.Sprintf("Endpoint{URL=%q, StatusCode=%d, ExpectedContent=%q, SupportsTailscaleChallenge=%v, Provider=%s}", e.URL, e.StatusCode, e.ExpectedContent, e.SupportsTailscaleChallenge, e.Provider.String())
|
||||
}
|
||||
|
||||
func (e Endpoint) Equal(other Endpoint) bool {
|
||||
return e.URL.String() == other.URL.String() &&
|
||||
e.StatusCode == other.StatusCode &&
|
||||
e.ExpectedContent == other.ExpectedContent &&
|
||||
e.SupportsTailscaleChallenge == other.SupportsTailscaleChallenge &&
|
||||
e.Provider == other.Provider
|
||||
}
|
||||
|
||||
// availableEndpoints returns a set of Endpoints which can be used for captive portal detection by performing
|
||||
// one or more HTTP requests and looking at the response. The returned Endpoints are ordered by preference,
|
||||
// with the most preferred Endpoint being the first in the slice.
|
||||
func availableEndpoints(derpMap *tailcfg.DERPMap, preferredDERPRegionID int, logf logger.Logf, goos string) []Endpoint {
|
||||
endpoints := []Endpoint{}
|
||||
|
||||
if derpMap == nil || len(derpMap.Regions) == 0 {
|
||||
// When the client first starts, we don't have a DERPMap in LocalBackend yet. In this case,
|
||||
// we use the static DERPMap from dnsfallback.
|
||||
logf("captivedetection: current DERPMap is empty, using map from dnsfallback")
|
||||
derpMap = dnsfallback.GetDERPMap()
|
||||
}
|
||||
// Use the DERP IPs as captive portal detection endpoints. Using IPs is better than hostnames
|
||||
// because they do not depend on DNS resolution.
|
||||
for _, region := range derpMap.Regions {
|
||||
if region.Avoid {
|
||||
continue
|
||||
}
|
||||
for _, node := range region.Nodes {
|
||||
if node.IPv4 == "" || !node.CanPort80 {
|
||||
continue
|
||||
}
|
||||
str := "http://" + node.IPv4 + "/generate_204"
|
||||
u, err := url.Parse(str)
|
||||
if err != nil {
|
||||
logf("captivedetection: failed to parse DERP node URL %q: %v", str, err)
|
||||
continue
|
||||
}
|
||||
p := DERPMapOther
|
||||
if region.RegionID == preferredDERPRegionID {
|
||||
p = DERPMapPreferred
|
||||
}
|
||||
e := Endpoint{u, http.StatusNoContent, "", true, p}
|
||||
endpoints = append(endpoints, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Let's also try the default Tailscale coordination server and admin console.
|
||||
// These are likely to be blocked on some networks.
|
||||
appendTailscaleEndpoint := func(urlString string) {
|
||||
u, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
logf("captivedetection: failed to parse Tailscale URL %q: %v", urlString, err)
|
||||
return
|
||||
}
|
||||
endpoints = append(endpoints, Endpoint{u, http.StatusNoContent, "", false, Tailscale})
|
||||
}
|
||||
appendTailscaleEndpoint("http://controlplane.tailscale.com/generate_204")
|
||||
appendTailscaleEndpoint("http://login.tailscale.com/generate_204")
|
||||
|
||||
// Sort the endpoints by provider so that we can prioritize DERP nodes in the preferred region, followed by
|
||||
// any other DERP server elsewhere, then followed by Tailscale endpoints.
|
||||
slices.SortFunc(endpoints, func(x, y Endpoint) int {
|
||||
return cmp.Compare(x.Provider, y.Provider)
|
||||
})
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// responseLooksLikeCaptive checks if the given HTTP response matches the expected response for the Endpoint.
|
||||
func (e Endpoint) responseLooksLikeCaptive(r *http.Response, logf logger.Logf) bool {
|
||||
defer r.Body.Close()
|
||||
|
||||
// Check the status code first.
|
||||
if r.StatusCode != e.StatusCode {
|
||||
logf("[v1] unexpected status code in captive portal response: want=%d, got=%d", e.StatusCode, r.StatusCode)
|
||||
return true
|
||||
}
|
||||
|
||||
// If the endpoint supports the Tailscale challenge header, check that the response contains the expected header.
|
||||
if e.SupportsTailscaleChallenge {
|
||||
expectedResponse := "response ts_" + e.URL.Host
|
||||
hasResponse := r.Header.Get("X-Tailscale-Response") == expectedResponse
|
||||
if !hasResponse {
|
||||
// The response did not contain the expected X-Tailscale-Response header, which means we are most likely
|
||||
// behind a captive portal (somebody is tampering with the response headers).
|
||||
logf("captive portal check response did not contain expected X-Tailscale-Response header: want=%q, got=%q", expectedResponse, r.Header.Get("X-Tailscale-Response"))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If we don't have an expected content string, we don't need to check the response body.
|
||||
if e.ExpectedContent == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Read the response body and check if it contains the expected content.
|
||||
b, err := io.ReadAll(io.LimitReader(r.Body, 4096))
|
||||
if err != nil {
|
||||
logf("reading captive portal check response body failed: %v", err)
|
||||
return false
|
||||
}
|
||||
hasExpectedContent := mem.Contains(mem.B(b), mem.S(e.ExpectedContent))
|
||||
if !hasExpectedContent {
|
||||
// The response body did not contain the expected content, that means we are most likely behind a captive portal.
|
||||
logf("[v1] captive portal check response body did not contain expected content: want=%q", e.ExpectedContent)
|
||||
return true
|
||||
}
|
||||
|
||||
// If we got here, the response looks good.
|
||||
return false
|
||||
}
|
||||
19
net/captivedetection/rawconn.go
Normal file
19
net/captivedetection/rawconn.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(ios || darwin)
|
||||
|
||||
package captivedetection
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// setSocketInterfaceIndex sets the IP_BOUND_IF socket option on the given RawConn.
|
||||
// This forces the socket to use the given interface.
|
||||
func setSocketInterfaceIndex(c syscall.RawConn, ifIndex int, logf logger.Logf) error {
|
||||
// No-op on non-Darwin platforms.
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user