This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+2 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package deephash
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package deephash
+45
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package dirwalk
+44 -5
View File
@@ -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
}
Binary file not shown.
Binary file not shown.
+10 -1
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+54 -15
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package eventbus
+217 -72
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package goroutines
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package osdiag
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package set
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package metrics
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package rsop
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package rsop
+1 -1
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package rsop
+3 -5
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
+1 -1
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package setting
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
+226
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package source
-264
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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