Compare commits
62 Commits
bradfitz/j
...
marwan/off
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d15835bb3 | ||
|
|
57856fc0d5 | ||
|
|
9904421853 | ||
|
|
5d09649b0b | ||
|
|
d500a92926 | ||
|
|
1f94047475 | ||
|
|
bd54b61746 | ||
|
|
20562a4fb9 | ||
|
|
e7bf6e716b | ||
|
|
32ce18716b | ||
|
|
0f57b9340b | ||
|
|
b2c522ce95 | ||
|
|
54f58d1143 | ||
|
|
485018696a | ||
|
|
1608831c33 | ||
|
|
d3af54444c | ||
|
|
d97cddd876 | ||
|
|
f77821fd63 | ||
|
|
0b32adf9ec | ||
|
|
1ac14d7216 | ||
|
|
4ff276cf52 | ||
|
|
2742153f84 | ||
|
|
646990a7d0 | ||
|
|
8882c6b730 | ||
|
|
35d2efd692 | ||
|
|
fc074a6b9f | ||
|
|
014bf25c0a | ||
|
|
0834712c91 | ||
|
|
fec41e4904 | ||
|
|
fd0acc4faf | ||
|
|
380a3a0834 | ||
|
|
5d61d1c7b0 | ||
|
|
9609b26541 | ||
|
|
7403d8e9a8 | ||
|
|
f0b9d3f477 | ||
|
|
3f3edeec07 | ||
|
|
808b4139ee | ||
|
|
49bf63cdd0 | ||
|
|
d209b032ab | ||
|
|
fc28c8e7f3 | ||
|
|
b7c3cfe049 | ||
|
|
8d7b78f3f7 | ||
|
|
041733d3d1 | ||
|
|
874972b683 | ||
|
|
b546a6e758 | ||
|
|
c6af5bbfe8 | ||
|
|
e92f4c6af8 | ||
|
|
986d60a094 | ||
|
|
6a982faa7d | ||
|
|
c8f258a904 | ||
|
|
726d5d507d | ||
|
|
2238ca8a05 | ||
|
|
8bd442ba8c | ||
|
|
7b1c764088 | ||
|
|
b8af91403d | ||
|
|
e21d8768f9 | ||
|
|
5576972261 | ||
|
|
ba517ab388 | ||
|
|
2b638f550d | ||
|
|
9102a5bb73 | ||
|
|
c8fe9f0064 | ||
|
|
42dac7c5c2 |
11
Dockerfile
11
Dockerfile
@@ -1,17 +1,6 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in container
|
||||
# environments, such as Docker and Kubernetes. Though it should work, we
|
||||
# don't regularly test it, and we know there are some feature limitations.
|
||||
#
|
||||
# See current bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
|
||||
2
Makefile
2
Makefile
@@ -21,6 +21,7 @@ updatedeps: ## Update depaware deps
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
|
||||
depaware: ## Run depaware checks
|
||||
@@ -30,6 +31,7 @@ depaware: ## Run depaware checks
|
||||
tailscale.com/cmd/tailscaled \
|
||||
tailscale.com/cmd/tailscale \
|
||||
tailscale.com/cmd/derper \
|
||||
tailscale.com/cmd/k8s-operator \
|
||||
tailscale.com/cmd/stund
|
||||
|
||||
buildwindows: ## Build tailscale CLI for windows/amd64
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.69.0
|
||||
1.71.0
|
||||
|
||||
3
api.md
3
api.md
@@ -1,3 +1,6 @@
|
||||
> [!IMPORTANT]
|
||||
> The Tailscale API documentation has moved to https://tailscale.com/api
|
||||
|
||||
# Tailscale API
|
||||
|
||||
The Tailscale API documentation is located in **[tailscale/publicapi](./publicapi/readme.md#tailscale-api)**.
|
||||
|
||||
@@ -11,6 +11,7 @@ package appc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/execqueue"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -78,6 +80,42 @@ type RouteAdvertiser interface {
|
||||
UnadvertiseRoute(...netip.Prefix) error
|
||||
}
|
||||
|
||||
var (
|
||||
metricStoreRoutesRateBuckets = []int64{1, 2, 3, 4, 5, 10, 100, 1000}
|
||||
metricStoreRoutesNBuckets = []int64{1, 2, 3, 4, 5, 10, 100, 1000, 10000}
|
||||
metricStoreRoutesRate []*clientmetric.Metric
|
||||
metricStoreRoutesN []*clientmetric.Metric
|
||||
)
|
||||
|
||||
func initMetricStoreRoutes() {
|
||||
for _, n := range metricStoreRoutesRateBuckets {
|
||||
metricStoreRoutesRate = append(metricStoreRoutesRate, clientmetric.NewCounter(fmt.Sprintf("appc_store_routes_rate_%d", n)))
|
||||
}
|
||||
metricStoreRoutesRate = append(metricStoreRoutesRate, clientmetric.NewCounter("appc_store_routes_rate_over"))
|
||||
for _, n := range metricStoreRoutesNBuckets {
|
||||
metricStoreRoutesN = append(metricStoreRoutesN, clientmetric.NewCounter(fmt.Sprintf("appc_store_routes_n_routes_%d", n)))
|
||||
}
|
||||
metricStoreRoutesN = append(metricStoreRoutesN, clientmetric.NewCounter("appc_store_routes_n_routes_over"))
|
||||
}
|
||||
|
||||
func recordMetric(val int64, buckets []int64, metrics []*clientmetric.Metric) {
|
||||
if len(buckets) < 1 {
|
||||
return
|
||||
}
|
||||
// finds the first bucket where val <=, or len(buckets) if none match
|
||||
// for bucket values of 1, 10, 100; 0-1 goes to [0], 2-10 goes to [1], 11-100 goes to [2], 101+ goes to [3]
|
||||
bucket, _ := slices.BinarySearch(buckets, val)
|
||||
metrics[bucket].Add(1)
|
||||
}
|
||||
|
||||
func metricStoreRoutes(rate, nRoutes int64) {
|
||||
if len(metricStoreRoutesRate) == 0 {
|
||||
initMetricStoreRoutes()
|
||||
}
|
||||
recordMetric(rate, metricStoreRoutesRateBuckets, metricStoreRoutesRate)
|
||||
recordMetric(nRoutes, metricStoreRoutesNBuckets, metricStoreRoutesN)
|
||||
}
|
||||
|
||||
// RouteInfo is a data structure used to persist the in memory state of an AppConnector
|
||||
// so that we can know, even after a restart, which routes came from ACLs and which were
|
||||
// learned from domains.
|
||||
@@ -141,6 +179,7 @@ func NewAppConnector(logf logger.Logf, routeAdvertiser RouteAdvertiser, routeInf
|
||||
}
|
||||
ac.writeRateMinute = newRateLogger(time.Now, time.Minute, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in minute starting at %v (%d routes)", c, s, l)
|
||||
metricStoreRoutes(c, l)
|
||||
})
|
||||
ac.writeRateDay = newRateLogger(time.Now, 24*time.Hour, func(c int64, s time.Time, l int64) {
|
||||
ac.logf("routeInfo write rate: %d in 24 hours starting at %v (%d routes)", c, s, l)
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -49,7 +49,7 @@ case "$TARGET" in
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--gotags="ts_kube" \
|
||||
--gotags="ts_kube,ts_package_container" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
--target="${PLATFORM}" \
|
||||
|
||||
@@ -37,6 +37,16 @@ type ACLTest struct {
|
||||
Allow []string `json:"allow,omitempty"` // old name for accept
|
||||
}
|
||||
|
||||
// NodeAttrGrant defines additional string attributes that apply to specific devices.
|
||||
type NodeAttrGrant struct {
|
||||
// Target specifies which nodes the attributes apply to. The nodes can be a
|
||||
// tag (tag:server), user (alice@example.com), group (group:kids), or *.
|
||||
Target []string `json:"target,omitempty"`
|
||||
|
||||
// Attr are the attributes to set on Target(s).
|
||||
Attr []string `json:"attr,omitempty"`
|
||||
}
|
||||
|
||||
// ACLDetails contains all the details for an ACL.
|
||||
type ACLDetails struct {
|
||||
Tests []ACLTest `json:"tests,omitempty"`
|
||||
@@ -44,6 +54,7 @@ type ACLDetails struct {
|
||||
Groups map[string][]string `json:"groups,omitempty"`
|
||||
TagOwners map[string][]string `json:"tagowners,omitempty"`
|
||||
Hosts map[string]string `json:"hosts,omitempty"`
|
||||
NodeAttrs []NodeAttrGrant `json:"nodeAttrs,omitempty"`
|
||||
}
|
||||
|
||||
// ACL contains an ACLDetails and metadata.
|
||||
@@ -150,7 +161,12 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
|
||||
// ACLTestFailureSummary specifies the JSON format sent to the
|
||||
// JavaScript client to be rendered in the HTML.
|
||||
type ACLTestFailureSummary struct {
|
||||
User string `json:"user,omitempty"`
|
||||
// User is the source ("src") value of the ACL test that failed.
|
||||
// The name "user" is a legacy holdover from the original naming and
|
||||
// is kept for compatibility but it may also contain any value
|
||||
// that's valid in a ACL test "src" field.
|
||||
User string `json:"user,omitempty"`
|
||||
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
|
||||
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(lc.socket())
|
||||
return safesocket.ConnectContext(ctx, lc.socket())
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
@@ -933,7 +933,20 @@ func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err e
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
|
||||
return lc.CertPairWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// CertPairWithValidity returns a cert and private key for the provided DNS
|
||||
// domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
// When minValidity is non-zero, the returned certificate will be valid for at
|
||||
// least the given duration, if permitted by the CA. If the certificate is
|
||||
// valid, but for less than minValidity, it will be synchronously renewed.
|
||||
//
|
||||
// API maturity: this is considered a stable API.
|
||||
func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": "18.16.1",
|
||||
"node": "18.20.4",
|
||||
"yarn": "1.22.19"
|
||||
},
|
||||
"type": "module",
|
||||
|
||||
@@ -248,6 +248,11 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
|
||||
// CanAutoUpdate reports whether auto-updating via the clientupdate package
|
||||
// is supported for the current os/distro.
|
||||
func CanAutoUpdate() bool {
|
||||
if version.IsMacSysExt() {
|
||||
// Macsys uses Sparkle for auto-updates, which doesn't have an update
|
||||
// function in this package.
|
||||
return true
|
||||
}
|
||||
_, canAutoUpdate := (&Updater{}).getUpdateFunction()
|
||||
return canAutoUpdate
|
||||
}
|
||||
|
||||
@@ -78,7 +78,11 @@ func main() {
|
||||
w(" return false")
|
||||
w("}")
|
||||
}
|
||||
cloneOutput := pkg.Name + "_clone.go"
|
||||
cloneOutput := pkg.Name + "_clone"
|
||||
if *flagBuildTags == "test" {
|
||||
cloneOutput += "_test"
|
||||
}
|
||||
cloneOutput += ".go"
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -91,16 +95,19 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
}
|
||||
|
||||
name := typ.Obj().Name()
|
||||
typeParams := typ.Origin().TypeParams()
|
||||
_, typeParamNames := codegen.FormatTypeParams(typeParams, it)
|
||||
nameWithParams := name + typeParamNames
|
||||
fmt.Fprintf(buf, "// Clone makes a deep copy of %s.\n", name)
|
||||
fmt.Fprintf(buf, "// The result aliases no memory with the original.\n")
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", name, name)
|
||||
fmt.Fprintf(buf, "func (src *%s) Clone() *%s {\n", nameWithParams, nameWithParams)
|
||||
writef := func(format string, args ...any) {
|
||||
fmt.Fprintf(buf, "\t"+format+"\n", args...)
|
||||
}
|
||||
writef("if src == nil {")
|
||||
writef("\treturn nil")
|
||||
writef("}")
|
||||
writef("dst := new(%s)", name)
|
||||
writef("dst := new(%s)", nameWithParams)
|
||||
writef("*dst = *src")
|
||||
for i := range t.NumFields() {
|
||||
fname := t.Field(i).Name()
|
||||
@@ -126,16 +133,23 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
|
||||
writef("for i := range dst.%s {", fname)
|
||||
if ptr, isPtr := ft.Elem().(*types.Pointer); isPtr {
|
||||
if _, isBasic := ptr.Elem().Underlying().(*types.Basic); isBasic {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("if src.%s[i] == nil { dst.%s[i] = nil } else {", fname, fname)
|
||||
writef("\tdst.%s[i] = ptr.To(*src.%s[i])", fname, fname)
|
||||
writef("}")
|
||||
writef("if src.%s[i] == nil { dst.%s[i] = nil } else {", fname, fname)
|
||||
if codegen.ContainsPointers(ptr.Elem()) {
|
||||
if _, isIface := ptr.Elem().Underlying().(*types.Interface); isIface {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\tdst.%s[i] = ptr.To((*src.%s[i]).Clone())", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
} else {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\tdst.%s[i] = ptr.To(*src.%s[i])", fname, fname)
|
||||
}
|
||||
writef("}")
|
||||
} else if ft.Elem().String() == "encoding/json.RawMessage" {
|
||||
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
|
||||
} else if _, isIface := ft.Elem().Underlying().(*types.Interface); isIface {
|
||||
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
|
||||
} else {
|
||||
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
|
||||
}
|
||||
@@ -145,14 +159,19 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
|
||||
}
|
||||
case *types.Pointer:
|
||||
if named, _ := ft.Elem().(*types.Named); named != nil && codegen.ContainsPointers(ft.Elem()) {
|
||||
base := ft.Elem()
|
||||
hasPtrs := codegen.ContainsPointers(base)
|
||||
if named, _ := base.(*types.Named); named != nil && hasPtrs {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = ptr.To(*src.%s)", fname, fname)
|
||||
if codegen.ContainsPointers(ft.Elem()) {
|
||||
if _, isIface := base.Underlying().(*types.Interface); isIface && hasPtrs {
|
||||
writef("\tdst.%s = ptr.To((*src.%s).Clone())", fname, fname)
|
||||
} else if !hasPtrs {
|
||||
writef("\tdst.%s = ptr.To(*src.%s)", fname, fname)
|
||||
} else {
|
||||
writef("\t" + `panic("TODO pointers in pointers")`)
|
||||
}
|
||||
writef("}")
|
||||
@@ -172,18 +191,50 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("if dst.%s != nil {", fname)
|
||||
writef("\tdst.%s = map[%s]%s{}", fname, it.QualifiedName(ft.Key()), it.QualifiedName(elem))
|
||||
writef("\tfor k, v := range src.%s {", fname)
|
||||
switch elem.(type) {
|
||||
|
||||
switch elem := elem.Underlying().(type) {
|
||||
case *types.Pointer:
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
writef("\t\tif v == nil { dst.%s[k] = nil } else {", fname)
|
||||
if base := elem.Elem().Underlying(); codegen.ContainsPointers(base) {
|
||||
if _, isIface := base.(*types.Interface); isIface {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\t\t\tdst.%s[k] = ptr.To((*v).Clone())", fname)
|
||||
} else {
|
||||
writef("\t\t\tdst.%s[k] = v.Clone()", fname)
|
||||
}
|
||||
} else {
|
||||
it.Import("tailscale.com/types/ptr")
|
||||
writef("\t\t\tdst.%s[k] = ptr.To(*v)", fname)
|
||||
}
|
||||
writef("}")
|
||||
case *types.Interface:
|
||||
if cloneResultType := methodResultType(elem, "Clone"); cloneResultType != nil {
|
||||
if _, isPtr := cloneResultType.(*types.Pointer); isPtr {
|
||||
writef("\t\tdst.%s[k] = *(v.Clone())", fname)
|
||||
} else {
|
||||
writef("\t\tdst.%s[k] = v.Clone()", fname)
|
||||
}
|
||||
} else {
|
||||
writef(`panic("%s (%v) does not have a Clone method")`, fname, elem)
|
||||
}
|
||||
default:
|
||||
writef("\t\tdst.%s[k] = *(v.Clone())", fname)
|
||||
}
|
||||
|
||||
writef("\t}")
|
||||
writef("}")
|
||||
} else {
|
||||
it.Import("maps")
|
||||
writef("\tdst.%s = maps.Clone(src.%s)", fname, fname)
|
||||
}
|
||||
case *types.Interface:
|
||||
// If ft is an interface with a "Clone() ft" method, it can be used to clone the field.
|
||||
// This includes scenarios where ft is a constrained type parameter.
|
||||
if cloneResultType := methodResultType(ft, "Clone"); cloneResultType.Underlying() == ft {
|
||||
writef("dst.%s = src.%s.Clone()", fname, fname)
|
||||
continue
|
||||
}
|
||||
writef(`panic("%s (%v) does not have a compatible Clone method")`, fname, ft)
|
||||
default:
|
||||
writef(`panic("TODO: %s (%T)")`, fname, ft)
|
||||
}
|
||||
@@ -191,7 +242,7 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
||||
writef("return dst")
|
||||
fmt.Fprintf(buf, "}\n\n")
|
||||
|
||||
buf.Write(codegen.AssertStructUnchanged(t, name, "Clone", it))
|
||||
buf.Write(codegen.AssertStructUnchanged(t, name, typeParams, "Clone", it))
|
||||
}
|
||||
|
||||
// hasBasicUnderlying reports true when typ.Underlying() is a slice or a map.
|
||||
@@ -203,3 +254,15 @@ func hasBasicUnderlying(typ types.Type) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func methodResultType(typ types.Type, method string) types.Type {
|
||||
viewMethod := codegen.LookupMethod(typ, method)
|
||||
if viewMethod == nil {
|
||||
return nil
|
||||
}
|
||||
sig, ok := viewMethod.Type().(*types.Signature)
|
||||
if !ok || sig.Results().Len() != 1 {
|
||||
return nil
|
||||
}
|
||||
return sig.Results().At(0).Type()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer
|
||||
|
||||
// Package clonerex is an example package for the cloner tool.
|
||||
package clonerex
|
||||
|
||||
type SliceContainer struct {
|
||||
|
||||
@@ -10,7 +10,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
|
||||
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
|
||||
1002
cmd/k8s-operator/depaware.txt
Normal file
1002
cmd/k8s-operator/depaware.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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,7 @@ 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
|
||||
// TODO (irbekrm): generate CRD docs from the yamls
|
||||
|
||||
func main() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
|
||||
@@ -11,16 +11,19 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
tskube "tailscale.com/kube"
|
||||
"tailscale.com/ssh/tailssh"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -30,10 +33,32 @@ 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")
|
||||
|
||||
// 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")
|
||||
)
|
||||
|
||||
type apiServerProxyMode int
|
||||
|
||||
func (a apiServerProxyMode) String() string {
|
||||
switch a {
|
||||
case apiserverProxyModeDisabled:
|
||||
return "disabled"
|
||||
case apiserverProxyModeEnabled:
|
||||
return "auth"
|
||||
case apiserverProxyModeNoAuth:
|
||||
return "noauth"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
apiserverProxyModeDisabled apiServerProxyMode = iota
|
||||
apiserverProxyModeEnabled
|
||||
@@ -97,26 +122,7 @@ func maybeLaunchAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config,
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode)
|
||||
}
|
||||
|
||||
// apiserverProxy is an http.Handler that authenticates requests using the Tailscale
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type apiserverProxy struct {
|
||||
log *zap.SugaredLogger
|
||||
lc *tailscale.LocalClient
|
||||
rp *httputil.ReverseProxy
|
||||
}
|
||||
|
||||
func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := h.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
if err != nil {
|
||||
h.log.Errorf("failed to authenticate caller: %v", err)
|
||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
h.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
go runAPIServerProxy(s, rt, zlog.Named("apiserver-proxy"), mode, restConfig.Host)
|
||||
}
|
||||
|
||||
// runAPIServerProxy runs an HTTP server that authenticates requests using the
|
||||
@@ -133,64 +139,42 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// are passed through to the Kubernetes API.
|
||||
//
|
||||
// It never returns.
|
||||
func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode) {
|
||||
func runAPIServerProxy(ts *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLogger, mode apiServerProxyMode, host string) {
|
||||
if mode == apiserverProxyModeDisabled {
|
||||
return
|
||||
}
|
||||
ln, err := s.Listen("tcp", ":443")
|
||||
ln, err := ts.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatalf("could not listen on :443: %v", err)
|
||||
}
|
||||
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
u, err := url.Parse(host)
|
||||
if err != nil {
|
||||
log.Fatalf("runAPIServerProxy: failed to parse URL %v", err)
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
lc, err := ts.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get local client: %v", err)
|
||||
}
|
||||
|
||||
ap := &apiserverProxy{
|
||||
log: log,
|
||||
lc: lc,
|
||||
rp: &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
|
||||
r.Out.URL.Scheme = u.Scheme
|
||||
r.Out.URL.Host = u.Host
|
||||
if mode == apiserverProxyModeNoAuth {
|
||||
// If we are not providing authentication, then we are just
|
||||
// proxying to the Kubernetes API, so we don't need to do
|
||||
// anything else.
|
||||
return
|
||||
}
|
||||
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Out.Header.Del("Authorization")
|
||||
r.Out.Header.Del("Impersonate-Group")
|
||||
r.Out.Header.Del("Impersonate-User")
|
||||
r.Out.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Out.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
r.Out.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
if err := addImpersonationHeaders(r.Out, log); err != nil {
|
||||
panic("failed to add impersonation headers: " + err.Error())
|
||||
}
|
||||
},
|
||||
Transport: rt,
|
||||
},
|
||||
log: log,
|
||||
lc: lc,
|
||||
mode: mode,
|
||||
upstreamURL: u,
|
||||
ts: ts,
|
||||
}
|
||||
ap.rp = &httputil.ReverseProxy{
|
||||
Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
ap.addImpersonationHeadersAsRequired(pr.Out)
|
||||
},
|
||||
Transport: rt,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/", ap.serveDefault)
|
||||
mux.HandleFunc("/api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExec)
|
||||
|
||||
hs := &http.Server{
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
@@ -199,14 +183,131 @@ func runAPIServerProxy(s *tsnet.Server, rt http.RoundTripper, log *zap.SugaredLo
|
||||
NextProtos: []string{"http/1.1"},
|
||||
},
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: ap,
|
||||
Handler: mux,
|
||||
}
|
||||
log.Infof("listening on %s", ln.Addr())
|
||||
log.Infof("API server proxy in %q mode is listening on %s", mode, ln.Addr())
|
||||
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatalf("runAPIServerProxy: failed to serve %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// apiserverProxy is an [net/http.Handler] that authenticates requests using the Tailscale
|
||||
// LocalAPI and then proxies them to the Kubernetes API.
|
||||
type apiserverProxy struct {
|
||||
log *zap.SugaredLogger
|
||||
lc *tailscale.LocalClient
|
||||
rp *httputil.ReverseProxy
|
||||
|
||||
mode apiServerProxyMode
|
||||
ts *tsnet.Server
|
||||
upstreamURL *url.URL
|
||||
}
|
||||
|
||||
// serveDefault is the default handler for Kubernetes API server requests.
|
||||
func (ap *apiserverProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := ap.whoIs(r)
|
||||
if err != nil {
|
||||
ap.authError(w, err)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
}
|
||||
|
||||
// serveExec serves 'kubectl exec' requests, optionally configuring the kubectl
|
||||
// exec sessions to be recorded.
|
||||
func (ap *apiserverProxy) serveExec(w http.ResponseWriter, r *http.Request) {
|
||||
who, err := ap.whoIs(r)
|
||||
if err != nil {
|
||||
ap.authError(w, err)
|
||||
return
|
||||
}
|
||||
counterNumRequestsProxied.Add(1)
|
||||
failOpen, addrs, err := determineRecorderConfig(who)
|
||||
if err != nil {
|
||||
ap.log.Errorf("error trying to determine whether the 'kubectl exec' session needs to be recorded: %v", err)
|
||||
return
|
||||
}
|
||||
if failOpen && len(addrs) == 0 { // will not record
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
return
|
||||
}
|
||||
counterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
|
||||
if !failOpen && len(addrs) == 0 {
|
||||
msg := "forbidden: 'kubectl exec' session must be recorded, but no recorders are available."
|
||||
ap.log.Error(msg)
|
||||
http.Error(w, msg, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" || r.Header.Get("Upgrade") != "SPDY/3.1" {
|
||||
msg := "'kubectl exec' session recording is configured, but the request is not over SPDY. Session recording is currently only supported for SPDY based clients"
|
||||
if failOpen {
|
||||
msg = msg + "; failure mode is 'fail open'; continuing session without recording."
|
||||
ap.log.Warn(msg)
|
||||
ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
return
|
||||
}
|
||||
ap.log.Error(msg)
|
||||
msg += "; failure mode is 'fail closed'; closing connection."
|
||||
http.Error(w, msg, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
spdyH := &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,
|
||||
}
|
||||
|
||||
ap.rp.ServeHTTP(spdyH, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
|
||||
}
|
||||
|
||||
func (h *apiserverProxy) addImpersonationHeadersAsRequired(r *http.Request) {
|
||||
r.URL.Scheme = h.upstreamURL.Scheme
|
||||
r.URL.Host = h.upstreamURL.Host
|
||||
if h.mode == apiserverProxyModeNoAuth {
|
||||
// If we are not providing authentication, then we are just
|
||||
// proxying to the Kubernetes API, so we don't need to do
|
||||
// anything else.
|
||||
return
|
||||
}
|
||||
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Header.Del("Authorization")
|
||||
r.Header.Del("Impersonate-Group")
|
||||
r.Header.Del("Impersonate-User")
|
||||
r.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
r.Header.Del(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
if err := addImpersonationHeaders(r, h.log); err != nil {
|
||||
log.Printf("failed to add impersonation headers: " + err.Error())
|
||||
}
|
||||
}
|
||||
func (ap *apiserverProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
|
||||
return ap.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
}
|
||||
func (ap *apiserverProxy) authError(w http.ResponseWriter, err error) {
|
||||
ap.log.Errorf("failed to authenticate caller: %v", err)
|
||||
http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
const (
|
||||
// oldCapabilityName is a legacy form of
|
||||
// tailfcg.PeerCapabilityKubernetes capability. The only capability rule
|
||||
@@ -266,3 +367,34 @@ func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineRecorderConfig determines recorder config from requester's peer
|
||||
// capabilities. Determines whether a 'kubectl exec' session from this requester
|
||||
// needs to be recorded and what recorders the recording should be sent to.
|
||||
func determineRecorderConfig(who *apitype.WhoIsResponse) (failOpen bool, recorderAddresses []netip.AddrPort, _ error) {
|
||||
if who == nil {
|
||||
return false, nil, errors.New("[unexpected] cannot determine caller")
|
||||
}
|
||||
failOpen = true
|
||||
rules, err := tailcfg.UnmarshalCapJSON[tskube.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
|
||||
if err != nil {
|
||||
return failOpen, nil, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err)
|
||||
}
|
||||
if len(rules) == 0 {
|
||||
return failOpen, nil, nil
|
||||
}
|
||||
|
||||
for _, rule := range rules {
|
||||
if len(rule.RecorderAddrs) != 0 {
|
||||
// TODO (irbekrm): here or later determine if the
|
||||
// recorders behind those addrs are online - else we
|
||||
// spend 30s trying to reach a recorder whose tailscale
|
||||
// status is offline.
|
||||
recorderAddresses = append(recorderAddresses, rule.RecorderAddrs...)
|
||||
}
|
||||
if rule.EnforceRecorder {
|
||||
failOpen = false
|
||||
}
|
||||
}
|
||||
return failOpen, recorderAddresses, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -126,3 +128,72 @@ func TestImpersonationHeaders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_determineRecorderConfig(t *testing.T) {
|
||||
addr1, addr2 := netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"), netip.MustParseAddrPort("100.99.99.99:80")
|
||||
tests := []struct {
|
||||
name string
|
||||
wantFailOpen bool
|
||||
wantRecorderAddresses []netip.AddrPort
|
||||
who *apitype.WhoIsResponse
|
||||
}{
|
||||
{
|
||||
name: "two_ips_fail_closed",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
|
||||
},
|
||||
{
|
||||
name: "two_ips_fail_open",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr1, addr2},
|
||||
wantFailOpen: true,
|
||||
},
|
||||
{
|
||||
name: "odd_rule_combination_fail_closed",
|
||||
who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}),
|
||||
wantRecorderAddresses: []netip.AddrPort{addr2, addr1},
|
||||
},
|
||||
{
|
||||
name: "no_caps",
|
||||
who: whoResp(map[string][]string{}),
|
||||
wantFailOpen: true,
|
||||
},
|
||||
{
|
||||
name: "no_recorder_caps",
|
||||
who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(tailcfg.PeerCapabilityKubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}),
|
||||
wantFailOpen: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotFailOpen, gotRecorderAddresses, err := determineRecorderConfig(tt.who)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotFailOpen != tt.wantFailOpen {
|
||||
t.Errorf("determineRecorderConfig() gotFailOpen = %v, want %v", gotFailOpen, tt.wantFailOpen)
|
||||
}
|
||||
if !reflect.DeepEqual(gotRecorderAddresses, tt.wantRecorderAddresses) {
|
||||
t.Errorf("determineRecorderConfig() gotRecorderAddresses = %v, want %v", gotRecorderAddresses, tt.wantRecorderAddresses)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func whoResp(capMap map[string][]string) *apitype.WhoIsResponse {
|
||||
resp := &apitype.WhoIsResponse{
|
||||
CapMap: tailcfg.PeerCapMap{},
|
||||
}
|
||||
for cap, rules := range capMap {
|
||||
resp.CapMap[tailcfg.PeerCapability(cap)] = raw(rules...)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func raw(in ...string) []tailcfg.RawMessage {
|
||||
var out []tailcfg.RawMessage
|
||||
for _, i := range in {
|
||||
out = append(out, tailcfg.RawMessage(i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
dockerref "github.com/distribution/reference"
|
||||
"go.uber.org/zap"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
apivalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metavalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -25,6 +28,8 @@ import (
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -41,8 +46,20 @@ type ProxyClassReconciler struct {
|
||||
recorder record.EventRecorder
|
||||
logger *zap.SugaredLogger
|
||||
clock tstime.Clock
|
||||
|
||||
mu sync.Mutex // protects following
|
||||
|
||||
// managedProxyClasses is a set of all ProxyClass resources that we're currently
|
||||
// managing. This is only used for metrics.
|
||||
managedProxyClasses set.Slice[types.UID]
|
||||
}
|
||||
|
||||
var (
|
||||
// gaugeProxyClassResources tracks the number of ProxyClass resources
|
||||
// that we're currently managing.
|
||||
gaugeProxyClassResources = clientmetric.NewGauge("k8s_proxyclass_resources")
|
||||
)
|
||||
|
||||
func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (res reconcile.Result, err error) {
|
||||
logger := pcr.logger.With("ProxyClass", req.Name)
|
||||
logger.Debugf("starting reconcile")
|
||||
@@ -57,9 +74,26 @@ func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Re
|
||||
return reconcile.Result{}, fmt.Errorf("failed to get tailscale.com ProxyClass: %w", err)
|
||||
}
|
||||
if !pc.DeletionTimestamp.IsZero() {
|
||||
logger.Debugf("ProxyClass is being deleted, do nothing")
|
||||
return reconcile.Result{}, nil
|
||||
logger.Debugf("ProxyClass is being deleted")
|
||||
return reconcile.Result{}, pcr.maybeCleanup(ctx, logger, pc)
|
||||
}
|
||||
|
||||
// Add a finalizer so that we can ensure that metrics get updated when
|
||||
// this ProxyClass is deleted.
|
||||
if !slices.Contains(pc.Finalizers, FinalizerName) {
|
||||
logger.Debugf("updating ProxyClass finalizers")
|
||||
pc.Finalizers = append(pc.Finalizers, FinalizerName)
|
||||
if err := pcr.Update(ctx, pc); err != nil {
|
||||
return res, fmt.Errorf("failed to add finalizer: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure this ProxyClass is tracked in metrics.
|
||||
pcr.mu.Lock()
|
||||
pcr.managedProxyClasses.Add(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
pcr.mu.Unlock()
|
||||
|
||||
oldPCStatus := pc.Status.DeepCopy()
|
||||
if errs := pcr.validate(pc); errs != nil {
|
||||
msg := fmt.Sprintf(messageProxyClassInvalid, errs.ToAggregate().Error())
|
||||
@@ -77,7 +111,7 @@ func (pcr *ProxyClassReconciler) Reconcile(ctx context.Context, req reconcile.Re
|
||||
return reconcile.Result{}, nil
|
||||
}
|
||||
|
||||
func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
|
||||
func (pcr *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.ErrorList) {
|
||||
if sts := pc.Spec.StatefulSet; sts != nil {
|
||||
if len(sts.Labels) > 0 {
|
||||
if errs := metavalidation.ValidateLabels(sts.Labels, field.NewPath(".spec.statefulSet.labels")); errs != nil {
|
||||
@@ -103,13 +137,13 @@ func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.
|
||||
if tc := pod.TailscaleContainer; tc != nil {
|
||||
for _, e := range tc.Env {
|
||||
if strings.HasPrefix(string(e.Name), "TS_") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_TS_CONFIGFILE_PATH") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
if strings.EqualFold(string(e.Name), "EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS") {
|
||||
a.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
pcr.recorder.Event(pc, corev1.EventTypeWarning, reasonCustomTSEnvVar, fmt.Sprintf(messageCustomTSEnvVar, string(e.Name), "tailscale"))
|
||||
}
|
||||
}
|
||||
if tc.Image != "" {
|
||||
@@ -135,3 +169,27 @@ func (a *ProxyClassReconciler) validate(pc *tsapi.ProxyClass) (violations field.
|
||||
// time.
|
||||
return violations
|
||||
}
|
||||
|
||||
// maybeCleanup removes tailscale.com finalizer and ensures that the ProxyClass
|
||||
// is no longer counted towards k8s_proxyclass_resources.
|
||||
func (pcr *ProxyClassReconciler) maybeCleanup(ctx context.Context, logger *zap.SugaredLogger, pc *tsapi.ProxyClass) error {
|
||||
ix := slices.Index(pc.Finalizers, FinalizerName)
|
||||
if ix < 0 {
|
||||
logger.Debugf("no finalizer, nothing to do")
|
||||
pcr.mu.Lock()
|
||||
defer pcr.mu.Unlock()
|
||||
pcr.managedProxyClasses.Remove(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
return nil
|
||||
}
|
||||
pc.Finalizers = append(pc.Finalizers[:ix], pc.Finalizers[ix+1:]...)
|
||||
if err := pcr.Update(ctx, pc); err != nil {
|
||||
return fmt.Errorf("failed to remove finalizer: %w", err)
|
||||
}
|
||||
pcr.mu.Lock()
|
||||
defer pcr.mu.Unlock()
|
||||
pcr.managedProxyClasses.Remove(pc.UID)
|
||||
gaugeProxyClassResources.Set(int64(pcr.managedProxyClasses.Len()))
|
||||
logger.Infof("ProxyClass resources have been cleaned up")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ func TestProxyClass(t *testing.T) {
|
||||
// The apiserver is supposed to set the UID, but the fake client
|
||||
// doesn't. So, set it explicitly because other code later depends
|
||||
// on it being set.
|
||||
UID: types.UID("1234-UID"),
|
||||
UID: types.UID("1234-UID"),
|
||||
Finalizers: []string{"tailscale.com/finalizer"},
|
||||
},
|
||||
Spec: tsapi.ProxyClassSpec{
|
||||
StatefulSet: &tsapi.StatefulSet{
|
||||
|
||||
88
cmd/k8s-operator/recorder.go
Normal file
88
cmd/k8s-operator/recorder.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
// recorder knows how to send the provided bytes to the configured tsrecorder
|
||||
// instance in asciinema format.
|
||||
type recorder struct {
|
||||
start time.Time
|
||||
clock tstime.Clock
|
||||
|
||||
// failOpen specifies whether the session should be allowed to
|
||||
// continue if writing to the recording fails.
|
||||
failOpen bool
|
||||
|
||||
// backOff is set to true if we've failed open and should stop
|
||||
// attempting to write to tsrecorder.
|
||||
backOff bool
|
||||
|
||||
mu sync.Mutex // guards writes to conn
|
||||
conn io.WriteCloser // connection to a tsrecorder instance
|
||||
}
|
||||
|
||||
// Write appends timestamp to the provided bytes and sends them to the
|
||||
// configured tsrecorder.
|
||||
func (rec *recorder) Write(p []byte) (err error) {
|
||||
if len(p) == 0 {
|
||||
return nil
|
||||
}
|
||||
if rec.backOff {
|
||||
return nil
|
||||
}
|
||||
j, err := json.Marshal([]any{
|
||||
rec.clock.Now().Sub(rec.start).Seconds(),
|
||||
"o",
|
||||
string(p),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marhalling payload: %w", err)
|
||||
}
|
||||
j = append(j, '\n')
|
||||
if err := rec.writeCastLine(j); err != nil {
|
||||
if !rec.failOpen {
|
||||
return fmt.Errorf("error writing payload to recorder: %w", err)
|
||||
}
|
||||
rec.backOff = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rec *recorder) Close() error {
|
||||
rec.mu.Lock()
|
||||
defer rec.mu.Unlock()
|
||||
if rec.conn == nil {
|
||||
return nil
|
||||
}
|
||||
err := rec.conn.Close()
|
||||
rec.conn = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return errors.New("recorder closed")
|
||||
}
|
||||
_, err := rec.conn.Write(j)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recorder write error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
285
cmd/k8s-operator/spdy-frame.go
Normal file
285
cmd/k8s-operator/spdy-frame.go
Normal file
@@ -0,0 +1,285 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
SYN_STREAM ControlFrameType = 1 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.1
|
||||
SYN_REPLY ControlFrameType = 2 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.2
|
||||
SYN_PING ControlFrameType = 6 // https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.5
|
||||
)
|
||||
|
||||
// spdyFrame is a parsed SPDY frame as defined in
|
||||
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
|
||||
// A SPDY frame can be either a control frame or a data frame.
|
||||
type spdyFrame struct {
|
||||
Raw []byte // full frame as raw bytes
|
||||
|
||||
// Common frame fields:
|
||||
Ctrl bool // true if this is a SPDY control frame
|
||||
Payload []byte // payload as raw bytes
|
||||
|
||||
// Control frame fields:
|
||||
Version uint16 // SPDY protocol version
|
||||
Type ControlFrameType
|
||||
|
||||
// Data frame fields:
|
||||
// StreamID is the id of the steam to which this data frame belongs.
|
||||
// SPDY allows transmitting multiple data streams concurrently.
|
||||
StreamID uint32
|
||||
}
|
||||
|
||||
// Type of an SPDY control frame.
|
||||
type ControlFrameType uint16
|
||||
|
||||
// Parse parses bytes into spdyFrame.
|
||||
// If the bytes don't contain a full frame, return false.
|
||||
//
|
||||
// Control frame structure:
|
||||
//
|
||||
// +----------------------------------+
|
||||
// |C| Version(15bits) | Type(16bits) |
|
||||
// +----------------------------------+
|
||||
// | Flags (8) | Length (24 bits) |
|
||||
// +----------------------------------+
|
||||
// | Data |
|
||||
// +----------------------------------+
|
||||
//
|
||||
// Data frame structure:
|
||||
//
|
||||
// +----------------------------------+
|
||||
// |C| Stream-ID (31bits) |
|
||||
// +----------------------------------+
|
||||
// | Flags (8) | Length (24 bits) |
|
||||
// +----------------------------------+
|
||||
// | Data |
|
||||
// +----------------------------------+
|
||||
//
|
||||
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt
|
||||
func (sf *spdyFrame) Parse(b []byte, log *zap.SugaredLogger) (ok bool, _ error) {
|
||||
const (
|
||||
spdyHeaderLength = 8
|
||||
)
|
||||
have := len(b)
|
||||
if have < spdyHeaderLength { // input does not contain full frame
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if !isSPDYFrameHeader(b) {
|
||||
return false, fmt.Errorf("bytes %v do not seem to contain SPDY frames. Ensure that you are using a SPDY based client to 'kubectl exec'.", b)
|
||||
}
|
||||
|
||||
payloadLength := readInt24(b[5:8])
|
||||
frameLength := payloadLength + spdyHeaderLength
|
||||
if have < frameLength { // input does not contain full frame
|
||||
return false, nil
|
||||
}
|
||||
|
||||
frame := b[:frameLength:frameLength] // enforce frameLength capacity
|
||||
|
||||
sf.Raw = frame
|
||||
sf.Payload = frame[spdyHeaderLength:frameLength]
|
||||
|
||||
sf.Ctrl = hasControlBitSet(frame)
|
||||
|
||||
if !sf.Ctrl { // data frame
|
||||
sf.StreamID = dataFrameStreamID(frame)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
sf.Version = controlFrameVersion(frame)
|
||||
sf.Type = controlFrameType(frame)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// parseHeaders retrieves any headers from this spdyFrame.
|
||||
func (sf *spdyFrame) parseHeaders(z *zlibReader, log *zap.SugaredLogger) (http.Header, error) {
|
||||
if !sf.Ctrl {
|
||||
return nil, fmt.Errorf("[unexpected] parseHeaders called for a frame that is not a control frame")
|
||||
}
|
||||
const (
|
||||
// +------------------------------------+
|
||||
// |X| Stream-ID (31bits) |
|
||||
// +------------------------------------+
|
||||
// |X| Associated-To-Stream-ID (31bits) |
|
||||
// +------------------------------------+
|
||||
// | Pri|Unused | Slot | |
|
||||
// +-------------------+ |
|
||||
synStreamPayloadLengthBeforeHeaders = 10
|
||||
|
||||
// +------------------------------------+
|
||||
// |X| Stream-ID (31bits) |
|
||||
//+------------------------------------+
|
||||
synReplyPayloadLengthBeforeHeaders = 4
|
||||
|
||||
// +----------------------------------|
|
||||
// | 32-bit ID |
|
||||
// +----------------------------------+
|
||||
pingPayloadLength = 4
|
||||
)
|
||||
|
||||
switch sf.Type {
|
||||
case SYN_STREAM:
|
||||
if len(sf.Payload) < synStreamPayloadLengthBeforeHeaders {
|
||||
return nil, fmt.Errorf("SYN_STREAM frame too short: %v", len(sf.Payload))
|
||||
}
|
||||
z.Set(sf.Payload[synStreamPayloadLengthBeforeHeaders:])
|
||||
return parseHeaders(z, log)
|
||||
case SYN_REPLY:
|
||||
if len(sf.Payload) < synReplyPayloadLengthBeforeHeaders {
|
||||
return nil, fmt.Errorf("SYN_REPLY frame too short: %v", len(sf.Payload))
|
||||
}
|
||||
if len(sf.Payload) == synReplyPayloadLengthBeforeHeaders {
|
||||
return nil, nil // no headers
|
||||
}
|
||||
z.Set(sf.Payload[synReplyPayloadLengthBeforeHeaders:])
|
||||
return parseHeaders(z, log)
|
||||
case SYN_PING:
|
||||
if len(sf.Payload) != pingPayloadLength {
|
||||
return nil, fmt.Errorf("PING frame with unexpected length %v", len(sf.Payload))
|
||||
}
|
||||
return nil, nil // ping frame has no headers
|
||||
|
||||
default:
|
||||
log.Infof("[unexpected] unknown control frame type %v", sf.Type)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// parseHeaders expects to be passed a reader that contains a compressed SPDY control
|
||||
// frame Name/Value Header Block with 0 or more headers:
|
||||
//
|
||||
// | Number of Name/Value pairs (int32) | <+
|
||||
// +------------------------------------+ |
|
||||
// | Length of name (int32) | | This section is the "Name/Value
|
||||
// +------------------------------------+ | Header Block", and is compressed.
|
||||
// | Name (string) | |
|
||||
// +------------------------------------+ |
|
||||
// | Length of value (int32) | |
|
||||
// +------------------------------------+ |
|
||||
// | Value (string) | |
|
||||
// +------------------------------------+ |
|
||||
// | (repeats) | <+
|
||||
//
|
||||
// It extracts the headers and returns them as http.Header. By doing that it
|
||||
// also advances the provided reader past the headers block.
|
||||
// See also https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
|
||||
func parseHeaders(decompressor io.Reader, log *zap.SugaredLogger) (http.Header, error) {
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
defer bufPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
// readUint32 reads the next 4 decompressed bytes from the decompressor
|
||||
// as a uint32.
|
||||
readUint32 := func() (uint32, error) {
|
||||
const uint32Length = 4
|
||||
if _, err := io.CopyN(buf, decompressor, uint32Length); err != nil { // decompress
|
||||
return 0, fmt.Errorf("error decompressing bytes: %w", err)
|
||||
}
|
||||
return binary.BigEndian.Uint32(buf.Next(uint32Length)), nil // return as uint32
|
||||
}
|
||||
|
||||
// readLenBytes decompresses and returns as bytes the next 'Name' or 'Value'
|
||||
// field from SPDY Name/Value header block. decompressor must be at
|
||||
// 'Length of name'/'Length of value' field.
|
||||
readLenBytes := func() ([]byte, error) {
|
||||
xLen, err := readUint32() // length of field to read
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.CopyN(buf, decompressor, int64(xLen)); err != nil { // decompress
|
||||
return nil, err
|
||||
}
|
||||
return buf.Next(int(xLen)), nil
|
||||
}
|
||||
|
||||
numHeaders, err := readUint32()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error determining num headers: %v", err)
|
||||
}
|
||||
h := make(http.Header, numHeaders)
|
||||
for i := uint32(0); i < numHeaders; i++ {
|
||||
name, err := readLenBytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ns := string(name)
|
||||
if _, ok := h[ns]; ok {
|
||||
return nil, fmt.Errorf("invalid data: duplicate header %q", ns)
|
||||
}
|
||||
val, err := readLenBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading header data: %w", err)
|
||||
}
|
||||
for _, v := range bytes.Split(val, headerSep) {
|
||||
h.Add(ns, string(v))
|
||||
}
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// isSPDYFrame validates that the input bytes start with a valid SPDY frame
|
||||
// header.
|
||||
func isSPDYFrameHeader(f []byte) bool {
|
||||
if hasControlBitSet(f) {
|
||||
// If this is a control frame, version and type must be set.
|
||||
return controlFrameVersion(f) != uint16(0) && uint16(controlFrameType(f)) != uint16(0)
|
||||
}
|
||||
// If this is a data frame, stream ID must be set.
|
||||
return dataFrameStreamID(f) != uint32(0)
|
||||
}
|
||||
|
||||
// spdyDataFrameStreamID returns stream ID for an SPDY data frame passed as the
|
||||
// input data slice. StreaID is contained within bits [0-31) of a data frame
|
||||
// header.
|
||||
func dataFrameStreamID(frame []byte) uint32 {
|
||||
return binary.BigEndian.Uint32(frame[0:4]) & 0x7f
|
||||
}
|
||||
|
||||
// controlFrameType returns the type of a SPDY control frame.
|
||||
// See https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6
|
||||
func controlFrameType(f []byte) ControlFrameType {
|
||||
return ControlFrameType(binary.BigEndian.Uint16(f[2:4]))
|
||||
}
|
||||
|
||||
// spdyControlFrameVersion returns SPDY version extracted from input bytes that
|
||||
// must be a SPDY control frame.
|
||||
func controlFrameVersion(frame []byte) uint16 {
|
||||
bs := binary.BigEndian.Uint16(frame[0:2]) // first 16 bits
|
||||
return bs & 0x7f // discard control bit
|
||||
}
|
||||
|
||||
// hasControlBitSet returns true if the passsed bytes have SPDY control bit set.
|
||||
// SPDY frames can be either control frames or data frames. A control frame has
|
||||
// control bit set to 1 and a data frame has it set to 0.
|
||||
func hasControlBitSet(frame []byte) bool {
|
||||
return frame[0]&0x80 == 128 // 0x80
|
||||
}
|
||||
|
||||
var bufPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
// Headers in SPDY header name/value block are separated by a 0 byte.
|
||||
// https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10
|
||||
var headerSep = []byte{0}
|
||||
|
||||
func readInt24(b []byte) int {
|
||||
_ = b[2] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
||||
}
|
||||
293
cmd/k8s-operator/spdy-frame_test.go
Normal file
293
cmd/k8s-operator/spdy-frame_test.go
Normal file
@@ -0,0 +1,293 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Test_spdyFrame_Parse(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
gotBytes []byte
|
||||
wantFrame spdyFrame
|
||||
wantOk bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "control_frame_syn_stream",
|
||||
gotBytes: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0},
|
||||
wantFrame: spdyFrame{
|
||||
Version: 3,
|
||||
Type: SYN_STREAM,
|
||||
Ctrl: true,
|
||||
Raw: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0},
|
||||
Payload: []byte{},
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "control_frame_syn_reply",
|
||||
gotBytes: []byte{0x80, 0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
|
||||
wantFrame: spdyFrame{
|
||||
Ctrl: true,
|
||||
Version: 3,
|
||||
Type: SYN_REPLY,
|
||||
Raw: []byte{0x80, 0x3, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0},
|
||||
Payload: []byte{},
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "control_frame_headers",
|
||||
gotBytes: []byte{0x80, 0x3, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0},
|
||||
wantFrame: spdyFrame{
|
||||
Ctrl: true,
|
||||
Version: 3,
|
||||
Type: 8,
|
||||
Raw: []byte{0x80, 0x3, 0x0, 0x8, 0x0, 0x0, 0x0, 0x0},
|
||||
Payload: []byte{},
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "data_frame_stream_id_5",
|
||||
gotBytes: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0},
|
||||
wantFrame: spdyFrame{
|
||||
Payload: []byte{},
|
||||
StreamID: 5,
|
||||
Raw: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "frame_with_incomplete_header",
|
||||
gotBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
{
|
||||
name: "frame_with_incomplete_payload",
|
||||
gotBytes: []byte{0x0, 0x0, 0x0, 0x5, 0x0, 0x0, 0x0, 0x2}, // header specifies payload length of 2
|
||||
},
|
||||
{
|
||||
name: "control_bit_set_not_spdy_frame",
|
||||
gotBytes: []byte{0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, // header specifies payload length of 2
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "control_bit_not_set_not_spdy_frame",
|
||||
gotBytes: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, // header specifies payload length of 2
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sf := &spdyFrame{}
|
||||
gotOk, err := sf.Parse(tt.gotBytes, zl.Sugar())
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("spdyFrame.Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotOk != tt.wantOk {
|
||||
t.Errorf("spdyFrame.Parse() = %v, want %v", gotOk, tt.wantOk)
|
||||
}
|
||||
if diff := cmp.Diff(*sf, tt.wantFrame); diff != "" {
|
||||
t.Errorf("Unexpected SPDY frame (-got +want):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_spdyFrame_parseHeaders(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
isCtrl bool
|
||||
payload []byte
|
||||
typ ControlFrameType
|
||||
wantHeader http.Header
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "syn_stream_with_header",
|
||||
payload: payload(t, map[string]string{"Streamtype": "stdin"}, SYN_STREAM, 1),
|
||||
typ: SYN_STREAM,
|
||||
isCtrl: true,
|
||||
wantHeader: header(map[string]string{"Streamtype": "stdin"}),
|
||||
},
|
||||
{
|
||||
name: "syn_ping",
|
||||
payload: payload(t, nil, SYN_PING, 0),
|
||||
typ: SYN_PING,
|
||||
isCtrl: true,
|
||||
},
|
||||
{
|
||||
name: "syn_reply_headers",
|
||||
payload: payload(t, map[string]string{"foo": "bar", "bar": "baz"}, SYN_REPLY, 0),
|
||||
typ: SYN_REPLY,
|
||||
isCtrl: true,
|
||||
wantHeader: header(map[string]string{"foo": "bar", "bar": "baz"}),
|
||||
},
|
||||
{
|
||||
name: "syn_reply_no_headers",
|
||||
payload: payload(t, nil, SYN_REPLY, 0),
|
||||
typ: SYN_REPLY,
|
||||
isCtrl: true,
|
||||
},
|
||||
{
|
||||
name: "syn_stream_too_short_payload",
|
||||
payload: []byte{0, 1, 2, 3, 4},
|
||||
typ: SYN_STREAM,
|
||||
isCtrl: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "syn_reply_too_short_payload",
|
||||
payload: []byte{0, 1, 2},
|
||||
typ: SYN_REPLY,
|
||||
isCtrl: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "syn_ping_too_short_payload",
|
||||
payload: []byte{0, 1, 2},
|
||||
typ: SYN_PING,
|
||||
isCtrl: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not_a_control_frame",
|
||||
payload: []byte{0, 1, 2, 3},
|
||||
typ: SYN_PING,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
var reader zlibReader
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sf := &spdyFrame{
|
||||
Ctrl: tt.isCtrl,
|
||||
Type: tt.typ,
|
||||
Payload: tt.payload,
|
||||
}
|
||||
gotHeader, err := sf.parseHeaders(&reader, zl.Sugar())
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("spdyFrame.parseHeaders() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(gotHeader, tt.wantHeader) {
|
||||
t.Errorf("spdyFrame.parseHeaders() = %v, want %v", gotHeader, tt.wantHeader)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// payload takes a control frame type and a map with 0 or more header keys and
|
||||
// values and returns a SPDY control frame payload with the header as SPDY zlib
|
||||
// compressed header name/value block. The payload is padded with arbitrary
|
||||
// bytes to ensure the header name/value block is in the correct position for
|
||||
// the frame type.
|
||||
func payload(t *testing.T, headerM map[string]string, typ ControlFrameType, streamID int) []byte {
|
||||
t.Helper()
|
||||
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
writeControlFramePayloadBeforeHeaders(t, buf, typ, streamID)
|
||||
if len(headerM) == 0 {
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
w, err := zlib.NewWriterLevelDict(buf, zlib.BestCompression, spdyTxtDictionary)
|
||||
if err != nil {
|
||||
t.Fatalf("error creating new zlib writer: %v", err)
|
||||
}
|
||||
if len(headerM) != 0 {
|
||||
writeHeaderValueBlock(t, w, headerM)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("error writing headers: %v", err)
|
||||
}
|
||||
w.Flush()
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// writeControlFramePayloadBeforeHeaders writes to w N bytes, N being the number
|
||||
// of bytes that control frame payload for that control frame is required to
|
||||
// contain before the name/value header block.
|
||||
func writeControlFramePayloadBeforeHeaders(t *testing.T, w io.Writer, typ ControlFrameType, streamID int) {
|
||||
t.Helper()
|
||||
switch typ {
|
||||
case SYN_STREAM:
|
||||
// needs 10 bytes in payload before any headers
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(streamID)); err != nil {
|
||||
t.Fatalf("writing streamID: %v", err)
|
||||
}
|
||||
if err := binary.Write(w, binary.BigEndian, [6]byte{0}); err != nil {
|
||||
t.Fatalf("writing payload: %v", err)
|
||||
}
|
||||
case SYN_REPLY:
|
||||
// needs 4 bytes in payload before any headers
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(0)); err != nil {
|
||||
t.Fatalf("writing payload: %v", err)
|
||||
}
|
||||
case SYN_PING:
|
||||
// needs 4 bytes in payload
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(0)); err != nil {
|
||||
t.Fatalf("writing payload: %v", err)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("unexpected frame type: %v", typ)
|
||||
}
|
||||
}
|
||||
|
||||
// writeHeaderValue block takes http.Header and zlib writer, writes the headers
|
||||
// as SPDY zlib compressed bytes to the writer.
|
||||
// Adopted from https://github.com/moby/spdystream/blob/v0.2.0/spdy/write.go#L171-L198 (which is also what Kubernetes uses).
|
||||
func writeHeaderValueBlock(t *testing.T, w io.Writer, headerM map[string]string) {
|
||||
t.Helper()
|
||||
h := header(headerM)
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(h))); err != nil {
|
||||
t.Fatalf("error writing header block length: %v", err)
|
||||
}
|
||||
for name, values := range h {
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(name))); err != nil {
|
||||
t.Fatalf("error writing name length for name %q: %v", name, err)
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
if _, err := io.WriteString(w, name); err != nil {
|
||||
t.Fatalf("error writing name %q: %v", name, err)
|
||||
}
|
||||
v := strings.Join(values, string(headerSep))
|
||||
if err := binary.Write(w, binary.BigEndian, uint32(len(v))); err != nil {
|
||||
t.Fatalf("error writing value length for value %q: %v", v, err)
|
||||
}
|
||||
if _, err := io.WriteString(w, v); err != nil {
|
||||
t.Fatalf("error writing value %q: %v", v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func header(hs map[string]string) http.Header {
|
||||
h := make(http.Header, len(hs))
|
||||
for key, val := range hs {
|
||||
h.Add(key, val)
|
||||
}
|
||||
return h
|
||||
}
|
||||
213
cmd/k8s-operator/spdy-hijacker.go
Normal file
213
cmd/k8s-operator/spdy-hijacker.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// spdyHijacker 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 {
|
||||
http.ResponseWriter
|
||||
ts *tsnet.Server
|
||||
req *http.Request
|
||||
who *apitype.WhoIsResponse
|
||||
log *zap.SugaredLogger
|
||||
pod string // pod being exec-d
|
||||
ns string // namespace of the pod being exec-d
|
||||
addrs []netip.AddrPort // tsrecorder addresses
|
||||
failOpen bool // whether to fail open if recording fails
|
||||
connectToRecorder RecorderDialFn
|
||||
}
|
||||
|
||||
// RecorderDialFn dials the specified netip.AddrPorts that should be tsrecorder
|
||||
// addresses. It tries to connect to recorder endpoints one by one, till one
|
||||
// connection succeeds. In case of success, returns a list with a single
|
||||
// successful recording attempt and an error channel. If the connection errors
|
||||
// after having been established, an error is sent down the channel.
|
||||
type RecorderDialFn func(context.Context, []netip.AddrPort, func(context.Context, string, string) (net.Conn, error)) (io.WriteCloser, []*tailcfg.SSHRecordingAttempt, <-chan error, error)
|
||||
|
||||
// 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) {
|
||||
h.log.Infof("recorder addrs: %v, failOpen: %v", h.addrs, h.failOpen)
|
||||
reqConn, brw, err := h.ResponseWriter.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error hijacking connection: %w", err)
|
||||
}
|
||||
|
||||
conn, err := h.setUpRecording(context.Background(), reqConn)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error setting up session recording: %w", err)
|
||||
}
|
||||
return conn, brw, nil
|
||||
}
|
||||
|
||||
// setupRecording attempts to connect to the recorders set via
|
||||
// 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) {
|
||||
const (
|
||||
// https://docs.asciinema.org/manual/asciicast/v2/
|
||||
asciicastv2 = 2
|
||||
)
|
||||
var wc io.WriteCloser
|
||||
h.log.Infof("kubectl exec session will be recorded, recorders: %v, fail open policy: %t", h.addrs, h.failOpen)
|
||||
// TODO (irbekrm): send client a message that session will be recorded.
|
||||
rw, _, errChan, err := h.connectToRecorder(ctx, h.addrs, h.ts.Dial)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("error connecting to session recorders: %v", err)
|
||||
if h.failOpen {
|
||||
msg = msg + "; failure mode is 'fail open'; continuing session without recording."
|
||||
h.log.Warnf(msg)
|
||||
return conn, nil
|
||||
}
|
||||
msg = msg + "; failure mode is 'fail closed'; closing connection."
|
||||
if err := closeConnWithWarning(conn, msg); err != nil {
|
||||
return nil, multierr.New(errors.New(msg), err)
|
||||
}
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
// TODO (irbekrm): log which recorder
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
qp := h.req.URL.Query()
|
||||
ch := CastHeader{
|
||||
Version: asciicastv2,
|
||||
Timestamp: lc.rec.start.Unix(),
|
||||
Command: strings.Join(qp["command"], " "),
|
||||
SrcNode: strings.TrimSuffix(h.who.Node.Name, "."),
|
||||
SrcNodeID: h.who.Node.StableID,
|
||||
Kubernetes: &Kubernetes{
|
||||
PodName: h.pod,
|
||||
Namespace: h.ns,
|
||||
Container: strings.Join(qp["container"], " "),
|
||||
},
|
||||
}
|
||||
if !h.who.Node.IsTagged() {
|
||||
ch.SrcNodeUser = h.who.UserProfile.LoginName
|
||||
ch.SrcNodeUserID = h.who.Node.User
|
||||
} else {
|
||||
ch.SrcNodeTags = h.who.Node.Tags
|
||||
}
|
||||
lc.ch = ch
|
||||
go func() {
|
||||
var err error
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err = <-errChan:
|
||||
}
|
||||
if err == nil {
|
||||
counterSessionRecordingsUploaded.Add(1)
|
||||
h.log.Info("finished uploading the recording")
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf("connection to the session recorder errorred: %v;", err)
|
||||
if h.failOpen {
|
||||
msg += msg + "; failure mode is 'fail open'; continuing session without recording."
|
||||
h.log.Info(msg)
|
||||
return
|
||||
}
|
||||
msg += "; failure mode set to 'fail closed'; closing connection"
|
||||
h.log.Error(msg)
|
||||
lc.failed = true
|
||||
// TODO (irbekrm): write a message to the client
|
||||
if err := lc.Close(); err != nil {
|
||||
h.log.Infof("error closing recorder connections: %v", err)
|
||||
}
|
||||
return
|
||||
}()
|
||||
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
|
||||
Container 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}
|
||||
if err := resp.Write(conn); err != nil {
|
||||
return multierr.New(fmt.Errorf("error writing msg %q to conn: %v", msg, err), conn.Close())
|
||||
}
|
||||
return conn.Close()
|
||||
}
|
||||
111
cmd/k8s-operator/spdy-hijacker_test.go
Normal file
111
cmd/k8s-operator/spdy-hijacker_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func Test_SPDYHijacker(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
failOpen bool
|
||||
failRecorderConnect bool // fail initial connect to the recorder
|
||||
failRecorderConnPostConnect bool // send error down the error channel
|
||||
wantsConnClosed bool
|
||||
wantsSetupErr bool
|
||||
}{
|
||||
{
|
||||
name: "setup succeeds, conn stays open",
|
||||
},
|
||||
{
|
||||
name: "setup fails, policy is to fail open, conn stays open",
|
||||
failOpen: true,
|
||||
failRecorderConnect: true,
|
||||
},
|
||||
{
|
||||
name: "setup fails, policy is to fail closed, conn is closed",
|
||||
failRecorderConnect: true,
|
||||
wantsSetupErr: true,
|
||||
wantsConnClosed: true,
|
||||
},
|
||||
{
|
||||
name: "connection fails post-initial connect, policy is to fail open, conn stays open",
|
||||
failRecorderConnPostConnect: true,
|
||||
failOpen: true,
|
||||
},
|
||||
{
|
||||
name: "connection fails post-initial connect, policy is to fail closed, conn is closed",
|
||||
failRecorderConnPostConnect: true,
|
||||
wantsConnClosed: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tc := &testConn{}
|
||||
ch := make(chan error)
|
||||
h := &spdyHijacker{
|
||||
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")
|
||||
}
|
||||
return wc, rec, ch, err
|
||||
},
|
||||
failOpen: tt.failOpen,
|
||||
who: &apitype.WhoIsResponse{Node: &tailcfg.Node{}, UserProfile: &tailcfg.UserProfile{}},
|
||||
log: zl.Sugar(),
|
||||
ts: &tsnet.Server{},
|
||||
req: &http.Request{URL: &url.URL{}},
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err := h.setUpRecording(ctx, tc)
|
||||
if (err != nil) != tt.wantsSetupErr {
|
||||
t.Errorf("spdyHijacker.setupRecording() error = %v, wantErr %v", err, tt.wantsSetupErr)
|
||||
return
|
||||
}
|
||||
if tt.failRecorderConnPostConnect {
|
||||
select {
|
||||
case ch <- errors.New("err"):
|
||||
case <-time.After(time.Second * 15):
|
||||
t.Errorf("error from recorder conn was not read within 15 seconds")
|
||||
}
|
||||
}
|
||||
timeout := time.Second * 20
|
||||
// TODO (irbekrm): cover case where an error is received
|
||||
// over channel and the failure policy is to fail open
|
||||
// (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)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Errorf("connection did not reach the desired state within %s", timeout.String())
|
||||
}
|
||||
ctx.Done()
|
||||
})
|
||||
}
|
||||
}
|
||||
194
cmd/k8s-operator/spdy-remote-conn-recorder.go
Normal file
194
cmd/k8s-operator/spdy-remote-conn-recorder.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"go.uber.org/zap"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
net.Conn
|
||||
// rec knows how to send data written to it to a tsrecorder instance.
|
||||
rec *recorder
|
||||
ch CastHeader
|
||||
|
||||
stdoutStreamID atomic.Uint32
|
||||
stderrStreamID atomic.Uint32
|
||||
resizeStreamID atomic.Uint32
|
||||
|
||||
wmu sync.Mutex // sequences writes
|
||||
closed bool
|
||||
failed bool
|
||||
|
||||
rmu sync.Mutex // sequences reads
|
||||
writeCastHeaderOnce sync.Once
|
||||
|
||||
zlibReqReader zlibReader
|
||||
// writeBuf is used to store data written to the connection that has not
|
||||
// yet been parsed as SPDY frames.
|
||||
writeBuf bytes.Buffer
|
||||
// readBuf is used to store data read from the connection that has not
|
||||
// yet been parsed as SPDY frames.
|
||||
readBuf bytes.Buffer
|
||||
log *zap.SugaredLogger
|
||||
}
|
||||
|
||||
// Read reads bytes from the original connection and parses them as SPDY frames.
|
||||
// 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) {
|
||||
c.rmu.Lock()
|
||||
defer c.rmu.Unlock()
|
||||
n, err := c.Conn.Read(b)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("error reading from connection: %w", err)
|
||||
}
|
||||
c.readBuf.Write(b[:n])
|
||||
|
||||
var sf spdyFrame
|
||||
ok, err := sf.Parse(c.readBuf.Bytes(), c.log)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing data read from connection: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
// The parsed data in the buffer will be processed together with
|
||||
// the new data on the next call to Read.
|
||||
return n, nil
|
||||
}
|
||||
c.readBuf.Next(len(sf.Raw)) // advance buffer past the parsed frame
|
||||
|
||||
if !sf.Ctrl { // data frame
|
||||
switch sf.StreamID {
|
||||
case c.resizeStreamID.Load():
|
||||
var err error
|
||||
var msg spdyResizeMsg
|
||||
if err = json.Unmarshal(sf.Payload, &msg); err != nil {
|
||||
return 0, fmt.Errorf("error umarshalling resize msg: %w", err)
|
||||
}
|
||||
c.ch.Width = msg.Width
|
||||
c.ch.Height = msg.Height
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
// We always want to parse the headers, even if we don't care about the
|
||||
// frame, as we need to advance the zlib reader otherwise we will get
|
||||
// garbage.
|
||||
header, err := sf.parseHeaders(&c.zlibReqReader, c.log)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing frame headers: %w", err)
|
||||
}
|
||||
if sf.Type == SYN_STREAM {
|
||||
c.storeStreamID(sf, header)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
c.writeBuf.Write(b)
|
||||
|
||||
var sf spdyFrame
|
||||
ok, err := sf.Parse(c.writeBuf.Bytes(), c.log)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error parsing data: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
// The parsed data in the buffer will be processed together with
|
||||
// the new data on the next call to Write.
|
||||
return len(b), nil
|
||||
}
|
||||
c.writeBuf.Next(len(sf.Raw)) // advance buffer past the parsed frame
|
||||
|
||||
// If this is a stdout or stderr data frame, send its payload to the
|
||||
// session recorder.
|
||||
if !sf.Ctrl {
|
||||
switch sf.StreamID {
|
||||
case c.stdoutStreamID.Load(), c.stderrStreamID.Load():
|
||||
var err error
|
||||
c.writeCastHeaderOnce.Do(func() {
|
||||
var j []byte
|
||||
j, err = json.Marshal(c.ch)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
j = append(j, '\n')
|
||||
err = c.rec.writeCastLine(j)
|
||||
if err != nil {
|
||||
c.log.Errorf("received error from recorder: %v", err)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error writing CastHeader: %w", err)
|
||||
}
|
||||
if err := c.rec.Write(sf.Payload); err != nil {
|
||||
return 0, fmt.Errorf("error sending payload to session recorder: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Forward the whole frame to the original destination.
|
||||
_, err = c.Conn.Write(sf.Raw) // send to net.Conn
|
||||
return len(b), err
|
||||
}
|
||||
|
||||
func (c *spdyRemoteConnRecorder) Close() error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if c.closed {
|
||||
return nil
|
||||
}
|
||||
if !c.failed && c.writeBuf.Len() > 0 {
|
||||
c.Conn.Write(c.writeBuf.Bytes())
|
||||
}
|
||||
c.writeBuf.Reset()
|
||||
c.closed = true
|
||||
err := c.Conn.Close()
|
||||
c.rec.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// parseSynStream 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) {
|
||||
const (
|
||||
streamTypeHeaderKey = "Streamtype"
|
||||
)
|
||||
id := binary.BigEndian.Uint32(sf.Payload[0:4])
|
||||
switch header.Get(streamTypeHeaderKey) {
|
||||
case corev1.StreamTypeStdout:
|
||||
c.stdoutStreamID.Store(id)
|
||||
case corev1.StreamTypeStderr:
|
||||
c.stderrStreamID.Store(id)
|
||||
case corev1.StreamTypeResize:
|
||||
c.resizeStreamID.Store(id)
|
||||
}
|
||||
}
|
||||
|
||||
type spdyResizeMsg struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
326
cmd/k8s-operator/spdy-remote-conn-recorder_test.go
Normal file
326
cmd/k8s-operator/spdy-remote-conn-recorder_test.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstime"
|
||||
)
|
||||
|
||||
// Test_Writes tests that 1 or more Write calls to spdyRemoteConnRecorder
|
||||
// results in the expected data being forwarded to the original destination and
|
||||
// the session recorder.
|
||||
func Test_Writes(t *testing.T) {
|
||||
var stdoutStreamID, stderrStreamID uint32 = 1, 2
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs [][]byte
|
||||
wantForwarded []byte
|
||||
wantRecorded []byte
|
||||
firstWrite bool
|
||||
width int
|
||||
height int
|
||||
}{
|
||||
{
|
||||
name: "single_write_control_frame_with_payload",
|
||||
inputs: [][]byte{{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x5}},
|
||||
wantForwarded: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x5},
|
||||
},
|
||||
{
|
||||
name: "two_writes_control_frame_with_leftover",
|
||||
inputs: [][]byte{{0x80, 0x3, 0x0, 0x1}, {0x0, 0x0, 0x0, 0x1, 0x5, 0x80, 0x3}},
|
||||
wantForwarded: []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, 0x5},
|
||||
},
|
||||
{
|
||||
name: "single_write_stdout_data_frame",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0}},
|
||||
wantForwarded: []byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
{
|
||||
name: "single_data_frame_unknow_stream_with_payload",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5}},
|
||||
wantForwarded: []byte{0x0, 0x0, 0x0, 0x7, 0x0, 0x0, 0x0, 0x5, 0x1, 0x2, 0x3, 0x4, 0x5},
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
{
|
||||
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)...),
|
||||
width: 10,
|
||||
height: 20,
|
||||
firstWrite: true,
|
||||
},
|
||||
}
|
||||
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{
|
||||
Conn: tc,
|
||||
log: zl.Sugar(),
|
||||
rec: rec,
|
||||
ch: CastHeader{
|
||||
Width: tt.width,
|
||||
Height: tt.height,
|
||||
},
|
||||
}
|
||||
if !tt.firstWrite {
|
||||
// this test case does not intend to test that cast header gets written once
|
||||
c.writeCastHeaderOnce.Do(func() {})
|
||||
}
|
||||
|
||||
c.stdoutStreamID.Store(stdoutStreamID)
|
||||
c.stderrStreamID.Store(stderrStreamID)
|
||||
for i, input := range tt.inputs {
|
||||
if _, err := c.Write(input); err != nil {
|
||||
t.Errorf("[%d] spdyRemoteConnRecorder.Write() unexpected error %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Assert that the expected bytes have been forwarded to the original destination.
|
||||
gotForwarded := tc.writeBuf.Bytes()
|
||||
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()
|
||||
if !reflect.DeepEqual(gotRecorded, tt.wantRecorded) {
|
||||
t.Errorf("expected bytes not recorded, wants\n%v\ngot\n%v", tt.wantRecorded, gotRecorded)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test_Reads tests that 1 or more Read calls to spdyRemoteConnRecorder results
|
||||
// in the expected data being forwarded to the original destination and the
|
||||
// session recorder.
|
||||
func Test_Reads(t *testing.T) {
|
||||
zl, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cl := tstest.NewClock(tstest.ClockOpts{})
|
||||
var reader zlibReader
|
||||
resizeMsg := resizeMsgBytes(t, 10, 20)
|
||||
synStreamStdoutPayload := payload(t, map[string]string{"Streamtype": "stdout"}, SYN_STREAM, 1)
|
||||
synStreamStderrPayload := payload(t, map[string]string{"Streamtype": "stderr"}, SYN_STREAM, 2)
|
||||
synStreamResizePayload := payload(t, map[string]string{"Streamtype": "resize"}, SYN_STREAM, 3)
|
||||
syn_stream_ctrl_header := []byte{0x80, 0x3, 0x0, 0x1, 0x0, 0x0, 0x0, uint8(len(synStreamStdoutPayload))}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
inputs [][]byte
|
||||
wantStdoutStreamID uint32
|
||||
wantStderrStreamID uint32
|
||||
wantResizeStreamID uint32
|
||||
wantWidth int
|
||||
wantHeight int
|
||||
resizeStreamIDBeforeRead uint32
|
||||
}{
|
||||
{
|
||||
name: "resize_data_frame_single_read",
|
||||
inputs: [][]byte{append([]byte{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, uint8(len(resizeMsg))}, resizeMsg...)},
|
||||
resizeStreamIDBeforeRead: 1,
|
||||
wantWidth: 10,
|
||||
wantHeight: 20,
|
||||
},
|
||||
{
|
||||
name: "resize_data_frame_two_reads",
|
||||
inputs: [][]byte{{0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, uint8(len(resizeMsg))}, resizeMsg},
|
||||
resizeStreamIDBeforeRead: 1,
|
||||
wantWidth: 10,
|
||||
wantHeight: 20,
|
||||
},
|
||||
{
|
||||
name: "syn_stream_ctrl_frame_stdout_single_read",
|
||||
inputs: [][]byte{append(syn_stream_ctrl_header, synStreamStdoutPayload...)},
|
||||
wantStdoutStreamID: 1,
|
||||
},
|
||||
{
|
||||
name: "syn_stream_ctrl_frame_stderr_single_read",
|
||||
inputs: [][]byte{append(syn_stream_ctrl_header, synStreamStderrPayload...)},
|
||||
wantStderrStreamID: 2,
|
||||
},
|
||||
{
|
||||
name: "syn_stream_ctrl_frame_resize_single_read",
|
||||
inputs: [][]byte{append(syn_stream_ctrl_header, synStreamResizePayload...)},
|
||||
wantResizeStreamID: 3,
|
||||
},
|
||||
{
|
||||
name: "syn_stream_ctrl_frame_resize_four_reads_with_leftover",
|
||||
inputs: [][]byte{syn_stream_ctrl_header, append(synStreamResizePayload, syn_stream_ctrl_header...), append(synStreamStderrPayload, syn_stream_ctrl_header...), append(synStreamStdoutPayload, 0x0, 0x3)},
|
||||
wantStdoutStreamID: 1,
|
||||
wantStderrStreamID: 2,
|
||||
wantResizeStreamID: 3,
|
||||
},
|
||||
}
|
||||
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{
|
||||
Conn: tc,
|
||||
log: zl.Sugar(),
|
||||
rec: rec,
|
||||
}
|
||||
c.resizeStreamID.Store(tt.resizeStreamIDBeforeRead)
|
||||
|
||||
for i, input := range tt.inputs {
|
||||
c.zlibReqReader = reader
|
||||
tc.readBuf.Reset()
|
||||
_, err := tc.readBuf.Write(input)
|
||||
if err != nil {
|
||||
t.Fatalf("writing bytes to test conn: %v", err)
|
||||
}
|
||||
_, err = c.Read(make([]byte, len(input)))
|
||||
if err != nil {
|
||||
t.Errorf("[%d] spdyRemoteConnRecorder.Read() resulted in an unexpected error: %v", i, err)
|
||||
}
|
||||
}
|
||||
if id := c.resizeStreamID.Load(); id != tt.wantResizeStreamID && id != tt.resizeStreamIDBeforeRead {
|
||||
t.Errorf("wants resizeStreamID: %d, got %d", tt.wantResizeStreamID, id)
|
||||
}
|
||||
if id := c.stderrStreamID.Load(); id != tt.wantStderrStreamID {
|
||||
t.Errorf("wants stderrStreamID: %d, got %d", tt.wantStderrStreamID, id)
|
||||
}
|
||||
if id := c.stdoutStreamID.Load(); id != tt.wantStdoutStreamID {
|
||||
t.Errorf("wants stdoutStreamID: %d, got %d", tt.wantStdoutStreamID, id)
|
||||
}
|
||||
if tt.wantHeight != 0 || tt.wantWidth != 0 {
|
||||
if tt.wantWidth != c.ch.Width {
|
||||
t.Errorf("wants width: %v, got %v", tt.wantWidth, c.ch.Width)
|
||||
}
|
||||
if tt.wantHeight != c.ch.Height {
|
||||
t.Errorf("want height: %v, got %v", tt.wantHeight, c.ch.Height)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling resizeMsg: %v", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
221
cmd/k8s-operator/zlib-reader.go
Normal file
221
cmd/k8s-operator/zlib-reader.go
Normal file
@@ -0,0 +1,221 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"io"
|
||||
)
|
||||
|
||||
// zlibReader contains functionality to parse zlib compressed SPDY data.
|
||||
// See https://www.ietf.org/archive/id/draft-mbelshe-httpbis-spdy-00.txt section 2.6.10.1
|
||||
type zlibReader struct {
|
||||
io.ReadCloser
|
||||
underlying io.LimitedReader // zlib compressed SPDY data
|
||||
}
|
||||
|
||||
// Read decompresses zlibReader's underlying zlib compressed SPDY data and reads
|
||||
// it into b.
|
||||
func (z *zlibReader) Read(b []byte) (int, error) {
|
||||
if z.ReadCloser == nil {
|
||||
r, err := zlib.NewReaderDict(&z.underlying, spdyTxtDictionary)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
z.ReadCloser = r
|
||||
}
|
||||
return z.ReadCloser.Read(b)
|
||||
}
|
||||
|
||||
// Set sets zlibReader's underlying data. b must be zlib compressed SPDY data.
|
||||
func (z *zlibReader) Set(b []byte) {
|
||||
z.underlying.R = bytes.NewReader(b)
|
||||
z.underlying.N = int64(len(b))
|
||||
}
|
||||
|
||||
// spdyTxtDictionary is the dictionary defined in the SPDY spec.
|
||||
// https://datatracker.ietf.org/doc/html/draft-mbelshe-httpbis-spdy-00#section-2.6.10.1
|
||||
var spdyTxtDictionary = []byte{
|
||||
0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i
|
||||
0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h
|
||||
0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p
|
||||
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p
|
||||
0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e
|
||||
0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - -
|
||||
0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - -
|
||||
0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t -
|
||||
0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p
|
||||
0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e
|
||||
0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c
|
||||
0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o
|
||||
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - -
|
||||
0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l
|
||||
0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e -
|
||||
0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p
|
||||
0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s
|
||||
0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e -
|
||||
0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w
|
||||
0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h
|
||||
0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o
|
||||
0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c
|
||||
0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r
|
||||
0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o
|
||||
0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n
|
||||
0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
|
||||
0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e
|
||||
0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
|
||||
0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o
|
||||
0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - -
|
||||
0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t -
|
||||
0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e
|
||||
0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t
|
||||
0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g
|
||||
0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o
|
||||
0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - -
|
||||
0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n
|
||||
0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - -
|
||||
0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t
|
||||
0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - -
|
||||
0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n
|
||||
0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - -
|
||||
0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - -
|
||||
0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - -
|
||||
0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t
|
||||
0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i
|
||||
0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f
|
||||
0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h
|
||||
0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i
|
||||
0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h -
|
||||
0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o
|
||||
0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s
|
||||
0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - -
|
||||
0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e -
|
||||
0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - -
|
||||
0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g
|
||||
0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f -
|
||||
0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i
|
||||
0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e
|
||||
0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t
|
||||
0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e
|
||||
0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - -
|
||||
0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r
|
||||
0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - -
|
||||
0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a -
|
||||
0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y
|
||||
0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t
|
||||
0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - -
|
||||
0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a
|
||||
0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - -
|
||||
0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - -
|
||||
0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r
|
||||
0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r
|
||||
0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r -
|
||||
0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e
|
||||
0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e -
|
||||
0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l
|
||||
0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r
|
||||
0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e
|
||||
0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g -
|
||||
0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a
|
||||
0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s
|
||||
0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t
|
||||
0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y
|
||||
0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a -
|
||||
0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i
|
||||
0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w
|
||||
0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n
|
||||
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - -
|
||||
0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d
|
||||
0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t -
|
||||
0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u
|
||||
0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0
|
||||
0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v
|
||||
0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - -
|
||||
0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1
|
||||
0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r
|
||||
0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b
|
||||
0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s
|
||||
0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i
|
||||
0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e
|
||||
0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e -
|
||||
0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i
|
||||
0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2
|
||||
0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5
|
||||
0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0
|
||||
0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3
|
||||
0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7
|
||||
0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0
|
||||
0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4
|
||||
0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1
|
||||
0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1
|
||||
0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4
|
||||
0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4
|
||||
0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N
|
||||
0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o
|
||||
0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e
|
||||
0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 -
|
||||
0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e
|
||||
0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o
|
||||
0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m
|
||||
0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4
|
||||
0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R
|
||||
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0
|
||||
0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h
|
||||
0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0
|
||||
0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d
|
||||
0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N
|
||||
0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d
|
||||
0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e
|
||||
0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r
|
||||
0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o
|
||||
0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t
|
||||
0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e
|
||||
0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 -
|
||||
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e -
|
||||
0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a
|
||||
0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F
|
||||
0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A
|
||||
0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J
|
||||
0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A
|
||||
0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t -
|
||||
0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v -
|
||||
0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0
|
||||
0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n
|
||||
0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W
|
||||
0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u -
|
||||
0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a
|
||||
0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - -
|
||||
0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k
|
||||
0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t -
|
||||
0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a
|
||||
0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i
|
||||
0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g
|
||||
0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g
|
||||
0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x
|
||||
0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i
|
||||
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x
|
||||
0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l
|
||||
0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l
|
||||
0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t
|
||||
0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r
|
||||
0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l
|
||||
0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t
|
||||
0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e
|
||||
0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e
|
||||
0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d
|
||||
0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e
|
||||
0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c
|
||||
0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i
|
||||
0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 -
|
||||
0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - -
|
||||
0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e, // - e n q - 0 -
|
||||
}
|
||||
@@ -448,7 +448,7 @@ func (c *connector) handleTCPFlow(src, dst netip.AddrPort) (handler func(net.Con
|
||||
// in --ignore-destinations
|
||||
func (c *connector) ignoreDestination(dstAddrs []netip.Addr) bool {
|
||||
for _, a := range dstAddrs {
|
||||
if _, ok := c.ignoreDsts.Get(a); ok {
|
||||
if _, ok := c.ignoreDsts.Lookup(a); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -489,7 +489,7 @@ type perPeerState struct {
|
||||
func (ps *perPeerState) domainForIP(ip netip.Addr) (_ string, ok bool) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
return ps.addrToDomain.Get(ip)
|
||||
return ps.addrToDomain.Lookup(ip)
|
||||
}
|
||||
|
||||
// ipForDomain assigns a pair of unique IP addresses for the given domain and
|
||||
@@ -515,7 +515,7 @@ func (ps *perPeerState) ipForDomain(domain string) ([]netip.Addr, error) {
|
||||
// domain.
|
||||
// ps.mu must be held.
|
||||
func (ps *perPeerState) isIPUsedLocked(ip netip.Addr) bool {
|
||||
_, ok := ps.addrToDomain.Get(ip)
|
||||
_, ok := ps.addrToDomain.Lookup(ip)
|
||||
return ok
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
||||
|
||||
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
|
||||
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -210,6 +210,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
}
|
||||
}
|
||||
if maskedPrefs.AutoUpdateSet.ApplySet {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
// On macsys, tailscaled will set the Sparkle auto-update setting. It
|
||||
// does not use clientupdate.
|
||||
if version.IsMacSysExt() {
|
||||
@@ -221,10 +224,6 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable automatic updates: %v, %q", err, out)
|
||||
}
|
||||
} else {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return errors.New("automatic updates are not supported on this platform")
|
||||
}
|
||||
}
|
||||
}
|
||||
checkPrefs := curPrefs.Clone()
|
||||
|
||||
@@ -9,7 +9,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
|
||||
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json+
|
||||
|
||||
@@ -90,7 +90,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun+
|
||||
github.com/go-json-experiment/json from tailscale.com/types/views
|
||||
github.com/go-json-experiment/json from tailscale.com/types/opt
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext+
|
||||
@@ -402,6 +402,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/clientupdate+
|
||||
W 💣 tailscale.com/util/winutil/gp from tailscale.com/net/dns
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
W 💣 tailscale.com/util/winutil/winenv from tailscale.com/hostinfo+
|
||||
tailscale.com/util/zstdframe from tailscale.com/control/controlclient+
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -95,6 +95,10 @@ 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
|
||||
}
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
@@ -122,6 +126,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
||||
disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers)
|
||||
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
|
||||
disableCryptorouting = has(tailcfg.NodeAttrDisableMagicSockCryptoRouting)
|
||||
)
|
||||
|
||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||
@@ -147,6 +152,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
||||
k.DisableSplitDNSWhenNoCustomResolvers.Store(disableSplitDNSWhenNoCustomResolvers)
|
||||
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
|
||||
k.DisableCryptorouting.Store(disableCryptorouting)
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
@@ -173,5 +179,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
||||
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
|
||||
"DisableLocalDNSOverrideViaNRPT": k.DisableLocalDNSOverrideViaNRPT.Load(),
|
||||
"DisableCryptorouting": k.DisableCryptorouting.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-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=
|
||||
# nix-direnv cache busting line: sha256-2x9Ns5o6oenCcsHkOFjoCz/R5YjPwJEImK0a1valYBE=
|
||||
|
||||
5
go.mod
5
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,7 +19,6 @@ 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
|
||||
@@ -30,7 +28,7 @@ require (
|
||||
github.com/evanw/esbuild v0.19.11
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
github.com/fxamacker/cbor/v2 v2.6.0
|
||||
github.com/gaissmai/bart v0.4.1
|
||||
github.com/gaissmai/bart v0.11.1
|
||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0
|
||||
github.com/go-logr/zapr v1.3.0
|
||||
github.com/go-ole/go-ole v1.3.0
|
||||
@@ -44,7 +42,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
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-bQ1RvNNMKw8HA7paIq9XgtSfc4LvqyNhu/rQEh+IOts=
|
||||
sha256-2x9Ns5o6oenCcsHkOFjoCz/R5YjPwJEImK0a1valYBE=
|
||||
|
||||
10
go.sum
10
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=
|
||||
@@ -242,8 +240,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=
|
||||
@@ -312,8 +308,8 @@ github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1t
|
||||
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||
github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls=
|
||||
github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
|
||||
@@ -548,8 +544,6 @@ github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI=
|
||||
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
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=
|
||||
|
||||
@@ -69,6 +69,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 +165,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 +222,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 +300,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 +345,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 +387,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 +906,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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) stopOfflineAutoUpdate() {
|
||||
@@ -30,6 +31,10 @@ func (b *LocalBackend) maybeStartOfflineAutoUpdate(prefs ipn.PrefsView) {
|
||||
if !clientupdate.CanAutoUpdate() {
|
||||
return
|
||||
}
|
||||
// On macsys, auto-updates are managed by Sparkle.
|
||||
if version.IsMacSysExt() {
|
||||
return
|
||||
}
|
||||
|
||||
if b.offlineAutoUpdateCancel != nil {
|
||||
// Already running.
|
||||
|
||||
@@ -318,7 +318,7 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
|
||||
res := tailcfg.C2NPostureIdentityResponse{}
|
||||
|
||||
// Only collect serial numbers if enabled on the client,
|
||||
// Only collect posture identity if enabled on the client,
|
||||
// this will first check syspolicy, MDM settings like Registry
|
||||
// on Windows or defaults on macOS. If they are not set, it falls
|
||||
// back to the cli-flag, `--posture-checking`.
|
||||
@@ -337,8 +337,17 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.SerialNumbers = sns
|
||||
|
||||
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
|
||||
// and looks good in client metrics, remove this parameter and always report MAC
|
||||
// addresses.
|
||||
if r.FormValue("hwaddrs") == "true" {
|
||||
res.IfaceHardwareAddrs, err = posture.GetHardwareAddrs()
|
||||
if err != nil {
|
||||
b.logf("c2n: GetHardwareAddrs returned error: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res.PostureDisabled = true
|
||||
}
|
||||
@@ -355,7 +364,7 @@ func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
|
||||
prefs := b.Prefs().AutoUpdate()
|
||||
return tailcfg.C2NUpdateResponse{
|
||||
Enabled: envknob.AllowsRemoteUpdate() || prefs.Apply.EqualBool(true),
|
||||
Supported: clientupdate.CanAutoUpdate(),
|
||||
Supported: clientupdate.CanAutoUpdate() && !version.IsMacSysExt(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,9 +450,13 @@ func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
|
||||
// tailscaled is restarted during the update, systemd won't kill this
|
||||
// temporary update unit, which could cause unexpected breakage.
|
||||
//
|
||||
// We want to use the --wait flag for systemd-run, to block the update
|
||||
// command until completion and collect output. But this flag was added in
|
||||
// systemd 232, so we need to check the version first.
|
||||
// We want to use a few optional flags:
|
||||
// * --wait, to block the update command until completion (added in systemd 232)
|
||||
// * --pipe, to collect stdout/stderr (added in systemd 235)
|
||||
// * --collect, to clean up failed runs from memory (added in systemd 236)
|
||||
//
|
||||
// We need to check the version of systemd to figure out if those flags are
|
||||
// available.
|
||||
//
|
||||
// The output will look like:
|
||||
//
|
||||
@@ -461,10 +474,14 @@ func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
|
||||
if err != nil {
|
||||
return defaultCmd
|
||||
}
|
||||
if systemdVer < 232 {
|
||||
return exec.Command("systemd-run", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
} else {
|
||||
if systemdVer >= 236 {
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
} else if systemdVer >= 235 {
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", cmdTS, "update", "--yes")
|
||||
} else if systemdVer >= 232 {
|
||||
return exec.Command("systemd-run", "--wait", cmdTS, "update", "--yes")
|
||||
} else {
|
||||
return exec.Command("systemd-run", cmdTS, "update", "--yes")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,17 @@ var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
// If a cert is expired, it will be renewed synchronously otherwise it will be
|
||||
// renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertKeyPair, error) {
|
||||
return b.GetCertPEMWithValidity(ctx, domain, 0)
|
||||
}
|
||||
|
||||
// GetCertPEMWithValidity gets the TLSCertKeyPair for domain, either from cache
|
||||
// or via the ACME process. ACME process is used for new domain certs, existing
|
||||
// expired certs or existing certs that should get renewed sooner than
|
||||
// minValidity.
|
||||
//
|
||||
// If a cert is expired, or expires sooner than minValidity, it will be renewed
|
||||
// synchronously. Otherwise it will be renewed asynchronously.
|
||||
func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
@@ -109,17 +120,28 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// If we got here, we have a valid unexpired cert.
|
||||
// Check whether we should start an async renewal.
|
||||
if shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair); err != nil {
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if shouldRenew {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now)
|
||||
// Renewal check failed, but the current cert is valid and not
|
||||
// expired, so it's safe to return.
|
||||
return pair, nil
|
||||
}
|
||||
return pair, nil
|
||||
if !shouldRenew {
|
||||
return pair, nil
|
||||
}
|
||||
if minValidity == 0 {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background, return current valid cert.
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
|
||||
return pair, nil
|
||||
}
|
||||
// If the caller requested a specific validity duration, fall through
|
||||
// to synchronous renewal to fulfill that.
|
||||
logf("starting sync renewal")
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
@@ -129,7 +151,14 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
|
||||
// shouldStartDomainRenewal reports whether the domain's cert should be renewed
|
||||
// based on the current time, the cert's expiry, and the ARI check.
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair) (bool, error) {
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, now time.Time, pair *TLSCertKeyPair, minValidity time.Duration) (bool, error) {
|
||||
if minValidity != 0 {
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
return cert.NotAfter.Sub(now) < minValidity, nil
|
||||
}
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
if renewAt, ok := renewCertAt[domain]; ok {
|
||||
@@ -157,11 +186,7 @@ func (b *LocalBackend) domainRenewed(domain string) {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) domainRenewalTimeByExpiry(pair *TLSCertKeyPair) (time.Time, error) {
|
||||
block, _ := pem.Decode(pair.CertPEM)
|
||||
if block == nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
cert, err := pair.parseCertificate()
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("parsing certificate: %w", err)
|
||||
}
|
||||
@@ -366,6 +391,17 @@ type TLSCertKeyPair struct {
|
||||
Cached bool // whether result came from cache
|
||||
}
|
||||
|
||||
func (kp TLSCertKeyPair) parseCertificate() (*x509.Certificate, error) {
|
||||
block, _ := pem.Decode(kp.CertPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("error parsing certificate PEM")
|
||||
}
|
||||
if block.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("PEM block is %q, not a CERTIFICATE", block.Type)
|
||||
}
|
||||
return x509.ParseCertificate(block.Bytes)
|
||||
}
|
||||
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
@@ -383,7 +419,7 @@ func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKey
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
@@ -393,7 +429,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
|
||||
if p, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
// shouldStartDomainRenewal caches its result so it's OK to call this
|
||||
// frequently.
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p)
|
||||
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, p, minValidity)
|
||||
if err != nil {
|
||||
logf("error checking for certificate renewal: %v", err)
|
||||
} else if !shouldRenew {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ipnlocal is the heart of the Tailscale node agent that controls
|
||||
// all the other misc pieces of the Tailscale node.
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
@@ -389,18 +392,6 @@ 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)
|
||||
|
||||
@@ -415,7 +406,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
|
||||
sys: sys,
|
||||
health: sys.HealthTracker(),
|
||||
conf: sys.InitialConfig,
|
||||
e: e,
|
||||
dialer: dialer,
|
||||
store: store,
|
||||
@@ -432,6 +422,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
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 +610,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
|
||||
@@ -728,7 +763,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 +1226,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 +1263,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 +1287,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 +1355,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 {
|
||||
@@ -4865,8 +4915,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 +4963,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 +6645,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 +6653,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 +6677,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 (
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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/03c5a0ccf754/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,7 +65,7 @@ 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/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))
|
||||
@@ -74,12 +74,12 @@ 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))
|
||||
|
||||
@@ -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,7 +84,7 @@ 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/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/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
|
||||
@@ -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))
|
||||
@@ -66,14 +66,14 @@ Windows][]. 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/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/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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package dns contains code to configure and manage DNS settings.
|
||||
package dns
|
||||
|
||||
import (
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/gp"
|
||||
)
|
||||
|
||||
const testGPRuleID = "{7B1B6151-84E6-41A3-8967-62F7F7B45687}"
|
||||
@@ -51,7 +52,7 @@ func TestManagerWindowsGP(t *testing.T) {
|
||||
|
||||
// Make sure group policy is refreshed before this test exits but after we've
|
||||
// cleaned everything else up.
|
||||
defer procRefreshPolicyEx.Call(uintptr(1), uintptr(_RP_FORCE))
|
||||
defer gp.RefreshMachinePolicy(true)
|
||||
|
||||
err := createFakeGPKey()
|
||||
if err != nil {
|
||||
@@ -129,7 +130,7 @@ func TestManagerWindowsGPCopy(t *testing.T) {
|
||||
t.Fatalf("regWatcher.watch: %v\n", err)
|
||||
}
|
||||
|
||||
err = testDoRefresh()
|
||||
err = gp.RefreshMachinePolicy(true)
|
||||
if err != nil {
|
||||
t.Fatalf("testDoRefresh: %v\n", err)
|
||||
}
|
||||
@@ -153,7 +154,7 @@ func TestManagerWindowsGPCopy(t *testing.T) {
|
||||
t.Fatalf("regWatcher.watch: %v\n", err)
|
||||
}
|
||||
|
||||
err = testDoRefresh()
|
||||
err = gp.RefreshMachinePolicy(true)
|
||||
if err != nil {
|
||||
t.Fatalf("testDoRefresh: %v\n", err)
|
||||
}
|
||||
@@ -186,8 +187,8 @@ func checkGPNotificationsWork(t *testing.T) {
|
||||
}
|
||||
defer trk.Close()
|
||||
|
||||
r, _, err := procRefreshPolicyEx.Call(uintptr(1), uintptr(_RP_FORCE))
|
||||
if r == 0 {
|
||||
err = gp.RefreshMachinePolicy(true)
|
||||
if err != nil {
|
||||
t.Fatalf("RefreshPolicyEx error: %v\n", err)
|
||||
}
|
||||
|
||||
@@ -516,13 +517,11 @@ func genRandomSubdomains(t *testing.T, n int) []dnsname.FQDN {
|
||||
return domains
|
||||
}
|
||||
|
||||
func testDoRefresh() (err error) {
|
||||
r, _, e := procRefreshPolicyEx.Call(uintptr(1), uintptr(_RP_FORCE))
|
||||
if r == 0 {
|
||||
err = e
|
||||
}
|
||||
return err
|
||||
}
|
||||
var (
|
||||
libUserenv = windows.NewLazySystemDLL("userenv.dll")
|
||||
procRegisterGPNotification = libUserenv.NewProc("RegisterGPNotification")
|
||||
procUnregisterGPNotification = libUserenv.NewProc("UnregisterGPNotification")
|
||||
)
|
||||
|
||||
// gpNotificationTracker registers with the Windows policy engine and receives
|
||||
// notifications when policy refreshes occur.
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/util/winutil/gp"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -49,20 +50,11 @@ const (
|
||||
nrptRuleFlagsName = `ConfigOptions`
|
||||
)
|
||||
|
||||
var (
|
||||
libUserenv = windows.NewLazySystemDLL("userenv.dll")
|
||||
procRefreshPolicyEx = libUserenv.NewProc("RefreshPolicyEx")
|
||||
procRegisterGPNotification = libUserenv.NewProc("RegisterGPNotification")
|
||||
procUnregisterGPNotification = libUserenv.NewProc("UnregisterGPNotification")
|
||||
)
|
||||
|
||||
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
|
||||
|
||||
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
|
||||
// Table (NRPT).
|
||||
type nrptRuleDatabase struct {
|
||||
logf logger.Logf
|
||||
watcher *gpNotificationWatcher
|
||||
watcher *gp.ChangeWatcher
|
||||
isGPRefreshPending atomic.Bool
|
||||
mu sync.Mutex // protects the fields below
|
||||
ruleIDs []string
|
||||
@@ -303,12 +295,8 @@ func (db *nrptRuleDatabase) refreshLocked() {
|
||||
// positives.
|
||||
db.isGPRefreshPending.Store(true)
|
||||
|
||||
ok, _, err := procRefreshPolicyEx.Call(
|
||||
uintptr(1), // Win32 TRUE: Refresh computer policy, not user policy.
|
||||
uintptr(_RP_FORCE),
|
||||
)
|
||||
if ok == 0 {
|
||||
db.logf("RefreshPolicyEx failed: %v", err)
|
||||
if err := gp.RefreshMachinePolicy(true); err != nil {
|
||||
db.logf("RefreshMachinePolicy failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -376,7 +364,7 @@ func (db *nrptRuleDatabase) watchForGPChanges() {
|
||||
db.detectWriteAsGP()
|
||||
}
|
||||
|
||||
watcher, err := newGPNotificationWatcher(watchHandler)
|
||||
watcher, err := gp.NewChangeWatcher(gp.MachinePolicy, watchHandler)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -469,103 +457,3 @@ func (db *nrptRuleDatabase) Close() error {
|
||||
db.watcher = nil
|
||||
return err
|
||||
}
|
||||
|
||||
type gpNotificationWatcher struct {
|
||||
gpWaitEvents [2]windows.Handle
|
||||
handler func()
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// newGPNotificationWatcher creates an instance of gpNotificationWatcher that
|
||||
// invokes handler every time Windows notifies it of a group policy change.
|
||||
func newGPNotificationWatcher(handler func()) (*gpNotificationWatcher, error) {
|
||||
var err error
|
||||
|
||||
// evtDone is signaled by (*gpNotificationWatcher).Close() to indicate that
|
||||
// the doWatch goroutine should exit.
|
||||
evtDone, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(evtDone)
|
||||
}
|
||||
}()
|
||||
|
||||
// evtChanged is registered with the Windows policy engine to become
|
||||
// signalled any time group policy has been refreshed.
|
||||
evtChanged, err := windows.CreateEvent(nil, 0, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
windows.CloseHandle(evtChanged)
|
||||
}
|
||||
}()
|
||||
|
||||
// Tell Windows to signal evtChanged whenever group policies are refreshed.
|
||||
ok, _, e := procRegisterGPNotification.Call(
|
||||
uintptr(evtChanged),
|
||||
uintptr(1), // Win32 TRUE: We want to monitor computer policy changes, not user policy changes.
|
||||
)
|
||||
if ok == 0 {
|
||||
err = e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &gpNotificationWatcher{
|
||||
// Ordering of the event handles in gpWaitEvents is important:
|
||||
// When calling windows.WaitForMultipleObjects and multiple objects are
|
||||
// signalled simultaneously, it always returns the wait code for the
|
||||
// lowest-indexed handle in its input array. evtDone is higher priority for
|
||||
// us than evtChanged, so the former must be placed into the array ahead of
|
||||
// the latter.
|
||||
gpWaitEvents: [2]windows.Handle{
|
||||
evtDone,
|
||||
evtChanged,
|
||||
},
|
||||
handler: handler,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
go result.doWatch()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (w *gpNotificationWatcher) doWatch() {
|
||||
// The wait code corresponding to the event that is signalled when a group
|
||||
// policy change occurs.
|
||||
const expectedWaitCode = windows.WAIT_OBJECT_0 + 1
|
||||
for {
|
||||
if waitCode, _ := windows.WaitForMultipleObjects(w.gpWaitEvents[:], false, windows.INFINITE); waitCode != expectedWaitCode {
|
||||
break
|
||||
}
|
||||
w.handler()
|
||||
}
|
||||
close(w.done)
|
||||
}
|
||||
|
||||
func (w *gpNotificationWatcher) Close() error {
|
||||
// Notify doWatch that we're done and it should exit.
|
||||
if err := windows.SetEvent(w.gpWaitEvents[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
procUnregisterGPNotification.Call(uintptr(w.gpWaitEvents[1]))
|
||||
|
||||
// Wait for doWatch to complete.
|
||||
<-w.done
|
||||
|
||||
// Now we may safely clean up all the things.
|
||||
for i, evt := range w.gpWaitEvents {
|
||||
windows.CloseHandle(evt)
|
||||
w.gpWaitEvents[i] = 0
|
||||
}
|
||||
|
||||
w.handler = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/netip"
|
||||
"sort"
|
||||
@@ -122,6 +123,9 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
|
||||
}
|
||||
}
|
||||
if pathStr, ok := strings.CutPrefix(dohBase, controlDBase); ok {
|
||||
if i := strings.IndexFunc(pathStr, isSlashOrQuestionMark); i != -1 {
|
||||
pathStr = pathStr[:i]
|
||||
}
|
||||
return []netip.Addr{
|
||||
controlDv4One,
|
||||
controlDv4Two,
|
||||
@@ -318,7 +322,10 @@ func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
|
||||
// e.g. https://dns.controld.com/hyq3ipr2ct
|
||||
func controlDv6Gen(ip netip.Addr, id string) netip.Addr {
|
||||
b := make([]byte, 8)
|
||||
decoded, _ := strconv.ParseUint(id, 36, 64)
|
||||
decoded, err := strconv.ParseUint(id, 36, 64)
|
||||
if err != nil {
|
||||
log.Printf("controlDv6Gen: failed to parse id %q: %v", id, err)
|
||||
}
|
||||
binary.BigEndian.PutUint64(b, decoded)
|
||||
a := ip.AsSlice()
|
||||
copy(a[6:14], b)
|
||||
|
||||
@@ -134,6 +134,15 @@ func TestDoHIPsOfBase(t *testing.T) {
|
||||
"2606:1a40:1:ffff:ffff:ffff:ffff:0",
|
||||
),
|
||||
},
|
||||
{
|
||||
base: "https://dns.controld.com/hyq3ipr2ct/test-host-name",
|
||||
want: ips(
|
||||
"76.76.2.22",
|
||||
"76.76.10.22",
|
||||
"2606:1a40:0:6:7b5b:5949:35ad:0",
|
||||
"2606:1a40:1:6:7b5b:5949:35ad:0",
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := DoHIPsOfBase(tt.base)
|
||||
|
||||
@@ -22,7 +22,7 @@ func emptySet(ip netip.Addr) bool { return false }
|
||||
|
||||
func bartLookup(t *bart.Table[struct{}]) func(netip.Addr) bool {
|
||||
return func(ip netip.Addr) bool {
|
||||
_, ok := t.Get(ip)
|
||||
_, ok := t.Lookup(ip)
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package interfaces contains helpers for looking up system network interfaces.
|
||||
package netmon
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package netstat returns the local machine's network connection table.
|
||||
package netstat
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package netutil contains misc shared networking code & types.
|
||||
package netutil
|
||||
|
||||
import (
|
||||
|
||||
@@ -417,7 +417,7 @@ func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn,
|
||||
}
|
||||
|
||||
if routes := d.routes.Load(); routes != nil {
|
||||
if isTailscaleRoute, _ := routes.Get(ipp.Addr()); isTailscaleRoute {
|
||||
if isTailscaleRoute, _ := routes.Lookup(ipp.Addr()); isTailscaleRoute {
|
||||
return d.getPeerDialer().DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tstun provides a TUN struct implementing the tun.Device interface
|
||||
// with additional features as required by wgengine.
|
||||
package tstun
|
||||
|
||||
import (
|
||||
@@ -626,7 +624,7 @@ func (pc *peerConfigTable) mapDstIP(src, oldDst netip.Addr) netip.Addr {
|
||||
// The 'dst' of the packet is the address for this local node. It could
|
||||
// be a masquerade address that we told other nodes to use, or one of
|
||||
// our local node's Addresses.
|
||||
c, ok := pc.byIP.Get(src)
|
||||
c, ok := pc.byIP.Lookup(src)
|
||||
if !ok {
|
||||
return oldDst
|
||||
}
|
||||
@@ -657,7 +655,7 @@ func (pc *peerConfigTable) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
|
||||
}
|
||||
|
||||
// Look up the configuration for the destination
|
||||
c, ok := pc.byIP.Get(dst)
|
||||
c, ok := pc.byIP.Lookup(dst)
|
||||
if !ok {
|
||||
return oldSrc
|
||||
}
|
||||
@@ -767,7 +765,7 @@ func (pc *peerConfigTable) inboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
if pc == nil {
|
||||
return false
|
||||
}
|
||||
c, ok := pc.byIP.Get(p.Src.Addr())
|
||||
c, ok := pc.byIP.Lookup(p.Src.Addr())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
@@ -778,14 +776,14 @@ func (pc *peerConfigTable) outboundPacketIsJailed(p *packet.Parsed) bool {
|
||||
if pc == nil {
|
||||
return false
|
||||
}
|
||||
c, ok := pc.byIP.Get(p.Dst.Addr())
|
||||
c, ok := pc.byIP.Lookup(p.Dst.Addr())
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return c.jailed
|
||||
}
|
||||
|
||||
// SetNetMap is called when a new NetworkMap is received.
|
||||
// SetWGConfig is called when a new NetworkMap is received.
|
||||
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
|
||||
cfg := peerConfigTableFromWGConfig(wcfg)
|
||||
|
||||
|
||||
75
pkgdoc_test.go
Normal file
75
pkgdoc_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailscaleroot
|
||||
|
||||
import (
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPackageDocs(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "linux":
|
||||
// Enough coverage for CI+devs.
|
||||
default:
|
||||
t.Skipf("skipping on %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
var goFiles []string
|
||||
err := filepath.Walk(".", func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.Mode().IsRegular() && strings.HasSuffix(path, ".go") {
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
goFiles = append(goFiles, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
byDir := map[string][]string{} // dir => files
|
||||
for _, fileName := range goFiles {
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, fileName, nil, parser.PackageClauseOnly|parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to ParseFile %q: %v", fileName, err)
|
||||
}
|
||||
dir := filepath.Dir(fileName)
|
||||
if _, ok := byDir[dir]; !ok {
|
||||
byDir[dir] = nil
|
||||
}
|
||||
if f.Doc != nil {
|
||||
byDir[dir] = append(byDir[dir], fileName)
|
||||
txt := f.Doc.Text()
|
||||
if strings.Contains(txt, "SPDX-License-Identifier") {
|
||||
t.Errorf("the copyright header for %s became its package doc due to missing blank line", fileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
for dir, ff := range byDir {
|
||||
switch dir {
|
||||
case "tstest/integration/vms":
|
||||
// This package has a couple go:build ignore commands and this test doesn't
|
||||
// handle parsing those. Just allowlist that package for now (2024-07-10).
|
||||
continue
|
||||
}
|
||||
if len(ff) > 1 {
|
||||
t.Logf("multiple files with package doc in %s: %q", dir, ff)
|
||||
}
|
||||
if len(ff) == 0 {
|
||||
t.Errorf("no package doc in %s", dir)
|
||||
}
|
||||
}
|
||||
t.Logf("parsed %d files", len(goFiles))
|
||||
}
|
||||
6
posture/doc.go
Normal file
6
posture/doc.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package posture contains functions to query the local system
|
||||
// state for managed posture checks.
|
||||
package posture
|
||||
26
posture/hwaddr.go
Normal file
26
posture/hwaddr.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package posture
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"slices"
|
||||
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
// GetHardwareAddrs returns the hardware addresses of all non-loopback
|
||||
// network interfaces.
|
||||
func GetHardwareAddrs() (hwaddrs []string, err error) {
|
||||
err = netmon.ForeachInterface(func(i netmon.Interface, _ []netip.Prefix) {
|
||||
if i.IsLoopback() {
|
||||
return
|
||||
}
|
||||
if a := i.HardwareAddr.String(); a != "" {
|
||||
hwaddrs = append(hwaddrs, a)
|
||||
}
|
||||
})
|
||||
slices.Sort(hwaddrs)
|
||||
return
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user