Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+2
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package backoff provides a back-off timer type.
|
||||
@@ -60,10 +60,7 @@ func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
|
||||
if d > b.maxBackoff {
|
||||
d = b.maxBackoff
|
||||
}
|
||||
d := min(time.Duration(b.n*b.n)*10*time.Millisecond, b.maxBackoff)
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package bufiox provides extensions to the standard bufio package.
|
||||
package bufiox
|
||||
|
||||
import "io"
|
||||
|
||||
// BufferedReader is an interface for readers that support peeking
|
||||
// into an internal buffer, like [bufio.Reader].
|
||||
type BufferedReader interface {
|
||||
Peek(n int) ([]byte, error)
|
||||
Discard(n int) (discarded int, err error)
|
||||
}
|
||||
|
||||
// ReadFull reads exactly len(buf) bytes from r into buf, like
|
||||
// [io.ReadFull], but without heap allocations. It uses Peek to
|
||||
// access the buffered data directly, copies it into buf, then
|
||||
// discards the consumed bytes. If an error occurs,
|
||||
// discard is not called and the buffer is left unchanged.
|
||||
func ReadFull(r BufferedReader, buf []byte) (int, error) {
|
||||
b, err := r.Peek(len(buf))
|
||||
if err != nil {
|
||||
if len(b) > 0 && err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
defer r.Discard(len(buf))
|
||||
return copy(buf, b), nil
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package checkchange defines a utility for determining whether a value
|
||||
|
||||
+13
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package cibuild reports runtime CI information.
|
||||
@@ -12,3 +12,15 @@ func On() bool {
|
||||
// https://docs.github.com/en/actions/learn-github-actions/environment-variables#default-environment-variables
|
||||
return os.Getenv("GITHUB_ACTIONS") != "" || os.Getenv("CI") == "true"
|
||||
}
|
||||
|
||||
// OnTailscaleCI reports whether the current binary is executing on
|
||||
// tailscale/tailscale's own GitHub Actions CI, as opposed to a fork's CI
|
||||
// or an unrelated downstream CI (such as a Linux distribution's package
|
||||
// build infrastructure) that also sets the generic CI=true environment
|
||||
// variable.
|
||||
func OnTailscaleCI() bool {
|
||||
// GITHUB_REPOSITORY_OWNER is set by GitHub Actions to the owner of
|
||||
// the repository whose workflow is running. For pull requests, this
|
||||
// is the base repository's owner, not the fork's.
|
||||
return os.Getenv("GITHUB_REPOSITORY_OWNER") == "tailscale"
|
||||
}
|
||||
+20
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_clientmetrics
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -452,6 +453,24 @@ func (b *deltaEncBuf) writeHexVarint(v int64) {
|
||||
b.buf.Write(hexBuf)
|
||||
}
|
||||
|
||||
// ResetForTest resets all client metric values to zero.
|
||||
// It panics if not in a test or if called from a parallel test.
|
||||
func ResetForTest(t testenv.TB) {
|
||||
if !testenv.InTest() {
|
||||
panic("clientmetric.ResetForTest called outside a test")
|
||||
}
|
||||
if testenv.InParallelTest(t) {
|
||||
panic("clientmetric.ResetForTest called from a parallel test")
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
for _, m := range metrics {
|
||||
if m.v != nil {
|
||||
atomic.StoreInt64(m.v, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var TestHooks testHooks
|
||||
|
||||
type testHooks struct{}
|
||||
|
||||
+7
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_clientmetrics
|
||||
@@ -26,6 +26,9 @@ func WritePrometheusExpositionFormat(any) {}
|
||||
|
||||
var zeroMetric Metric
|
||||
|
||||
func NewCounter(string) *Metric { return &zeroMetric }
|
||||
func NewGauge(string) *Metric { return &zeroMetric }
|
||||
func NewAggregateCounter(string) *Metric { return &zeroMetric }
|
||||
func NewCounter(string) *Metric { return &zeroMetric }
|
||||
func NewGauge(string) *Metric { return &zeroMetric }
|
||||
func NewAggregateCounter(string) *Metric { return &zeroMetric }
|
||||
func NewCounterFunc(string, func() int64) *Metric { return &zeroMetric }
|
||||
|
||||
func ResetForTest(any) {}
|
||||
+22
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package cloudenv reports which known cloud environment we're running in.
|
||||
@@ -47,6 +47,7 @@ const (
|
||||
Azure = Cloud("azure") // Microsoft Azure
|
||||
GCP = Cloud("gcp") // Google Cloud
|
||||
DigitalOcean = Cloud("digitalocean") // DigitalOcean
|
||||
Hetzner = Cloud("hetzner") // Hetzner Cloud
|
||||
)
|
||||
|
||||
// ResolverIP returns the cloud host's recursive DNS server or the
|
||||
@@ -64,6 +65,8 @@ func (c Cloud) ResolverIP() string {
|
||||
return AzureResolverIP
|
||||
case DigitalOcean:
|
||||
return getDigitalOceanResolver()
|
||||
case Hetzner:
|
||||
return getHetznerResolver()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -82,6 +85,21 @@ func getDigitalOceanResolver() string {
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
// https://docs.hetzner.com/robot/dedicated-server/general-information/recursive-name-servers/
|
||||
// IPv6 resolvers also exist: 2a01:4ff:ff00::add:1, 2a01:4ff:ff00::add:2.
|
||||
hetznerResolvers = []string{"185.12.64.1", "185.12.64.2"}
|
||||
hetznerResolver lazy.SyncValue[string]
|
||||
)
|
||||
|
||||
func getHetznerResolver() string {
|
||||
// Randomly select one of the available resolvers so we don't overload
|
||||
// one of them by sending all traffic there.
|
||||
return hetznerResolver.Get(func() string {
|
||||
return hetznerResolvers[rand.IntN(len(hetznerResolvers))]
|
||||
})
|
||||
}
|
||||
|
||||
// HasInternalTLD reports whether c is a cloud environment
|
||||
// whose ResolverIP serves *.internal records.
|
||||
func (c Cloud) HasInternalTLD() bool {
|
||||
@@ -128,6 +146,9 @@ func getCloud() Cloud {
|
||||
if sysVendor == "DigitalOcean" {
|
||||
return DigitalOcean
|
||||
}
|
||||
if sysVendor == "Hetzner" {
|
||||
return Hetzner
|
||||
}
|
||||
// TODO(andrew): "Vultr" is also valid if we need it
|
||||
|
||||
prod := readFileTrimmed("/sys/class/dmi/id/product_name")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !(ios || android || js)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || android || js
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package cmpver implements a variant of debian version number
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package coalescedop provides a mechanism to serialize and coalesce
|
||||
// calls to a function. At most one call is in flight at a time, and
|
||||
// if additional calls are requested while one is running, exactly one
|
||||
// more execution is scheduled after the current one completes.
|
||||
//
|
||||
// Unlike [singleflight.Group], which deduplicates concurrent callers
|
||||
// onto a single in-flight call, [CoalescedOp] ensures that a new
|
||||
// execution occurs after the current one finishes, so changes that
|
||||
// happened during the in-flight call are picked up by the next run.
|
||||
package coalescedop
|
||||
|
||||
import "sync"
|
||||
|
||||
// CoalescedOp serializes and coalesces calls to a function.
|
||||
// At most one call is in flight at a time. If additional calls
|
||||
// are requested while one is running, exactly one more execution
|
||||
// is scheduled (not one per request).
|
||||
//
|
||||
// The zero value is not valid; use [New].
|
||||
type CoalescedOp struct {
|
||||
mu sync.Mutex
|
||||
fn func()
|
||||
running bool
|
||||
pending bool
|
||||
}
|
||||
|
||||
// New returns a CoalescedOp that calls fn.
|
||||
func New(fn func()) *CoalescedOp {
|
||||
return &CoalescedOp{fn: fn}
|
||||
}
|
||||
|
||||
// Do requests that the operation be run. If no execution is in
|
||||
// progress, a new goroutine is started to run the function. If an
|
||||
// execution is already in progress, the function will be run exactly
|
||||
// once more after the current execution completes.
|
||||
//
|
||||
// Do never blocks.
|
||||
func (c *CoalescedOp) Do() {
|
||||
c.mu.Lock()
|
||||
if c.running {
|
||||
c.pending = true
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.running = true
|
||||
c.mu.Unlock()
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *CoalescedOp) run() {
|
||||
for {
|
||||
c.fn()
|
||||
|
||||
c.mu.Lock()
|
||||
if !c.pending {
|
||||
c.running = false
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.pending = false
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// ctxkey provides type-safe key-value pairs for use with [context.Context].
|
||||
// Package ctxkey provides type-safe key-value pairs for use with [context.Context].
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build deephash_debug
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package deephash hashes a Go value recursively, in a predictable order,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package deephash
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !race
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build race
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package deephash
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package def parses strings and environment variables with fallback default values.
|
||||
package def
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Bool parses s as a bool, returning def when s is empty or invalid.
|
||||
func Bool(s string, def bool) bool {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.ParseBool(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Duration parses s as a time.Duration, returning def when s is empty or invalid.
|
||||
func Duration(s string, def time.Duration) time.Duration {
|
||||
if s == "" {
|
||||
return def
|
||||
}
|
||||
v, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// LookupEnv retrieves the value of the environment variable named by the key.
|
||||
// If the variable is present in the environment the value (which may be
|
||||
// empty) is returned. Otherwise, it returns def.
|
||||
func LookupEnv(key, def string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package dirwalk contains code to walk a directory.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package dirwalk
|
||||
|
||||
+44
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package dnsname contains string functions for working with DNS names.
|
||||
@@ -20,6 +20,12 @@ const (
|
||||
// A FQDN is a fully-qualified DNS name or name suffix.
|
||||
type FQDN string
|
||||
|
||||
// ToFQDN returns s as an FQDN, with a trailing dot added if missing. The
|
||||
// empty string and "." both parse as ".".
|
||||
//
|
||||
// Names containing whitespace or control characters are rejected. See the
|
||||
// comment in the label loop below for why other characters are not
|
||||
// validated.
|
||||
func ToFQDN(s string) (FQDN, error) {
|
||||
if len(s) == 0 || s == "." {
|
||||
return FQDN("."), nil
|
||||
@@ -39,6 +45,18 @@ func ToFQDN(s string) (FQDN, error) {
|
||||
return "", vizerror.Errorf("%q is too long to be a DNS name", s)
|
||||
}
|
||||
|
||||
// DNS labels on the wire may contain any non-zero byte (see the note in
|
||||
// the label loop below about issue 2024), but names we accept from
|
||||
// control are also written verbatim into line-oriented OS configuration
|
||||
// files like resolv.conf and hosts, where a newline or carriage return
|
||||
// can inject whole new directives. Reject whitespace and control
|
||||
// characters so a malicious control server can't do that.
|
||||
for i := range len(s) {
|
||||
if c := s[i]; c <= ' ' || c == 0x7f {
|
||||
return "", vizerror.Errorf("%q contains invalid character %q", raw, c)
|
||||
}
|
||||
}
|
||||
|
||||
st := 0
|
||||
for i := range len(s) {
|
||||
if s[i] != '.' {
|
||||
@@ -94,14 +112,34 @@ func (f FQDN) Contains(other FQDN) bool {
|
||||
return strings.HasSuffix(other.WithTrailingDot(), cmp)
|
||||
}
|
||||
|
||||
// Parent returns the parent domain by stripping the first label.
|
||||
// For "foo.bar.baz.", it returns "bar.baz."
|
||||
// It returns an empty FQDN for root or single-label domains.
|
||||
func (f FQDN) Parent() FQDN {
|
||||
s := f.WithTrailingDot()
|
||||
_, rest, ok := strings.Cut(s, ".")
|
||||
if !ok || rest == "" {
|
||||
return ""
|
||||
}
|
||||
return FQDN(rest)
|
||||
}
|
||||
|
||||
// ValidLabel reports whether label is a valid DNS label. All errors are
|
||||
// [vizerror.Error].
|
||||
func ValidLabel(label string) error {
|
||||
return ValidLabelLike(label, maxLabelLength)
|
||||
}
|
||||
|
||||
// ValidLabelLike reports whether label contains DNS-valid characters
|
||||
// only, adheres to a given maximum length, and starts and ends with
|
||||
// a letter or number. For strict DNS-validity use [ValidLabel].
|
||||
// All errors are [vizerror.Error].
|
||||
func ValidLabelLike(label string, maxLen int) error {
|
||||
if len(label) == 0 {
|
||||
return vizerror.New("empty DNS label")
|
||||
}
|
||||
if len(label) > maxLabelLength {
|
||||
return vizerror.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
|
||||
if len(label) > maxLen {
|
||||
return vizerror.Errorf("%q is too long, max length is %d bytes", label, maxLen)
|
||||
}
|
||||
if !isalphanum(label[0]) {
|
||||
return vizerror.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
|
||||
@@ -109,7 +147,8 @@ func ValidLabel(label string) error {
|
||||
if !isalphanum(label[len(label)-1]) {
|
||||
return vizerror.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
|
||||
}
|
||||
if len(label) < 2 {
|
||||
if len(label) <= 2 {
|
||||
// Return early because we've already checked start and end characters (the only 2) are alphanumeric.
|
||||
return nil
|
||||
}
|
||||
for i := 1; i < len(label)-1; i++ {
|
||||
@@ -222,7 +261,7 @@ func ValidHostname(hostname string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, label := range strings.Split(fqdn.WithoutTrailingDot(), ".") {
|
||||
for label := range strings.SplitSeq(fqdn.WithoutTrailingDot(), ".") {
|
||||
if err := ValidLabel(label); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+10
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
@@ -103,6 +103,15 @@ func (b *Bus) Debugger() *Debugger {
|
||||
return &Debugger{b}
|
||||
}
|
||||
|
||||
// ProbeLocks acquires and releases the bus's internal mutexes.
|
||||
func (b *Bus) ProbeLocks() {
|
||||
b.topicsMu.Lock()
|
||||
b.topicsMu.Unlock()
|
||||
|
||||
b.clientsMu.Lock()
|
||||
b.clientsMu.Unlock()
|
||||
}
|
||||
|
||||
// Close closes the bus. It implicitly closes all clients, publishers and
|
||||
// subscribers attached to the bus.
|
||||
//
|
||||
|
||||
+16
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
@@ -146,7 +146,10 @@ func Subscribe[T any](c *Client) *Subscriber[T] {
|
||||
|
||||
r := c.subscribeStateLocked()
|
||||
s := newSubscriber[T](r, logfForCaller(c.logger()))
|
||||
r.addSubscriber(s)
|
||||
// Register the non-generic core with the bus rather than the typed facade,
|
||||
// mirroring SubscribeFunc and Publish: this keeps the bus's outputs map
|
||||
// and subscriber-interface itab out of per-T cost.
|
||||
r.addSubscriber(s.core)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -169,7 +172,11 @@ func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
|
||||
|
||||
r := c.subscribeStateLocked()
|
||||
s := newSubscriberFunc[T](r, f, logfForCaller(c.logger()))
|
||||
r.addSubscriber(s)
|
||||
// Register the non-generic core, not the typed facade. Doing
|
||||
// so means the bus's outputs map and the subscriber interface
|
||||
// itab are not parameterized by T, eliminating per-T itab and
|
||||
// dictionary cost.
|
||||
r.addSubscriber(s.core)
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -177,6 +184,11 @@ func SubscribeFunc[T any](c *Client, f func(T)) *SubscriberFunc[T] {
|
||||
// It panics if c is closed.
|
||||
func Publish[T any](c *Client) *Publisher[T] {
|
||||
p := newPublisher[T](c)
|
||||
c.addPublisher(p)
|
||||
// Register the non-generic core with the client so the
|
||||
// per-Client publisher set, the publisher interface itab, and
|
||||
// the publisher equality function are not parameterized by T.
|
||||
// This eliminates per-T itab/dictionary/eq cost for every new
|
||||
// event type passed through Publish[T].
|
||||
c.addPublisher(p.core)
|
||||
return p
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !ts_omit_debugeventbus
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ios || android || ts_omit_debugeventbus
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package eventbus provides an in-process event bus.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+54
-15
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
@@ -7,8 +7,13 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// publisher is a uniformly typed wrapper around Publisher[T], so that
|
||||
// debugging facilities can look at active publishers.
|
||||
// publisher is a uniformly typed wrapper around publisherCore so that
|
||||
// debugging facilities can enumerate active publishers on a [Client]
|
||||
// and report the types each one publishes. The interface is
|
||||
// implemented by the non-generic *publisherCore (not by the typed
|
||||
// user-facing *Publisher[T]); this keeps the bus's per-Client
|
||||
// publisher set, and the publisher itab/dictionary, free of
|
||||
// per-T duplication.
|
||||
type publisher interface {
|
||||
publishType() reflect.Type
|
||||
Close()
|
||||
@@ -16,12 +21,35 @@ type publisher interface {
|
||||
|
||||
// A Publisher publishes typed events on a bus.
|
||||
type Publisher[T any] struct {
|
||||
// Implementation note: Publisher[T] is a thin user-facing facade over a
|
||||
// non-generic *publisherCore. Carrying T on the public type preserves the
|
||||
// typed API of Publish(v T), but all of the actual state (the *Client
|
||||
// back-pointer, the stop flag, and the cached reflect.Type used by
|
||||
// diagnostic introspection) lives on the core and is not duplicated per T.
|
||||
//
|
||||
// The diagnostic surface that motivates the publisher interface
|
||||
// (Debugger.PublishTypes) is served by *publisherCore directly, so adding
|
||||
// new typed publishers does not pay an itab+dictionary cost just to satisfy
|
||||
// diagnostic enumeration.
|
||||
core *publisherCore
|
||||
}
|
||||
|
||||
// publisherCore is the non-generic implementation of a Publisher.
|
||||
// It implements the package-private publisher interface; the bus's
|
||||
// outputs map and itab key on this single type, not on Publisher[T].
|
||||
type publisherCore struct {
|
||||
client *Client
|
||||
stop stopFlag
|
||||
typ reflect.Type // cached reflect.TypeFor[T]()
|
||||
}
|
||||
|
||||
func newPublisher[T any](c *Client) *Publisher[T] {
|
||||
return &Publisher[T]{client: c}
|
||||
return &Publisher[T]{
|
||||
core: &publisherCore{
|
||||
client: c,
|
||||
typ: reflect.TypeFor[T](),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the publisher.
|
||||
@@ -31,35 +59,46 @@ func newPublisher[T any](c *Client) *Publisher[T] {
|
||||
// If the Bus or Client from which the Publisher was created is closed,
|
||||
// the Publisher is implicitly closed and does not need to be closed
|
||||
// separately.
|
||||
func (p *Publisher[T]) Close() {
|
||||
func (p *Publisher[T]) Close() { p.core.Close() }
|
||||
|
||||
// Close implements the publisher interface and the user-facing
|
||||
// (*Publisher[T]).Close.
|
||||
func (c *publisherCore) Close() {
|
||||
// Just unblocks any active calls to Publish, no other
|
||||
// synchronization needed.
|
||||
p.stop.Stop()
|
||||
p.client.deletePublisher(p)
|
||||
c.stop.Stop()
|
||||
c.client.deletePublisher(c)
|
||||
}
|
||||
|
||||
func (p *Publisher[T]) publishType() reflect.Type {
|
||||
return reflect.TypeFor[T]()
|
||||
}
|
||||
// publishType implements the publisher interface.
|
||||
func (c *publisherCore) publishType() reflect.Type { return c.typ }
|
||||
|
||||
// Publish publishes event v on the bus.
|
||||
func (p *Publisher[T]) Publish(v T) {
|
||||
publish(p.core, v)
|
||||
}
|
||||
|
||||
// publish is the non-generic body of Publisher[T].Publish. The only
|
||||
// per-T work is the boxing of v into evt.Event (an `any` field) and
|
||||
// the construction of the PublishedEvent struct itself; all of the
|
||||
// channel/select dance is shared across every T.
|
||||
func publish(c *publisherCore, v any) {
|
||||
// Check for just a stopped publisher or bus before trying to
|
||||
// write, so that once closed Publish consistently does nothing.
|
||||
select {
|
||||
case <-p.stop.Done():
|
||||
case <-c.stop.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
evt := PublishedEvent{
|
||||
Event: v,
|
||||
From: p.client,
|
||||
From: c.client,
|
||||
}
|
||||
|
||||
select {
|
||||
case p.client.publish() <- evt:
|
||||
case <-p.stop.Done():
|
||||
case c.client.publish() <- evt:
|
||||
case <-c.stop.Done():
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,5 +109,5 @@ func (p *Publisher[T]) Publish(v T) {
|
||||
// nobody seems to care. Publishers must not assume that someone will
|
||||
// definitely receive an event if ShouldPublish returns true.
|
||||
func (p *Publisher[T]) ShouldPublish() bool {
|
||||
return p.client.shouldPublish(reflect.TypeFor[T]())
|
||||
return p.core.client.shouldPublish(p.core.typ)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
|
||||
+217
-72
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package eventbus
|
||||
@@ -184,49 +184,62 @@ func (s *subscribeState) closed() <-chan struct{} {
|
||||
// A Subscriber delivers one type of event from a [Client].
|
||||
// Events are sent to the [Subscriber.Events] channel.
|
||||
type Subscriber[T any] struct {
|
||||
stop stopFlag
|
||||
read chan T
|
||||
unregister func()
|
||||
logf logger.Logf
|
||||
slow *time.Timer // used to detect slow subscriber service
|
||||
// core holds the non-generic subscriber-interface implementation
|
||||
// (Close, subscribeType, dispatch, slow timer, unregister) shared
|
||||
// with [SubscriberFunc] via [subscriberCore]. The only per-T state
|
||||
// owned by the facade itself is the typed delivery channel; the
|
||||
// dispatch loop, unlike SubscriberFunc, must remain per-T — see
|
||||
// [Subscriber.dispatchTyped].
|
||||
core *subscriberCore
|
||||
read chan T
|
||||
}
|
||||
|
||||
func newSubscriber[T any](r *subscribeState, logf logger.Logf) *Subscriber[T] {
|
||||
slow := time.NewTimer(0)
|
||||
slow.Stop() // reset in dispatch
|
||||
return &Subscriber[T]{
|
||||
read: make(chan T),
|
||||
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
|
||||
logf: logf,
|
||||
slow: slow,
|
||||
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
|
||||
s := &Subscriber[T]{
|
||||
core: core,
|
||||
read: make(chan T),
|
||||
}
|
||||
}
|
||||
|
||||
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
|
||||
ret := &Subscriber[T]{
|
||||
read: make(chan T, 100), // arbitrary, large
|
||||
// Subscriber[T] keeps a per-T dispatch loop; see [Subscriber.dispatchTyped]
|
||||
// for why we don't share the non-generic dispatchFunc that SubscriberFunc
|
||||
// uses.
|
||||
core.dispatchFn = func(
|
||||
ctx context.Context,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
) bool {
|
||||
return s.dispatchTyped(ctx, vals, acceptCh, snapshot)
|
||||
}
|
||||
ret.unregister = attach(ret.monitor)
|
||||
return ret
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) subscribeType() reflect.Type {
|
||||
return reflect.TypeFor[T]()
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) monitor(debugEvent T) {
|
||||
select {
|
||||
case s.read <- debugEvent:
|
||||
case <-s.stop.Done():
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
|
||||
// dispatchTyped is the per-T dispatch loop for Subscriber[T]. It has to remain
|
||||
// generic because the typed channel send `case s.read <- t:` must appear
|
||||
// lexically inside the select. The rest of the cases match the non-generic
|
||||
// dispatchFunc body to keep behavior aligned between Subscriber and
|
||||
// SubscriberFunc.
|
||||
//
|
||||
// We don't share dispatchFunc (the way SubscriberFunc does) because bridging
|
||||
// the typed channel send and the non-generic select would require running the
|
||||
// send on its own goroutine on every event delivery. That bridge was measured
|
||||
// at ~2.7x throughput regression on BenchmarkBasicThroughput, so we keep
|
||||
// dispatchTyped generic and pay the per-shape stencil cost instead (measured
|
||||
// at ~1,600 B body + ~1,100 B pclntab per shape on linux/amd64 tailscaled).
|
||||
// Only the typed select lives in the per-shape stencil; the surrounding state
|
||||
// (slow timer, log function, type name) is reached through the non-generic
|
||||
// core.
|
||||
func (s *Subscriber[T]) dispatchTyped(
|
||||
ctx context.Context,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
) bool {
|
||||
t := vals.Peek().Event.(T)
|
||||
|
||||
start := time.Now()
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
defer s.slow.Stop()
|
||||
s.core.slow.Reset(slowSubscriberTimeout)
|
||||
defer s.core.slow.Stop()
|
||||
|
||||
for {
|
||||
// Keep the cases in this select in sync with subscribeState.pump
|
||||
@@ -242,13 +255,35 @@ func (s *Subscriber[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent
|
||||
return false
|
||||
case ch := <-snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
case <-s.slow.C:
|
||||
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
case <-s.core.slow.C:
|
||||
s.core.logf("subscriber for %s is slow (%v elapsed)", s.core.typeName, time.Since(start))
|
||||
s.core.slow.Reset(slowSubscriberTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newMonitor[T any](attach func(fn func(T)) (cancel func())) *Subscriber[T] {
|
||||
s := &Subscriber[T]{
|
||||
// Monitors don't go through the bus's dispatch path (they
|
||||
// are attached directly to the debug hook), so they don't
|
||||
// need a fully-initialized subscriberCore — only the typed
|
||||
// delivery channel and an unregister callback. We give them
|
||||
// a placeholder core so Close() and Done() work uniformly.
|
||||
core: &subscriberCore{},
|
||||
read: make(chan T, 100), // arbitrary, large
|
||||
}
|
||||
cancel := attach(s.monitor)
|
||||
s.core.unregister = func(reflect.Type) { cancel() }
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Subscriber[T]) monitor(debugEvent T) {
|
||||
select {
|
||||
case s.read <- debugEvent:
|
||||
case <-s.core.stop.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// Events returns a channel on which the subscriber's events are
|
||||
// delivered.
|
||||
func (s *Subscriber[T]) Events() <-chan T {
|
||||
@@ -258,7 +293,7 @@ func (s *Subscriber[T]) Events() <-chan T {
|
||||
// Done returns a channel that is closed when the subscriber is
|
||||
// closed.
|
||||
func (s *Subscriber[T]) Done() <-chan struct{} {
|
||||
return s.stop.Done()
|
||||
return s.core.stop.Done()
|
||||
}
|
||||
|
||||
// Close closes the Subscriber, indicating the caller no longer wishes
|
||||
@@ -268,30 +303,105 @@ func (s *Subscriber[T]) Done() <-chan struct{} {
|
||||
// If the Bus from which the Subscriber was created is closed,
|
||||
// the Subscriber is implicitly closed and does not need to be closed
|
||||
// separately.
|
||||
func (s *Subscriber[T]) Close() {
|
||||
s.stop.Stop() // unblock receivers
|
||||
s.unregister()
|
||||
}
|
||||
func (s *Subscriber[T]) Close() { s.core.Close() }
|
||||
|
||||
// A SubscriberFunc delivers one type of event from a [Client].
|
||||
// Events are forwarded synchronously to a function provided at construction.
|
||||
type SubscriberFunc[T any] struct {
|
||||
// core holds the non-generic subscriber-interface implementation shared
|
||||
// with [Subscriber] via [subscriberCore]. The user callback is captured
|
||||
// in the dispatchFn closure on the core, so SubscriberFunc[T] itself
|
||||
// carries no per-T state beyond the core pointer; per-T cost is limited
|
||||
// to the small forwarding Close method below.
|
||||
core *subscriberCore
|
||||
}
|
||||
|
||||
// subscriberCore is the non-generic backing for both Subscriber[T] and
|
||||
// SubscriberFunc[T]. It implements the package-private subscriber interface
|
||||
// so that the bus (and the subscribeState map) can store it without per-T
|
||||
// itabs or dictionaries. The per-T behavior (type assertion plus either typed
|
||||
// channel send or user callback invocation) is encapsulated in the dispatchFn
|
||||
// closure set up by the constructor of the typed facade.
|
||||
type subscriberCore struct {
|
||||
stop stopFlag
|
||||
read func(T)
|
||||
unregister func()
|
||||
unregister func(reflect.Type)
|
||||
logf logger.Logf
|
||||
slow *time.Timer // used to detect slow subscriber service
|
||||
|
||||
// typ is the cached reflect.Type of T. Returned by
|
||||
// subscribeType() and used by the dispatch closure to format
|
||||
// slow-subscriber log messages.
|
||||
typ reflect.Type
|
||||
// typeName is the cached reflect.TypeFor[T]().String() result.
|
||||
// Computed once at construction time so the dispatch closure
|
||||
// (which runs once per delivered event) doesn't allocate a
|
||||
// fresh string on every call. The string is also independent
|
||||
// of T, so it doesn't contribute to per-T stencil cost.
|
||||
typeName string
|
||||
|
||||
// dispatchFn is the per-T dispatch closure. It performs the type
|
||||
// assertion vals.Peek().Event.(T) and runs the typed delivery (either a
|
||||
// user-callback invocation for SubscriberFunc[T] or a typed channel send
|
||||
// for Subscriber[T]). The closure body is non-generic apart from those
|
||||
// two T-bound operations; the bulk of the dispatch work happens in the
|
||||
// non-generic dispatchFunc helper (used by SubscriberFunc) or in the
|
||||
// Subscriber[T].dispatchTyped per-shape stencil.
|
||||
dispatchFn func(
|
||||
ctx context.Context,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
) bool
|
||||
}
|
||||
|
||||
func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *SubscriberFunc[T] {
|
||||
core := newSubscriberCore(r, logf, reflect.TypeFor[T]())
|
||||
// The dispatch closure is the only piece that intrinsically
|
||||
// needs T: it performs the type assertion on the head queue
|
||||
// value and forwards the unboxed value to the user callback.
|
||||
// All non-generic setup (timer, core allocation, unregister
|
||||
// closure) lives in newSubscriberCore so it isn't
|
||||
// duplicated per T.
|
||||
core.dispatchFn = func(
|
||||
ctx context.Context,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
) bool {
|
||||
t := vals.Peek().Event.(T)
|
||||
callDone := make(chan struct{})
|
||||
// `go runFuncCallback(f, t, callDone)` binds its arguments
|
||||
// directly to the new goroutine's frame; using a closure
|
||||
// (`go func() { f(t) }()`) would allocate a closure on the
|
||||
// heap on every dispatched event.
|
||||
go runFuncCallback(f, t, callDone)
|
||||
return dispatchFunc(ctx, core, vals, acceptCh, snapshot, callDone)
|
||||
}
|
||||
return &SubscriberFunc[T]{core: core}
|
||||
}
|
||||
|
||||
// newSubscriberCore performs the non-generic portion of subscriber
|
||||
// construction: timer setup, core struct allocation, and assignment of the
|
||||
// unregister method-value. The caller fills in the per-T dispatchFn
|
||||
// afterward.
|
||||
//
|
||||
// Hoisting this out of the typed constructors (newSubscriber[T] and
|
||||
// newSubscriberFunc[T]) eliminates the bulk of their per-T stencil cost; the
|
||||
// only T-typed instructions left in each generic constructor are the
|
||||
// reflect.TypeFor[T]() call (whose body is shared via the
|
||||
// internal/abi.TypeFor[T] dictionary) and the construction of the dispatch
|
||||
// closure itself.
|
||||
func newSubscriberCore(r *subscribeState, logf logger.Logf, typ reflect.Type) *subscriberCore {
|
||||
slow := time.NewTimer(0)
|
||||
slow.Stop() // reset in dispatch
|
||||
return &SubscriberFunc[T]{
|
||||
read: f,
|
||||
unregister: func() { r.deleteSubscriber(reflect.TypeFor[T]()) },
|
||||
logf: logf,
|
||||
slow: slow,
|
||||
core := &subscriberCore{
|
||||
logf: logf,
|
||||
slow: slow,
|
||||
typ: typ,
|
||||
typeName: typ.String(),
|
||||
}
|
||||
core.unregister = r.deleteSubscriber
|
||||
return core
|
||||
}
|
||||
|
||||
// Close closes the SubscriberFunc, indicating the caller no longer wishes to
|
||||
@@ -300,24 +410,54 @@ func newSubscriberFunc[T any](r *subscribeState, f func(T), logf logger.Logf) *S
|
||||
//
|
||||
// If the [Bus] from which s was created is closed, s is implicitly closed and
|
||||
// does not need to be closed separately.
|
||||
func (s *SubscriberFunc[T]) Close() { s.stop.Stop(); s.unregister() }
|
||||
func (s *SubscriberFunc[T]) Close() { s.core.Close() }
|
||||
|
||||
// subscribeType implements part of the subscriber interface.
|
||||
func (s *SubscriberFunc[T]) subscribeType() reflect.Type { return reflect.TypeFor[T]() }
|
||||
// Close implements the subscriber interface and the user-facing Close on
|
||||
// both Subscriber[T] and SubscriberFunc[T].
|
||||
func (c *subscriberCore) Close() {
|
||||
c.stop.Stop()
|
||||
c.unregister(c.typ)
|
||||
}
|
||||
|
||||
// dispatch implements part of the subscriber interface.
|
||||
func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredEvent], acceptCh func() chan DeliveredEvent, snapshot chan chan []DeliveredEvent) bool {
|
||||
t := vals.Peek().Event.(T)
|
||||
callDone := make(chan struct{})
|
||||
go s.runCallback(t, callDone)
|
||||
// subscribeType implements the subscriber interface.
|
||||
func (c *subscriberCore) subscribeType() reflect.Type { return c.typ }
|
||||
|
||||
// dispatch implements the subscriber interface by invoking the
|
||||
// per-T dispatch closure that was captured at construction time.
|
||||
func (c *subscriberCore) dispatch(
|
||||
ctx context.Context,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
) bool {
|
||||
return c.dispatchFn(ctx, vals, acceptCh, snapshot)
|
||||
}
|
||||
|
||||
// dispatchFunc is the non-generic body of SubscriberFunc[T].dispatch.
|
||||
// It is identical in observable behavior to the original loop; the
|
||||
// only differences are that the dispatched value has already been
|
||||
// unboxed by the caller (and the user callback is already running
|
||||
// on its own goroutine, signaling completion via callDone) and the
|
||||
// slow-subscriber timer / cached type name come from the
|
||||
// non-generic core, not from a per-T struct.
|
||||
//
|
||||
// callDone is closed by runFuncCallback when the user callback returns.
|
||||
func dispatchFunc(
|
||||
ctx context.Context,
|
||||
core *subscriberCore,
|
||||
vals *queue[DeliveredEvent],
|
||||
acceptCh func() chan DeliveredEvent,
|
||||
snapshot chan chan []DeliveredEvent,
|
||||
callDone chan struct{},
|
||||
) bool {
|
||||
start := time.Now()
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
defer s.slow.Stop()
|
||||
core.slow.Reset(slowSubscriberTimeout)
|
||||
defer core.slow.Stop()
|
||||
|
||||
// Keep the cases in this select in sync with subscribeState.pump
|
||||
// above. The only difference should be that this select
|
||||
// delivers a value by calling s.read.
|
||||
// delivers a value by calling the user callback (via the
|
||||
// goroutine spawned by the typed wrapper).
|
||||
for {
|
||||
select {
|
||||
case <-callDone:
|
||||
@@ -327,30 +467,35 @@ func (s *SubscriberFunc[T]) dispatch(ctx context.Context, vals *queue[DeliveredE
|
||||
vals.Add(val)
|
||||
case <-ctx.Done():
|
||||
// Wait for the callback to be complete, but not forever.
|
||||
s.slow.Reset(5 * slowSubscriberTimeout)
|
||||
core.slow.Reset(5 * slowSubscriberTimeout)
|
||||
select {
|
||||
case <-s.slow.C:
|
||||
s.logf("giving up on subscriber for %T after %v at close", t, time.Since(start))
|
||||
case <-core.slow.C:
|
||||
core.logf("giving up on subscriber for %s after %v at close", core.typeName, time.Since(start))
|
||||
if cibuild.On() {
|
||||
all := make([]byte, 2<<20)
|
||||
n := runtime.Stack(all, true)
|
||||
s.logf("goroutine stacks:\n%s", all[:n])
|
||||
core.logf("goroutine stacks:\n%s", all[:n])
|
||||
}
|
||||
case <-callDone:
|
||||
}
|
||||
return false
|
||||
case ch := <-snapshot:
|
||||
ch <- vals.Snapshot()
|
||||
case <-s.slow.C:
|
||||
s.logf("subscriber for %T is slow (%v elapsed)", t, time.Since(start))
|
||||
s.slow.Reset(slowSubscriberTimeout)
|
||||
case <-core.slow.C:
|
||||
core.logf("subscriber for %s is slow (%v elapsed)", core.typeName, time.Since(start))
|
||||
core.slow.Reset(slowSubscriberTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runCallback invokes the callback on v and closes ch when it returns.
|
||||
// This should be run in a goroutine.
|
||||
func (s *SubscriberFunc[T]) runCallback(v T, ch chan struct{}) {
|
||||
defer close(ch)
|
||||
s.read(v)
|
||||
// runFuncCallback runs f(t) and closes done when it returns. It is
|
||||
// the per-T worker spawned as a goroutine for each dispatched
|
||||
// event. Keeping it as a regular generic function (rather than a
|
||||
// closure) means `go runFuncCallback(f, t, done)` binds its
|
||||
// arguments to the goroutine's frame directly, with no per-event
|
||||
// closure allocation. The body is small (defer + one indirect
|
||||
// call), so the per-shape stencil cost is minimal.
|
||||
func runFuncCallback[T any](f func(T), t T, done chan struct{}) {
|
||||
defer close(done)
|
||||
f(t)
|
||||
}
|
||||
+31
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package execqueue implements an ordered asynchronous queue for executing functions.
|
||||
@@ -88,6 +88,36 @@ func (q *ExecQueue) Shutdown() {
|
||||
}
|
||||
}
|
||||
|
||||
// ShutdownAndWait signals the queue to stop, discards any queued
|
||||
// functions that have not started, and waits for the currently
|
||||
// executing function, if any, to complete or ctx to expire.
|
||||
//
|
||||
// It must not be called while holding a lock that a queued function
|
||||
// may acquire, or it will not return until ctx expires.
|
||||
func (q *ExecQueue) ShutdownAndWait(ctx context.Context) error {
|
||||
q.mu.Lock()
|
||||
q.closed = true
|
||||
if q.cancel != nil {
|
||||
q.cancel()
|
||||
}
|
||||
waitCh := q.doneWaiter
|
||||
if q.inFlight && waitCh == nil {
|
||||
waitCh = make(chan struct{})
|
||||
q.doneWaiter = waitCh
|
||||
}
|
||||
q.mu.Unlock()
|
||||
|
||||
if waitCh == nil {
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (q *ExecQueue) initCtxLocked() {
|
||||
if q.ctx == nil {
|
||||
q.ctx, q.cancel = context.WithCancel(context.Background())
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The goroutines package contains utilities for tracking and getting active goroutines.
|
||||
// Package goroutines contains utilities for tracking and getting active goroutines.
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
@@ -52,7 +52,7 @@ func scrubHex(buf []byte) []byte {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
v := fmt.Appendf(nil, "v%d%%%d", len(saw)+1, u64%8)
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package goroutines
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package groupmember verifies group membership of the provided user on the
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package hashx provides a concrete implementation of [hash.Hash]
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package httpbody provides helpers for reading HTTP response bodies
|
||||
// defensively, capping how much memory a response can make the reader
|
||||
// buffer.
|
||||
//
|
||||
// A typical HTTP client applies the cap in its Do method, so every
|
||||
// response it returns is capped unless the request opts out:
|
||||
//
|
||||
// res, err := c.Client.Do(req)
|
||||
// httpbody.LimitSize(res)
|
||||
package httpbody
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/util/ctxkey"
|
||||
)
|
||||
|
||||
// ErrTooLarge is reported by a body wrapped with [LimitSize] once more
|
||||
// than the configured number of bytes have been read. The error text
|
||||
// includes the limit that was exceeded.
|
||||
var ErrTooLarge = errors.New("response body too large")
|
||||
|
||||
// DefaultMaxSize is the default response body size cap, in bytes, used
|
||||
// when a context carries no override set with [WithMaxSize]. HTTP clients
|
||||
// that apply response body caps should honor it, so requests can tune or
|
||||
// disable the cap uniformly across clients.
|
||||
const DefaultMaxSize = 1 << 20
|
||||
|
||||
// maxSizeKey is the context key carrying the response body size cap.
|
||||
var maxSizeKey = ctxkey.New[int64]("httpbody.maxSize", DefaultMaxSize)
|
||||
|
||||
// WithMaxSize returns a context that sets the response body size cap to
|
||||
// max, instead of [DefaultMaxSize], for HTTP clients that apply response
|
||||
// body caps. A max of zero or less disables the cap. Disable it for
|
||||
// responses that stream an unbounded number of individually bounded
|
||||
// messages, like a streaming map response.
|
||||
func WithMaxSize(ctx context.Context, max int64) context.Context {
|
||||
return maxSizeKey.WithValue(ctx, max)
|
||||
}
|
||||
|
||||
// MaxSize returns the response body size cap in ctx: the value set with
|
||||
// [WithMaxSize], or [DefaultMaxSize] if ctx carries none.
|
||||
func MaxSize(ctx context.Context) int64 {
|
||||
return maxSizeKey.Value(ctx)
|
||||
}
|
||||
|
||||
// LimitSize replaces res.Body with a wrapper that fails reads with an
|
||||
// error wrapping [ErrTooLarge] once more than the response body size
|
||||
// limit in res.Request's context, per [WithMaxSize], has been read:
|
||||
// [DefaultMaxSize] by default, or if res has no Request. A limit of zero
|
||||
// or less imposes no limit, removing any limit a previous call set.
|
||||
//
|
||||
// Repeated calls to LimitSize or LimitSizeTo replace the previous limit
|
||||
// rather than compounding it, so a later call can raise or remove the
|
||||
// limit an earlier one set. If bytes have already been read through a
|
||||
// previous limit, the new limit counts bytes read after it was applied.
|
||||
//
|
||||
// Unlike io.LimitReader, which silently truncates at its limit, the
|
||||
// wrapper reports an oversize body so callers can distinguish a short
|
||||
// body from a too-large one. A body of at most the limit, including one
|
||||
// of exactly the limit, reads back without error.
|
||||
func LimitSize(res *http.Response) {
|
||||
max := int64(DefaultMaxSize)
|
||||
if res.Request != nil {
|
||||
max = MaxSize(res.Request.Context())
|
||||
}
|
||||
limitSizeTo(res, max)
|
||||
}
|
||||
|
||||
// LimitSizeTo is like [LimitSize] but caps res.Body at exactly max bytes,
|
||||
// ignoring any override in res.Request's context. A max of zero or less
|
||||
// imposes no limit, removing any limit a previous call set. Repeated
|
||||
// calls to LimitSize or LimitSizeTo replace the previous limit rather
|
||||
// than compounding it.
|
||||
func LimitSizeTo(res *http.Response, max int64) {
|
||||
limitSizeTo(res, max)
|
||||
}
|
||||
|
||||
func limitSizeTo(res *http.Response, max int64) {
|
||||
// Unwrap any limit a previous call set instead of wrapping it again:
|
||||
// nested limits would compound, with the smaller always winning, so
|
||||
// re-limiting could never raise the cap. At most one wrapper can
|
||||
// exist, because this always unwraps first.
|
||||
if lb, ok := res.Body.(*limitedBody); ok {
|
||||
res.Body = lb.body
|
||||
}
|
||||
if max <= 0 {
|
||||
return
|
||||
}
|
||||
res.Body = &limitedBody{body: res.Body, max: max, remain: max}
|
||||
}
|
||||
|
||||
// limitedBody is the io.ReadCloser installed by [LimitSize].
|
||||
type limitedBody struct {
|
||||
body io.ReadCloser
|
||||
max int64 // the limit, for error reporting
|
||||
// remain is the number of bytes still allowed. Once the limit is
|
||||
// exhausted, a one-byte probe of body distinguishes a body of exactly
|
||||
// max bytes (EOF, fine) from one with more to come. remain goes
|
||||
// negative once oversize is detected, and stays there.
|
||||
remain int64
|
||||
}
|
||||
|
||||
func (b *limitedBody) Read(p []byte) (n int, err error) {
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
if b.remain < 0 {
|
||||
return 0, b.tooLarge()
|
||||
}
|
||||
if b.remain == 0 {
|
||||
// The limit is exhausted. A body of exactly max bytes is legal, so
|
||||
// check whether the body has actually ended before declaring it
|
||||
// too large. The probed byte, if any, is dropped: once a body is
|
||||
// too large, callers only want the error.
|
||||
var probe [1]byte
|
||||
m, err := b.body.Read(probe[:])
|
||||
if m == 0 && err != nil {
|
||||
return 0, err // io.EOF, or the underlying error
|
||||
}
|
||||
b.remain = -1
|
||||
return 0, b.tooLarge()
|
||||
}
|
||||
if int64(len(p)) > b.remain {
|
||||
p = p[:b.remain]
|
||||
}
|
||||
n, err = b.body.Read(p)
|
||||
b.remain -= int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (b *limitedBody) Close() error {
|
||||
return b.body.Close()
|
||||
}
|
||||
|
||||
func (b *limitedBody) tooLarge() error {
|
||||
return fmt.Errorf("%w: limit %d bytes", ErrTooLarge, b.max)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package httpm has shorter names for HTTP method constants.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package lineiter iterates over lines in things.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package mak helps make maps. It contains generic helpers to make/assign
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package must assists in calling functions that must succeed.
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// nocasemaps provides efficient functions to set and get entries in Go maps
|
||||
// keyed by a string, where the string is always lower-case.
|
||||
// Package nocasemaps provides efficient functions to set and get entries in Go
|
||||
// maps keyed by a string, where the string is always lower-case.
|
||||
package nocasemaps
|
||||
|
||||
import (
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Code generated by 'go generate'; DO NOT EDIT.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package osdiag
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package osdiag provides loggers for OS-specific diagnostic information.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package osdiag
|
||||
@@ -352,7 +352,7 @@ const (
|
||||
)
|
||||
|
||||
// Note that wsaProtocolInfo needs to be identical to windows.WSAProtocolInfo;
|
||||
// the purpose of this type is to have the ability to use it as a reciever in
|
||||
// the purpose of this type is to have the ability to use it as a receiver in
|
||||
// the path and categoryFlags funcs defined below.
|
||||
type wsaProtocolInfo windows.WSAProtocolInfo
|
||||
|
||||
|
||||
+15
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package osuser
|
||||
@@ -23,7 +23,7 @@ func GetGroupIds(user *user.User) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if runtime.GOOS != "linux" {
|
||||
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||
return user.GroupIds()
|
||||
}
|
||||
|
||||
@@ -46,13 +46,24 @@ func getGroupIdsWithId(usernameOrUID string) ([]string, error) {
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "id", "-Gz", usernameOrUID)
|
||||
out, err := cmd.Output()
|
||||
if runtime.GOOS == "freebsd" {
|
||||
cmd = exec.CommandContext(ctx, "id", "-G", usernameOrUID)
|
||||
}
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running 'id' command: %w", err)
|
||||
}
|
||||
|
||||
return parseGroupIds(out), nil
|
||||
}
|
||||
|
||||
func parseGroupIds(cmdOutput []byte) []string {
|
||||
return strings.Split(strings.Trim(string(cmdOutput), "\n\x00"), "\x00")
|
||||
s := strings.TrimSpace(string(cmdOutput))
|
||||
// Parse NUL-delimited output.
|
||||
if strings.ContainsRune(s, '\x00') {
|
||||
return strings.Split(strings.Trim(s, "\x00"), "\x00")
|
||||
}
|
||||
// Parse whitespace-delimited output.
|
||||
return strings.Fields(s)
|
||||
}
|
||||
+77
-8
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package osuser implements OS user lookup. It's a wrapper around os/user that
|
||||
@@ -8,11 +8,14 @@ package osuser
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -50,6 +53,12 @@ func LookupByUsername(username string) (*user.User, error) {
|
||||
// lookupStd is either user.Lookup or user.LookupId.
|
||||
type lookupStd func(string) (*user.User, error)
|
||||
|
||||
var execGetent = func(ctx context.Context, args ...string) ([]byte, error) {
|
||||
return exec.CommandContext(ctx, "getent", args...).Output()
|
||||
}
|
||||
|
||||
var getentDoubleDashSupported = sync.OnceValue(probeGetentDoubleDashSupport)
|
||||
|
||||
func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, string, error) {
|
||||
// Skip getent entirely on Non-Unix platforms that won't ever have it.
|
||||
// (Using HasPrefix for "wasip1", anticipating that WASI support will
|
||||
@@ -118,6 +127,12 @@ func checkGetentInput(usernameOrUID string) bool {
|
||||
if len(usernameOrUID) > maxUid || len(usernameOrUID) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Leading dashes aren't valid for usernames.
|
||||
if strings.HasPrefix(usernameOrUID, "-") {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range usernameOrUID {
|
||||
if r < ' ' || r == 0x7f || r == utf8.RuneError { // TODO(bradfitz): more?
|
||||
return false
|
||||
@@ -139,23 +154,77 @@ func userLookupGetent(usernameOrUID string, std lookupStd) (*user.User, string,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, "getent", "passwd", usernameOrUID).Output()
|
||||
|
||||
args := []string{"passwd"}
|
||||
// Append "--" only if the local getent accepts it, to prevent a username or
|
||||
// UID from being interpreted as an option without breaking getent variants
|
||||
// that do not support end-of-options parsing here.
|
||||
if getentDoubleDashSupported() {
|
||||
args = append(args, "--")
|
||||
}
|
||||
args = append(args, usernameOrUID)
|
||||
|
||||
out, err := execGetent(ctx, args...)
|
||||
if err != nil {
|
||||
log.Printf("error calling getent for user %q: %v", usernameOrUID, err)
|
||||
u, err := std(usernameOrUID)
|
||||
return u, "", err
|
||||
}
|
||||
u, shell, err := parseGetentUser(out)
|
||||
if err != nil {
|
||||
log.Printf("getent for user %q returned invalid output %q: %v", usernameOrUID, out, err)
|
||||
u, err := std(usernameOrUID)
|
||||
return u, "", err
|
||||
}
|
||||
return u, shell, nil
|
||||
}
|
||||
|
||||
func probeGetentDoubleDashSupport() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
out, err := execGetent(ctx, "passwd", "root")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
outDoubleDash, err := execGetent(ctx, "passwd", "--", "root")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
u, shell, err := parseGetentUser(out)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
uDoubleDash, shellDoubleDash, err := parseGetentUser(outDoubleDash)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Short-circuit if the user is obviously incorrect
|
||||
if uDoubleDash.Name != "root" || uDoubleDash.Uid != "0" || uDoubleDash.Gid != "0" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(u, uDoubleDash) || shell != shellDoubleDash {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func parseGetentUser(out []byte) (*user.User, string, error) {
|
||||
// output is "alice:x:1001:1001:Alice Smith,,,:/home/alice:/bin/bash"
|
||||
f := strings.SplitN(strings.TrimSpace(string(out)), ":", 10)
|
||||
for len(f) < 7 {
|
||||
f = append(f, "")
|
||||
}
|
||||
var mandatoryFields = []int{0, 2, 3, 5}
|
||||
for _, v := range mandatoryFields {
|
||||
if f[v] == "" {
|
||||
log.Printf("getent for user %q returned invalid output: %q", usernameOrUID, out)
|
||||
u, err := std(usernameOrUID)
|
||||
return u, "", err
|
||||
var mandatoryFields = map[int]string{0: "Username", 2: "Uid", 3: "Gid", 5: "HomeDir"}
|
||||
for k, v := range mandatoryFields {
|
||||
if f[k] == "" {
|
||||
return nil, "", fmt.Errorf("missing mandatory field %q", v)
|
||||
}
|
||||
}
|
||||
return &user.User{
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package race contains a helper to "race" two functions, returning the first
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !race
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build race
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package racebuild exports a constant about whether the current binary
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package rands contains utility functions for randomness.
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ringlog contains a limited-size concurrency-safe generic ring log.
|
||||
@@ -70,7 +70,12 @@ func (rb *RingLog[T]) Len() int {
|
||||
}
|
||||
|
||||
// Clear will empty the ring log.
|
||||
//
|
||||
// It does nothing if rb is nil.
|
||||
func (rb *RingLog[T]) Clear() {
|
||||
if rb == nil {
|
||||
return
|
||||
}
|
||||
rb.mu.Lock()
|
||||
defer rb.mu.Unlock()
|
||||
rb.pos = 0
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
@@ -152,7 +152,7 @@ func (s bitSet) values() iter.Seq[uint64] {
|
||||
return func(yield func(uint64) bool) {
|
||||
// Hyrum-proofing: randomly iterate in forwards or reverse.
|
||||
if rand.Uint64()%2 == 0 {
|
||||
for i := 0; i < bits.UintSize; i++ {
|
||||
for i := range bits.UintSize {
|
||||
if s.contains(uint64(i)) && !yield(uint64(i)) {
|
||||
return
|
||||
}
|
||||
|
||||
+94
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package set contains set types.
|
||||
@@ -6,7 +6,12 @@ package set
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"iter"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
// Set is a set of T.
|
||||
@@ -24,14 +29,35 @@ func Of[T comparable](slice ...T) Set[T] {
|
||||
return s
|
||||
}
|
||||
|
||||
// OfSliceView returns a new set constructed from the elements in v.
|
||||
func OfSliceView[T comparable](v views.Slice[T]) Set[T] {
|
||||
s := make(Set[T], v.Len())
|
||||
s.AddSliceView(v)
|
||||
return s
|
||||
}
|
||||
|
||||
// Clone returns a new set cloned from the elements in s.
|
||||
func (s Set[T]) Clone() Set[T] {
|
||||
return maps.Clone(s)
|
||||
}
|
||||
|
||||
// All returns an iterator over all elements in s.
|
||||
// The iteration order is not specified
|
||||
// and is not guaranteed to be the same from one call to the next.
|
||||
func (s Set[T]) All() iter.Seq[T] {
|
||||
return maps.Keys(s)
|
||||
}
|
||||
|
||||
// Add adds e to s.
|
||||
func (s Set[T]) Add(e T) { s[e] = struct{}{} }
|
||||
|
||||
// AddSeq adds each element of es to s.
|
||||
func (s Set[T]) AddSeq(es iter.Seq[T]) {
|
||||
for e := range es {
|
||||
s.Add(e)
|
||||
}
|
||||
}
|
||||
|
||||
// AddSlice adds each element of es to s.
|
||||
func (s Set[T]) AddSlice(es []T) {
|
||||
for _, e := range es {
|
||||
@@ -39,6 +65,13 @@ func (s Set[T]) AddSlice(es []T) {
|
||||
}
|
||||
}
|
||||
|
||||
// AddSliceView adds each element of v to s.
|
||||
func (s Set[T]) AddSliceView(v views.Slice[T]) {
|
||||
for _, e := range v.All() {
|
||||
s.Add(e)
|
||||
}
|
||||
}
|
||||
|
||||
// AddSet adds each element of es to s.
|
||||
func (s Set[T]) AddSet(es Set[T]) {
|
||||
for e := range es {
|
||||
@@ -53,19 +86,77 @@ func (s *Set[T]) Make() {
|
||||
}
|
||||
}
|
||||
|
||||
// Slice returns the elements of the set as a slice. The elements will not be
|
||||
// in any particular order.
|
||||
// Slice returns the elements of the set as a slice. If the element type is
|
||||
// ordered (integers, floats, or strings), the elements are returned in sorted
|
||||
// order. Otherwise, the order is not defined.
|
||||
func (s Set[T]) Slice() []T {
|
||||
es := make([]T, 0, s.Len())
|
||||
for k := range s {
|
||||
es = append(es, k)
|
||||
}
|
||||
if f := genOrderedSwapper(reflect.TypeFor[T]()); f != nil {
|
||||
sort.Slice(es, f(reflect.ValueOf(es)))
|
||||
}
|
||||
return es
|
||||
}
|
||||
|
||||
// genOrderedSwapper returns a generator for a swap function that can be used to
|
||||
// sort a slice of the given type. If rt is not an ordered type,
|
||||
// genOrderedSwapper returns nil.
|
||||
func genOrderedSwapper(rt reflect.Type) func(reflect.Value) func(i, j int) bool {
|
||||
switch rt.Kind() {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return func(rv reflect.Value) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return rv.Index(i).Uint() < rv.Index(j).Uint()
|
||||
}
|
||||
}
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return func(rv reflect.Value) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return rv.Index(i).Int() < rv.Index(j).Int()
|
||||
}
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return func(rv reflect.Value) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return rv.Index(i).Float() < rv.Index(j).Float()
|
||||
}
|
||||
}
|
||||
case reflect.String:
|
||||
return func(rv reflect.Value) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
return rv.Index(i).String() < rv.Index(j).String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes e from the set.
|
||||
func (s Set[T]) Delete(e T) { delete(s, e) }
|
||||
|
||||
// DeleteSeq removes all elements in es from the set.
|
||||
func (s Set[T]) DeleteSeq(es iter.Seq[T]) {
|
||||
for e := range es {
|
||||
s.Delete(e)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteSlice removes all elements in es from the set.
|
||||
func (s Set[T]) DeleteSlice(es []T) {
|
||||
for _, e := range es {
|
||||
s.Delete(e)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteSet removes all elements in es from the set.
|
||||
func (s Set[T]) DeleteSet(es Set[T]) {
|
||||
for e := range es {
|
||||
s.Delete(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Contains reports whether s contains e.
|
||||
func (s Set[T]) Contains(e T) bool {
|
||||
_, ok := s[e]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package set
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
@@ -36,7 +36,7 @@ var errGoexit = errors.New("runtime.Goexit was called")
|
||||
// A panicError is an arbitrary value recovered from a panic
|
||||
// with the stack trace during the execution of given function.
|
||||
type panicError struct {
|
||||
value interface{}
|
||||
value any
|
||||
stack []byte
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (p *panicError) Error() string {
|
||||
return fmt.Sprintf("%v\n\n%s", p.value, p.stack)
|
||||
}
|
||||
|
||||
func newPanicError(v interface{}) error {
|
||||
func newPanicError(v any) error {
|
||||
stack := debug.Stack()
|
||||
|
||||
// The first line of the stack trace is of the form "goroutine N [status]:"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package slicesx contains some helpful generic slice functions.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package internal contains miscellaneous functions and types
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package loggerx provides logging functions to the rest of the syspolicy packages.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package metrics provides logging and reporting for policy settings and scopes.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package metrics
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package pkey defines the keys used to store system policies in the registry.
|
||||
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// implicitDefinitions is a list of [setting.Definition] that will be registered
|
||||
// automatically when the policy setting definitions are first used by the syspolicy package hierarchy.
|
||||
// This includes the first time a policy needs to be read from any source.
|
||||
var implicitDefinitions = []*setting.Definition{
|
||||
// Device policy settings (can only be configured on a per-device basis):
|
||||
setting.NewDefinition(pkey.AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
|
||||
setting.NewDefinition(pkey.AllowExitNodeOverride, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AllowTailscaledRestart, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.AuthKey, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ControlURL, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.EnableDNSRegistration, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ExitNodeID, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ExitNodeIP, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.EncryptState, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.Hostname, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(pkey.LogTarget, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(pkey.ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||
setting.NewDefinition(pkey.Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.HardwareAttestation, setting.DeviceSetting, setting.BooleanValue),
|
||||
|
||||
// User policy settings (can be configured on a user- or device-basis):
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
|
||||
setting.NewDefinition(pkey.ManagedByCaption, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.ManagedByURL, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(pkey.NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(pkey.OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
}
|
||||
|
||||
func init() {
|
||||
internal.Init.MustDefer(func() error {
|
||||
// Avoid implicit [setting.Definition] registration during tests.
|
||||
// Each test should control which policy settings to register.
|
||||
// Use [setting.SetDefinitionsForTest] to specify necessary definitions,
|
||||
// or [setWellKnownSettingsForTest] to set implicit definitions for the test duration.
|
||||
if testenv.InTest() {
|
||||
return nil
|
||||
}
|
||||
for _, d := range implicitDefinitions {
|
||||
setting.RegisterDefinition(d)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_syspolicy
|
||||
|
||||
package policyclient
|
||||
|
||||
import "tailscale.com/util/syspolicy/setting"
|
||||
|
||||
// PolicySnapshot is an alias for [settings.Snapshot] unless syspolicy is omitted
|
||||
// from the build.
|
||||
type policySnapshot = setting.Snapshot
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_syspolicy
|
||||
|
||||
package policyclient
|
||||
|
||||
// PolicySnapshot is a stub when syspolicy is omitted from the build.
|
||||
type policySnapshot struct{}
|
||||
+19
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package policyclient contains the minimal syspolicy interface as needed by
|
||||
@@ -62,11 +62,21 @@ type Client interface {
|
||||
HasAnyOf(keys ...pkey.Key) (bool, error)
|
||||
|
||||
// RegisterChangeCallback registers a callback function that will be called
|
||||
// whenever a policy change is detected. It returns a function to unregister
|
||||
// the callback and an error if the registration fails.
|
||||
RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error)
|
||||
// whenever a policy change is detected. If uid is empty, the callback fires
|
||||
// for device-scope policy changes. If uid is non-empty, it fires for changes
|
||||
// to that user's effective policy (device + user policies merged). It returns
|
||||
// a function to unregister the callback and an error if the registration fails.
|
||||
RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error)
|
||||
|
||||
// GetPolicySnapshot returns the effective policy snapshot for the given user ID.
|
||||
// If uid is empty, returns the default-scope policy. Returns nil, nil if policy
|
||||
// snapshots are not supported.
|
||||
GetPolicySnapshot(uid string) (*PolicySnapshot, error)
|
||||
}
|
||||
|
||||
// PolicySnapshot is an alias for [setting.Snapshot] unless syspolicy is omitted from the build.
|
||||
type PolicySnapshot = policySnapshot
|
||||
|
||||
// Get returns a non-nil [Client] implementation as a function of the
|
||||
// build tags. It returns a no-op implementation if the full syspolicy
|
||||
// package is omitted from the build, or in tests.
|
||||
@@ -140,6 +150,10 @@ func (NoPolicyClient) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
|
||||
func (NoPolicyClient) SetDebugLoggingEnabled(enabled bool) {}
|
||||
|
||||
func (NoPolicyClient) RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error) {
|
||||
func (NoPolicyClient) RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error) {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetPolicySnapshot(uid string) (*PolicySnapshot, error) {
|
||||
return nil, nil
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package ptype contains types used by syspolicy.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package rsop facilitates [source.Store] registration via [RegisterStore]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
+3
-5
@@ -1,12 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,7 +37,7 @@ type ErrorText string
|
||||
|
||||
// NewErrorText returns a [ErrorText] with the specified error message.
|
||||
func NewErrorText(text string) *ErrorText {
|
||||
return ptr.To(ErrorText(text))
|
||||
return new(ErrorText(text))
|
||||
}
|
||||
|
||||
// MaybeErrorText returns an [ErrorText] with the text of the specified error,
|
||||
@@ -51,7 +49,7 @@ func MaybeErrorText(err error) *ErrorText {
|
||||
if err, ok := err.(*ErrorText); ok {
|
||||
return err
|
||||
}
|
||||
return ptr.To(ErrorText(err.Error()))
|
||||
return new(ErrorText(err.Error()))
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package setting contains types for defining and representing policy settings.
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package setting
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// hujsonStandardize is set to hujson.Standardize by json_policy_store_hujson.go
|
||||
// on platforms that have HuJSON support compiled in. When non-nil, the JSON
|
||||
// policy file may use HuJSON (comments and trailing commas); when nil, the
|
||||
// file must be pure standard JSON.
|
||||
var hujsonStandardize func([]byte) ([]byte, error)
|
||||
|
||||
var _ Store = (*JSONPolicyStore)(nil)
|
||||
|
||||
// JSONPolicyStore is a [Store] backed by a JSON object that maps policy
|
||||
// setting keys to values. It is a read-only snapshot; the underlying map
|
||||
// is captured at construction time and never re-read.
|
||||
//
|
||||
// JSON values are mapped to policy setting types as follows:
|
||||
// - strings map to [setting.StringValue], [setting.PreferenceOptionValue],
|
||||
// [setting.VisibilityValue], and [setting.DurationValue]. For
|
||||
// [setting.DurationValue], the string is parsed by [time.ParseDuration]
|
||||
// elsewhere in the package (e.g. "24h", "5m").
|
||||
// - booleans map to [setting.BooleanValue].
|
||||
// - numbers map to [setting.IntegerValue]. Negative or non-integer values
|
||||
// are rejected with [setting.ErrTypeMismatch].
|
||||
// - arrays of strings map to [setting.StringListValue].
|
||||
type JSONPolicyStore struct {
|
||||
m map[string]any
|
||||
}
|
||||
|
||||
// NewJSONPolicyStore returns a new [JSONPolicyStore] backed by the given map.
|
||||
// A nil or empty map results in a store that reports every key as
|
||||
// [setting.ErrNotConfigured].
|
||||
func NewJSONPolicyStore(m map[string]any) *JSONPolicyStore {
|
||||
return &JSONPolicyStore{m: m}
|
||||
}
|
||||
|
||||
// NewJSONPolicyStoreFromFile reads the file at path and returns a new
|
||||
// [JSONPolicyStore] backed by its contents. The file must contain a JSON
|
||||
// object at its top level. JSON numbers are decoded as [json.Number] to
|
||||
// preserve precision for [setting.IntegerValue] settings.
|
||||
func NewJSONPolicyStoreFromFile(path string) (*JSONPolicyStore, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewJSONPolicyStoreFromBytes(data)
|
||||
}
|
||||
|
||||
// NewJSONPolicyStoreFromBytes is like [NewJSONPolicyStoreFromFile] but reads
|
||||
// from data instead of a file. When HuJSON support is linked into the build,
|
||||
// data may be HuJSON (comments and trailing commas allowed); otherwise it
|
||||
// must be pure standard JSON.
|
||||
func NewJSONPolicyStoreFromBytes(data []byte) (*JSONPolicyStore, error) {
|
||||
if buildfeatures.HasHuJSONConf && hujsonStandardize != nil {
|
||||
std, err := hujsonStandardize(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: parsing HuJSON/JSON: %w", err)
|
||||
}
|
||||
data = std
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
dec.UseNumber()
|
||||
var m map[string]any
|
||||
if err := dec.Decode(&m); err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: parsing JSON: %w", err)
|
||||
}
|
||||
return &JSONPolicyStore{m: m}, nil
|
||||
}
|
||||
|
||||
// ReadString implements [Store].
|
||||
func (s *JSONPolicyStore) ReadString(key pkey.Key) (string, error) {
|
||||
v, ok := s.m[string(key)]
|
||||
if !ok {
|
||||
return "", setting.ErrNotConfigured
|
||||
}
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w: %q is %T, want string", setting.ErrTypeMismatch, key, v)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// ReadBoolean implements [Store].
|
||||
func (s *JSONPolicyStore) ReadBoolean(key pkey.Key) (bool, error) {
|
||||
v, ok := s.m[string(key)]
|
||||
if !ok {
|
||||
return false, setting.ErrNotConfigured
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%w: %q is %T, want bool", setting.ErrTypeMismatch, key, v)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [Store].
|
||||
func (s *JSONPolicyStore) ReadUInt64(key pkey.Key) (uint64, error) {
|
||||
v, ok := s.m[string(key)]
|
||||
if !ok {
|
||||
return 0, setting.ErrNotConfigured
|
||||
}
|
||||
switch n := v.(type) {
|
||||
case json.Number:
|
||||
u, err := strconv.ParseUint(n.String(), 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%w: %q is not a uint64: %v", setting.ErrTypeMismatch, key, err)
|
||||
}
|
||||
return u, nil
|
||||
case float64:
|
||||
if n < 0 || n > math.MaxUint64 || n != math.Trunc(n) {
|
||||
return 0, fmt.Errorf("%w: %q (%v) is not a uint64", setting.ErrTypeMismatch, key, n)
|
||||
}
|
||||
return uint64(n), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("%w: %q is %T, want number", setting.ErrTypeMismatch, key, v)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadStringArray implements [Store].
|
||||
func (s *JSONPolicyStore) ReadStringArray(key pkey.Key) ([]string, error) {
|
||||
v, ok := s.m[string(key)]
|
||||
if !ok {
|
||||
return nil, setting.ErrNotConfigured
|
||||
}
|
||||
arr, ok := v.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q is %T, want array", setting.ErrTypeMismatch, key, v)
|
||||
}
|
||||
res := make([]string, len(arr))
|
||||
for i, e := range arr {
|
||||
str, ok := e.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q[%d] is %T, want string", setting.ErrTypeMismatch, key, i, e)
|
||||
}
|
||||
res[i] = str
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Validate checks that every key in the parsed JSON corresponds to a
|
||||
// registered policy setting (per [setting.Definitions]) and that its value
|
||||
// can be successfully decoded as the registered setting's type. It joins
|
||||
// all problems into a single error so callers see every issue at once
|
||||
// instead of one per startup-then-runtime cycle.
|
||||
//
|
||||
// Validate triggers registration of any deferred setting definitions, so
|
||||
// it should only be called after all init-time registrations have run.
|
||||
func (s *JSONPolicyStore) Validate() error {
|
||||
defs, err := setting.Definitions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
byKey := make(map[pkey.Key]*setting.Definition, len(defs))
|
||||
for _, d := range defs {
|
||||
byKey[d.Key()] = d
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(s.m))
|
||||
for k := range s.m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var errs []error
|
||||
for _, k := range keys {
|
||||
def, ok := byKey[pkey.Key(k)]
|
||||
if !ok {
|
||||
errs = append(errs, fmt.Errorf("unknown policy setting %q", k))
|
||||
continue
|
||||
}
|
||||
if err := s.validateValue(def); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%q: %w", k, err))
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// validateValue is like [readPolicySettingValue] but is stricter for the
|
||||
// enum-like [setting.PreferenceOptionValue] and [setting.VisibilityValue]
|
||||
// types: their runtime UnmarshalText silently coerces unknown strings to a
|
||||
// default, which is fine at read time but defeats the point of load-time
|
||||
// validation, so this checks the raw string against the known values.
|
||||
func (s *JSONPolicyStore) validateValue(def *setting.Definition) error {
|
||||
key := def.Key()
|
||||
switch def.Type() {
|
||||
case setting.PreferenceOptionValue:
|
||||
str, err := s.ReadString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch str {
|
||||
case "always", "never", "user-decides":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`%w: %q is not a valid PreferenceOption ("always", "never", or "user-decides")`, setting.ErrTypeMismatch, str)
|
||||
case setting.VisibilityValue:
|
||||
str, err := s.ReadString(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch str {
|
||||
case "show", "hide":
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`%w: %q is not a valid Visibility ("show" or "hide")`, setting.ErrTypeMismatch, str)
|
||||
default:
|
||||
_, err := readPolicySettingValue(s, def)
|
||||
return err
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ios && !android && !ts_omit_hujsonconf
|
||||
|
||||
package source
|
||||
|
||||
import "github.com/tailscale/hujson"
|
||||
|
||||
// Only link the hujson package on platforms that use it, to reduce binary
|
||||
// size & memory a bit. iOS and Android don't load syspolicy files from
|
||||
// disk so they don't need HuJSON either.
|
||||
//
|
||||
// While the linker's deadcode mostly handles this today, this keeps us
|
||||
// honest for the future.
|
||||
|
||||
func init() {
|
||||
hujsonStandardize = hujson.Standardize
|
||||
}
|
||||
+10
-4
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
@@ -91,7 +91,7 @@ func newReader(store Store, origin *setting.Origin) (*Reader, error) {
|
||||
}
|
||||
|
||||
// GetSettings returns the current [*setting.Snapshot],
|
||||
// re-reading it from from the underlying [Store] only if the policy
|
||||
// re-reading it from the underlying [Store] only if the policy
|
||||
// has changed since it was read last. It never fails and returns
|
||||
// the previous version of the policy settings if a read attempt fails.
|
||||
func (r *Reader) GetSettings() *setting.Snapshot {
|
||||
@@ -125,6 +125,11 @@ func (r *Reader) ReadSettings() (*setting.Snapshot, error) {
|
||||
func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.closing {
|
||||
// The reader is closing (or already closed) and r.store may be nil.
|
||||
// Return the last known policy instead of reading from the store.
|
||||
return r.lastPolicy, nil
|
||||
}
|
||||
if r.upToDate && !force {
|
||||
return r.lastPolicy, nil
|
||||
}
|
||||
@@ -267,12 +272,13 @@ func (r *Reader) Close() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.store = nil
|
||||
|
||||
close(r.doneCh)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
// Nil out the store only while holding r.mu; reload reads r.store
|
||||
// under the same lock, so writing it unlocked would be a data race.
|
||||
r.store = nil
|
||||
for _, c := range r.sessions {
|
||||
c.closeInternal()
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package source defines interfaces for policy stores,
|
||||
|
||||
+29
-17
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
@@ -117,12 +117,12 @@ func NewUserPlatformPolicyStore(token windows.Token) (*PlatformPolicyStore, erro
|
||||
return newPlatformPolicyStore(gp.UserPolicy, softwareKey, policyLock), nil
|
||||
}
|
||||
|
||||
func newPlatformPolicyStore(scope gp.Scope, softwareKey registry.Key, policyLock *gp.PolicyLock) *PlatformPolicyStore {
|
||||
func newPlatformPolicyStore(scope gp.Scope, softwareKey registry.Key, policyLock lockableCloser) *PlatformPolicyStore {
|
||||
return &PlatformPolicyStore{
|
||||
scope: scope,
|
||||
softwareKey: softwareKey,
|
||||
done: make(chan struct{}),
|
||||
policyLock: &optionalPolicyLock{PolicyLock: policyLock},
|
||||
policyLock: &optionalPolicyLock{lockableCloser: policyLock},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,13 +468,14 @@ type gpLockState int
|
||||
const (
|
||||
gpUnlocked = gpLockState(iota)
|
||||
gpLocked
|
||||
gpLockRestricted // the lock could not be acquired due to a restriction in place
|
||||
gpLockSkipped // the lock was not acquired, but reading proceeds without it
|
||||
)
|
||||
|
||||
// optionalPolicyLock is a wrapper around [gp.PolicyLock] that locks
|
||||
// and unlocks the underlying [gp.PolicyLock].
|
||||
//
|
||||
// If the [gp.PolicyLock.Lock] returns [gp.ErrLockRestricted], the error is ignored,
|
||||
// If [gp.PolicyLock.Lock] returns [gp.ErrLockRestricted] or
|
||||
// [windows.ERROR_ACCESS_DENIED], the error is ignored,
|
||||
// and calling [optionalPolicyLock.Unlock] is a no-op.
|
||||
//
|
||||
// The underlying GP lock is kinda optional: it is safe to read policy settings
|
||||
@@ -483,27 +484,38 @@ const (
|
||||
//
|
||||
// It is not safe for concurrent use.
|
||||
type optionalPolicyLock struct {
|
||||
*gp.PolicyLock
|
||||
state gpLockState
|
||||
lockableCloser // typically a [*gp.PolicyLock]
|
||||
state gpLockState
|
||||
}
|
||||
|
||||
// Lock acquires the underlying [gp.PolicyLock], returning an error on failure.
|
||||
//
|
||||
// If the lock cannot be acquired due to a restriction in place
|
||||
// (e.g., attempting to acquire a lock while the service is starting),
|
||||
// the lock is considered to be held, the method returns nil, and a subsequent
|
||||
// call to [Unlock] is a no-op.
|
||||
// or because the caller is not allowed to acquire it, the lock is considered
|
||||
// to be held, the method returns nil, and a subsequent call to [Unlock] is a no-op.
|
||||
// The latter happens with [windows.ERROR_ACCESS_DENIED] when a user policy lock
|
||||
// is requested by a process whose user has no interactive logon session, such as
|
||||
// tailscaled started from an SSH session; policy settings are then read
|
||||
// without the lock.
|
||||
//
|
||||
// It is a runtime error to call Lock when the lock is already held.
|
||||
func (o *optionalPolicyLock) Lock() error {
|
||||
if o.state != gpUnlocked {
|
||||
panic("already locked")
|
||||
}
|
||||
switch err := o.PolicyLock.Lock(); err {
|
||||
case nil:
|
||||
err := o.lockableCloser.Lock()
|
||||
switch {
|
||||
case err == nil:
|
||||
o.state = gpLocked
|
||||
return nil
|
||||
case gp.ErrLockRestricted:
|
||||
case errors.Is(err, gp.ErrLockRestricted):
|
||||
loggerx.Errorf("GP lock not acquired: %v", err)
|
||||
o.state = gpLockRestricted
|
||||
o.state = gpLockSkipped
|
||||
return nil
|
||||
case errors.Is(err, windows.ERROR_ACCESS_DENIED):
|
||||
loggerx.Errorf("GP lock not acquired (%v); reading policy settings without it", err)
|
||||
o.state = gpLockSkipped
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
@@ -515,10 +527,10 @@ func (o *optionalPolicyLock) Lock() error {
|
||||
func (o *optionalPolicyLock) Unlock() {
|
||||
switch o.state {
|
||||
case gpLocked:
|
||||
o.PolicyLock.Unlock()
|
||||
case gpLockRestricted:
|
||||
// The GP lock wasn't acquired due to a restriction in place
|
||||
// when [optionalPolicyLock.Lock] was called. Unlock is a no-op.
|
||||
o.lockableCloser.Unlock()
|
||||
case gpLockSkipped:
|
||||
// The GP lock wasn't acquired when [optionalPolicyLock.Lock]
|
||||
// was called. Unlock is a no-op.
|
||||
case gpUnlocked:
|
||||
panic("not locked")
|
||||
default:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
-264
@@ -1,264 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package syspolicy contains the implementation of system policy management.
|
||||
// Calling code should use the client interface in
|
||||
// tailscale.com/util/syspolicy/policyclient.
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/syspolicy/ptype"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotConfigured is returned when the requested policy setting is not configured.
|
||||
ErrNotConfigured = setting.ErrNotConfigured
|
||||
// ErrTypeMismatch is returned when there's a type mismatch between the actual type
|
||||
// of the setting value and the expected type.
|
||||
ErrTypeMismatch = setting.ErrTypeMismatch
|
||||
// ErrNoSuchKey is returned by [setting.DefinitionOf] when no policy setting
|
||||
// has been registered with the specified key.
|
||||
//
|
||||
// This error is also returned by a (now deprecated) [Handler] when the specified
|
||||
// key does not have a value set. While the package maintains compatibility with this
|
||||
// usage of ErrNoSuchKey, it is recommended to return [ErrNotConfigured] from newer
|
||||
// [source.Store] implementations.
|
||||
ErrNoSuchKey = setting.ErrNoSuchKey
|
||||
)
|
||||
|
||||
// RegisterStore registers a new policy [source.Store] with the specified name and [setting.PolicyScope].
|
||||
//
|
||||
// It is a shorthand for [rsop.RegisterStore].
|
||||
func RegisterStore(name string, scope setting.PolicyScope, store source.Store) (*rsop.StoreRegistration, error) {
|
||||
return rsop.RegisterStore(name, scope, store)
|
||||
}
|
||||
|
||||
// hasAnyOf returns whether at least one of the specified policy settings is configured,
|
||||
// or an error if no keys are provided or the check fails.
|
||||
func hasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
if len(keys) == 0 {
|
||||
return false, errors.New("at least one key must be specified")
|
||||
}
|
||||
policy, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
effective := policy.Get()
|
||||
for _, k := range keys {
|
||||
_, err := effective.GetErr(k)
|
||||
if errors.Is(err, setting.ErrNotConfigured) || errors.Is(err, setting.ErrNoSuchKey) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// getString returns a string policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func getString(key pkey.Key, defaultValue string) (string, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// getUint64 returns a numeric policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func getUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// getBoolean returns a boolean policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func getBoolean(key pkey.Key, defaultValue bool) (bool, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// getStringArray returns a multi-string policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func getStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// getPreferenceOption loads a policy from the registry that can be
|
||||
// managed by an enterprise policy management system and allows administrative
|
||||
// overrides of users' choices in a way that we do not want tailcontrol to have
|
||||
// the authority to set. It describes user-decides/always/never options, where
|
||||
// "always" and "never" remove the user's ability to make a selection. If not
|
||||
// present or set to a different value, defaultValue (and a nil error) is returned.
|
||||
func getPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
|
||||
return getCurrentPolicySettingValue(name, defaultValue)
|
||||
}
|
||||
|
||||
// getVisibility loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes show/hide decisions
|
||||
// for UI elements. The registry value should be a string set to "show" (return
|
||||
// true) or "hide" (return true). If not present or set to a different value,
|
||||
// "show" (return false) is the default.
|
||||
func getVisibility(name pkey.Key) (ptype.Visibility, error) {
|
||||
return getCurrentPolicySettingValue(name, ptype.VisibleByPolicy)
|
||||
}
|
||||
|
||||
// getDuration loads a policy from the registry that can be managed
|
||||
// by an enterprise policy management system and describes a duration for some
|
||||
// action. The registry value should be a string that time.ParseDuration
|
||||
// understands. If the registry value is "" or can not be processed,
|
||||
// defaultValue is returned instead.
|
||||
func getDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
d, err := getCurrentPolicySettingValue(name, defaultValue)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
if d < 0 {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// registerChangeCallback adds a function that will be called whenever the effective policy
|
||||
// for the default scope changes. The returned function can be used to unregister the callback.
|
||||
func registerChangeCallback(cb rsop.PolicyChangeCallback) (unregister func(), err error) {
|
||||
effective, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return effective.RegisterChangeCallback(cb), nil
|
||||
}
|
||||
|
||||
// getCurrentPolicySettingValue returns the value of the policy setting
|
||||
// specified by its key from the [rsop.Policy] of the [setting.DefaultScope]. It
|
||||
// returns def if the policy setting is not configured, or an error if it has
|
||||
// an error or could not be converted to the specified type T.
|
||||
func getCurrentPolicySettingValue[T setting.ValueType](key pkey.Key, def T) (T, error) {
|
||||
effective, err := rsop.PolicyFor(setting.DefaultScope())
|
||||
if err != nil {
|
||||
return def, err
|
||||
}
|
||||
value, err := effective.Get().GetErr(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, setting.ErrNotConfigured) || errors.Is(err, setting.ErrNoSuchKey) {
|
||||
return def, nil
|
||||
}
|
||||
return def, err
|
||||
}
|
||||
if res, ok := value.(T); ok {
|
||||
return res, nil
|
||||
}
|
||||
return convertPolicySettingValueTo(value, def)
|
||||
}
|
||||
|
||||
func convertPolicySettingValueTo[T setting.ValueType](value any, def T) (T, error) {
|
||||
// Convert [PreferenceOption], [Visibility], or [time.Duration] back to a string
|
||||
// if someone requests a string instead of the actual setting's value.
|
||||
// TODO(nickkhyl): check if this behavior is relied upon anywhere besides the old tests.
|
||||
if reflect.TypeFor[T]().Kind() == reflect.String {
|
||||
if str, ok := value.(fmt.Stringer); ok {
|
||||
return any(str.String()).(T), nil
|
||||
}
|
||||
}
|
||||
return def, fmt.Errorf("%w: got %T, want %T", setting.ErrTypeMismatch, value, def)
|
||||
}
|
||||
|
||||
// SelectControlURL returns the ControlURL to use based on a value in
|
||||
// the registry (LoginURL) and the one on disk (in the GUI's
|
||||
// prefs.conf). If both are empty, it returns a default value. (It
|
||||
// always return a non-empty value)
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/2798 for some background.
|
||||
func SelectControlURL(reg, disk string) string {
|
||||
const def = "https://controlplane.tailscale.com"
|
||||
|
||||
// Prior to Dec 2020's commit 739b02e6, the installer
|
||||
// wrote a LoginURL value of https://login.tailscale.com to the registry.
|
||||
const oldRegDef = "https://login.tailscale.com"
|
||||
|
||||
// If they have an explicit value in the registry, use it,
|
||||
// unless it's an old default value from an old installer.
|
||||
// Then we have to see which is better.
|
||||
if reg != "" {
|
||||
if reg != oldRegDef {
|
||||
// Something explicit in the registry that we didn't
|
||||
// set ourselves by the installer.
|
||||
return reg
|
||||
}
|
||||
if disk == "" {
|
||||
// Something in the registry is better than nothing on disk.
|
||||
return reg
|
||||
}
|
||||
if disk != def && disk != oldRegDef {
|
||||
// The value in the registry is the old
|
||||
// default (login.tailscale.com) but the value
|
||||
// on disk is neither our old nor new default
|
||||
// value, so it must be some custom thing that
|
||||
// the user cares about. Prefer the disk value.
|
||||
return disk
|
||||
}
|
||||
}
|
||||
if disk != "" {
|
||||
return disk
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func init() {
|
||||
policyclient.RegisterClientImpl(globalSyspolicy{})
|
||||
}
|
||||
|
||||
// globalSyspolicy implements [policyclient.Client] using the syspolicy global
|
||||
// functions and global registrations.
|
||||
//
|
||||
// TODO: de-global-ify. This implementation using the old global functions
|
||||
// is an intermediate stage while changing policyclient to be modular.
|
||||
type globalSyspolicy struct{}
|
||||
|
||||
func (globalSyspolicy) GetBoolean(key pkey.Key, defaultValue bool) (bool, error) {
|
||||
return getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetString(key pkey.Key, defaultValue string) (string, error) {
|
||||
return getString(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetStringArray(key pkey.Key, defaultValue []string) ([]string, error) {
|
||||
return getStringArray(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) SetDebugLoggingEnabled(enabled bool) {
|
||||
loggerx.SetDebugLoggingEnabled(enabled)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetUint64(key pkey.Key, defaultValue uint64) (uint64, error) {
|
||||
return getUint64(key, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
return getDuration(name, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetPreferenceOption(name pkey.Key, defaultValue ptype.PreferenceOption) (ptype.PreferenceOption, error) {
|
||||
return getPreferenceOption(name, defaultValue)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetVisibility(name pkey.Key) (ptype.Visibility, error) {
|
||||
return getVisibility(name)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
return hasAnyOf(keys...)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) RegisterChangeCallback(cb func(policyclient.PolicyChange)) (unregister func(), err error) {
|
||||
return registerChangeCallback(cb)
|
||||
}
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/user"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// On Windows, we should automatically register the Registry-based policy
|
||||
// store for the device. If we are running in a user's security context
|
||||
// (e.g., we're the GUI), we should also register the Registry policy store for
|
||||
// the user. In the future, we should register (and unregister) user policy
|
||||
// stores whenever a user connects to (or disconnects from) the local backend.
|
||||
// This ensures the backend is aware of the user's policy settings and can send
|
||||
// them to the GUI/CLI/Web clients on demand or whenever they change.
|
||||
//
|
||||
// Other platforms, such as macOS, iOS and Android, should register their
|
||||
// platform-specific policy stores via [RegisterStore]
|
||||
// (or [RegisterHandler] until they implement the [source.Store] interface).
|
||||
//
|
||||
// External code, such as the ipnlocal package, may choose to register
|
||||
// additional policy stores, such as config files and policies received from
|
||||
// the control plane.
|
||||
internal.Init.MustDefer(func() error {
|
||||
// Do not register or use default policy stores during tests.
|
||||
// Each test should set up its own necessary configurations.
|
||||
if testenv.InTest() {
|
||||
return nil
|
||||
}
|
||||
return configureSyspolicy(nil)
|
||||
})
|
||||
}
|
||||
|
||||
// configureSyspolicy configures syspolicy for use on Windows,
|
||||
// either in test or regular builds depending on whether tb has a non-nil value.
|
||||
func configureSyspolicy(tb testenv.TB) error {
|
||||
const localSystemSID = "S-1-5-18"
|
||||
// Always create and register a machine policy store that reads
|
||||
// policy settings from the HKEY_LOCAL_MACHINE registry hive.
|
||||
machineStore, err := source.NewMachinePlatformPolicyStore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the machine policy store: %v", err)
|
||||
}
|
||||
if tb == nil {
|
||||
_, err = rsop.RegisterStore("Platform", setting.DeviceScope, machineStore)
|
||||
} else {
|
||||
_, err = rsop.RegisterStoreForTest(tb, "Platform", setting.DeviceScope, machineStore)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Check whether the current process is running as Local System or not.
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Uid == localSystemSID {
|
||||
return nil
|
||||
}
|
||||
// If it's not a Local System's process (e.g., it's the GUI rather than the tailscaled service),
|
||||
// we should create and use a policy store for the current user that reads
|
||||
// policy settings from that user's registry hive (HKEY_CURRENT_USER).
|
||||
userStore, err := source.NewUserPlatformPolicyStore(0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the current user's policy store: %v", err)
|
||||
}
|
||||
if tb == nil {
|
||||
_, err = rsop.RegisterStore("Platform", setting.CurrentUserScope, userStore)
|
||||
} else {
|
||||
_, err = rsop.RegisterStoreForTest(tb, "Platform", setting.CurrentUserScope, userStore)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// And also set [setting.CurrentUserScope] as the [setting.DefaultScope], so [GetString],
|
||||
// [GetVisibility] and similar functions would be returning a merged result
|
||||
// of the machine's and user's policies.
|
||||
if !setting.SetDefaultScope(setting.CurrentUserScope) {
|
||||
return errors.New("current scope already set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package testenv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// stackBufPool holds buffers for [InSynctestBubble]'s runtime.Stack calls,
|
||||
// which would otherwise allocate on every call: the compiler cannot prove
|
||||
// that a stack-allocated buffer does not escape into runtime.Stack. Plain
|
||||
// byte arrays, unlike the zstd coders that motivated InSynctestBubble, are
|
||||
// safe to share across synctest bubble boundaries.
|
||||
var stackBufPool = sync.Pool{New: func() any { return new([128]byte) }}
|
||||
|
||||
// InSynctestBubble reports whether the current goroutine is running within a
|
||||
// testing/synctest bubble.
|
||||
//
|
||||
// As of Go 1.26 there is no public API to query bubble membership
|
||||
// (internal/synctest.IsInBubble is runtime-internal), so this instead
|
||||
// looks for the "synctest bubble N" annotation that the runtime renders
|
||||
// in the current goroutine's [runtime.Stack] header. If a future Go
|
||||
// release changes that annotation, this reports false; tests that depend
|
||||
// on it for correctness (such as tailscale.com/util/zstdframe's) should
|
||||
// exercise the misdetection failure mode so the breakage is loud.
|
||||
func InSynctestBubble() bool {
|
||||
// Bubbles only exist within tests, so skip the (relatively) expensive
|
||||
// stack header check in non-test binaries. This deliberately does not
|
||||
// use InTest: InSynctestBubble may be reached from package init
|
||||
// functions (before the testing package has registered its flags),
|
||||
// and InTest would permanently latch a false result there. Bubbles
|
||||
// cannot exist during init, so returning false then is correct.
|
||||
if flag.Lookup("test.v") == nil {
|
||||
return false
|
||||
}
|
||||
// The current goroutine's header looks like:
|
||||
// goroutine 9 [running, synctest bubble 1]:
|
||||
// A 128-byte buffer always fits the header, and runtime.Stack
|
||||
// truncates the rest.
|
||||
buf := stackBufPool.Get().(*[128]byte)
|
||||
defer stackBufPool.Put(buf)
|
||||
n := runtime.Stack(buf[:], false)
|
||||
return bytes.Contains(buf[:n], []byte(" synctest bubble "))
|
||||
}
|
||||
+18
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package testenv provides utility functions for tests. It does not depend on
|
||||
@@ -8,6 +8,7 @@ package testenv
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
)
|
||||
@@ -21,8 +22,23 @@ func InTest() bool {
|
||||
})
|
||||
}
|
||||
|
||||
// Verbose reports whether the test binary is running with verbose
|
||||
// output (the -test.v flag), like testing.Verbose but without
|
||||
// importing the testing package.
|
||||
func Verbose() bool {
|
||||
f := flag.Lookup("test.v")
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
// The flag's String value is "false" when off, and "true" or
|
||||
// "test2json" when on.
|
||||
return f.Value.String() != "false"
|
||||
}
|
||||
|
||||
// TB is testing.TB, to avoid importing "testing" in non-test code.
|
||||
type TB interface {
|
||||
ArtifactDir() string
|
||||
Attr(key, value string)
|
||||
Cleanup(func())
|
||||
Error(args ...any)
|
||||
Errorf(format string, args ...any)
|
||||
@@ -35,6 +51,7 @@ type TB interface {
|
||||
Log(args ...any)
|
||||
Logf(format string, args ...any)
|
||||
Name() string
|
||||
Output() io.Writer
|
||||
Setenv(key, value string)
|
||||
Chdir(dir string)
|
||||
Skip(args ...any)
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package truncate provides a utility function for safely truncating UTF-8
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This file contains user-facing metrics that are used by multiple packages.
|
||||
|
||||
Loaded 100 of 2146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user