Update dependencies
This commit is contained in:
51
vendor/tailscale.com/util/clientmetric/clientmetric.go
generated
vendored
51
vendor/tailscale.com/util/clientmetric/clientmetric.go
generated
vendored
@@ -9,6 +9,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -223,6 +226,54 @@ func NewGaugeFunc(name string, f func() int64) *Metric {
|
||||
return m
|
||||
}
|
||||
|
||||
// AggregateCounter returns a sum of expvar counters registered with it.
|
||||
type AggregateCounter struct {
|
||||
mu sync.RWMutex
|
||||
counters set.Set[*expvar.Int]
|
||||
}
|
||||
|
||||
func (c *AggregateCounter) Value() int64 {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
var sum int64
|
||||
for cnt := range c.counters {
|
||||
sum += cnt.Value()
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// Register registers provided expvar counter.
|
||||
// When a counter is added to the counter, it will be reset
|
||||
// to start counting from 0. This is to avoid incrementing the
|
||||
// counter with an unexpectedly large value.
|
||||
func (c *AggregateCounter) Register(counter *expvar.Int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// No need to do anything if it's already registered.
|
||||
if c.counters.Contains(counter) {
|
||||
return
|
||||
}
|
||||
counter.Set(0)
|
||||
c.counters.Add(counter)
|
||||
}
|
||||
|
||||
// UnregisterAll unregisters all counters resulting in it
|
||||
// starting back down at zero. This is to ensure monotonicity
|
||||
// and respect the semantics of the counter.
|
||||
func (c *AggregateCounter) UnregisterAll() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.counters = set.Set[*expvar.Int]{}
|
||||
}
|
||||
|
||||
// NewAggregateCounter returns a new aggregate counter that returns
|
||||
// a sum of expvar variables registered with it.
|
||||
func NewAggregateCounter(name string) *AggregateCounter {
|
||||
c := &AggregateCounter{counters: set.Set[*expvar.Int]{}}
|
||||
NewCounterFunc(name, c.Value)
|
||||
return c
|
||||
}
|
||||
|
||||
// WritePrometheusExpositionFormat writes all client metrics to w in
|
||||
// the Prometheus text-based exposition format.
|
||||
//
|
||||
|
||||
21
vendor/tailscale.com/util/dnsname/dnsname.go
generated
vendored
21
vendor/tailscale.com/util/dnsname/dnsname.go
generated
vendored
@@ -5,9 +5,9 @@
|
||||
package dnsname
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/vizerror"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,7 +36,7 @@ func ToFQDN(s string) (FQDN, error) {
|
||||
totalLen += 1 // account for missing dot
|
||||
}
|
||||
if totalLen > maxNameLength {
|
||||
return "", fmt.Errorf("%q is too long to be a DNS name", s)
|
||||
return "", vizerror.Errorf("%q is too long to be a DNS name", s)
|
||||
}
|
||||
|
||||
st := 0
|
||||
@@ -54,7 +54,7 @@ func ToFQDN(s string) (FQDN, error) {
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/2024 for more.
|
||||
if len(label) == 0 || len(label) > maxLabelLength {
|
||||
return "", fmt.Errorf("%q is not a valid DNS label", label)
|
||||
return "", vizerror.Errorf("%q is not a valid DNS label", label)
|
||||
}
|
||||
st = i + 1
|
||||
}
|
||||
@@ -94,26 +94,27 @@ func (f FQDN) Contains(other FQDN) bool {
|
||||
return strings.HasSuffix(other.WithTrailingDot(), cmp)
|
||||
}
|
||||
|
||||
// ValidLabel reports whether label is a valid DNS label.
|
||||
// ValidLabel reports whether label is a valid DNS label. All errors are
|
||||
// [vizerror.Error].
|
||||
func ValidLabel(label string) error {
|
||||
if len(label) == 0 {
|
||||
return errors.New("empty DNS label")
|
||||
return vizerror.New("empty DNS label")
|
||||
}
|
||||
if len(label) > maxLabelLength {
|
||||
return fmt.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
|
||||
return vizerror.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
|
||||
}
|
||||
if !isalphanum(label[0]) {
|
||||
return fmt.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
|
||||
return vizerror.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
|
||||
}
|
||||
if !isalphanum(label[len(label)-1]) {
|
||||
return fmt.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
|
||||
return vizerror.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
|
||||
}
|
||||
if len(label) < 2 {
|
||||
return nil
|
||||
}
|
||||
for i := 1; i < len(label)-1; i++ {
|
||||
if !isdnschar(label[i]) {
|
||||
return fmt.Errorf("%q is not a valid DNS label: contains invalid character %q", label, label[i])
|
||||
return vizerror.Errorf("%q is not a valid DNS label: contains invalid character %q", label, label[i])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
2
vendor/tailscale.com/util/goroutines/goroutines.go
generated
vendored
2
vendor/tailscale.com/util/goroutines/goroutines.go
generated
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The goroutines package contains utilities for getting active goroutines.
|
||||
// The goroutines package contains utilities for tracking and getting active goroutines.
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
|
||||
66
vendor/tailscale.com/util/goroutines/tracker.go
generated
vendored
Normal file
66
vendor/tailscale.com/util/goroutines/tracker.go
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package goroutines
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// Tracker tracks a set of goroutines.
|
||||
type Tracker struct {
|
||||
started atomic.Int64 // counter
|
||||
running atomic.Int64 // gauge
|
||||
|
||||
mu sync.Mutex
|
||||
onDone set.HandleSet[func()]
|
||||
}
|
||||
|
||||
func (t *Tracker) Go(f func()) {
|
||||
t.started.Add(1)
|
||||
t.running.Add(1)
|
||||
go t.goAndDecr(f)
|
||||
}
|
||||
|
||||
func (t *Tracker) goAndDecr(f func()) {
|
||||
defer t.decr()
|
||||
f()
|
||||
}
|
||||
|
||||
func (t *Tracker) decr() {
|
||||
t.running.Add(-1)
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
for _, f := range t.onDone {
|
||||
go f()
|
||||
}
|
||||
}
|
||||
|
||||
// AddDoneCallback adds a callback to be called in a new goroutine
|
||||
// whenever a goroutine managed by t (excluding ones from this method)
|
||||
// finishes. It returns a function to remove the callback.
|
||||
func (t *Tracker) AddDoneCallback(f func()) (remove func()) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.onDone == nil {
|
||||
t.onDone = set.HandleSet[func()]{}
|
||||
}
|
||||
h := t.onDone.Add(f)
|
||||
return func() {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
delete(t.onDone, h)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) RunningGoroutines() int64 {
|
||||
return t.running.Load()
|
||||
}
|
||||
|
||||
func (t *Tracker) StartedGoroutines() int64 {
|
||||
return t.started.Load()
|
||||
}
|
||||
72
vendor/tailscale.com/util/lineiter/lineiter.go
generated
vendored
Normal file
72
vendor/tailscale.com/util/lineiter/lineiter.go
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package lineiter iterates over lines in things.
|
||||
package lineiter
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"iter"
|
||||
"os"
|
||||
|
||||
"tailscale.com/types/result"
|
||||
)
|
||||
|
||||
// File returns an iterator that reads lines from the named file.
|
||||
//
|
||||
// The returned substrings don't include the trailing newline.
|
||||
// Lines may be empty.
|
||||
func File(name string) iter.Seq[result.Of[[]byte]] {
|
||||
f, err := os.Open(name)
|
||||
return reader(f, f, err)
|
||||
}
|
||||
|
||||
// Bytes returns an iterator over the lines in bs.
|
||||
// The returned substrings don't include the trailing newline.
|
||||
// Lines may be empty.
|
||||
func Bytes(bs []byte) iter.Seq[[]byte] {
|
||||
return func(yield func([]byte) bool) {
|
||||
for len(bs) > 0 {
|
||||
i := bytes.IndexByte(bs, '\n')
|
||||
if i < 0 {
|
||||
yield(bs)
|
||||
return
|
||||
}
|
||||
if !yield(bs[:i]) {
|
||||
return
|
||||
}
|
||||
bs = bs[i+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reader returns an iterator over the lines in r.
|
||||
//
|
||||
// The returned substrings don't include the trailing newline.
|
||||
// Lines may be empty.
|
||||
func Reader(r io.Reader) iter.Seq[result.Of[[]byte]] {
|
||||
return reader(r, nil, nil)
|
||||
}
|
||||
|
||||
func reader(r io.Reader, c io.Closer, err error) iter.Seq[result.Of[[]byte]] {
|
||||
return func(yield func(result.Of[[]byte]) bool) {
|
||||
if err != nil {
|
||||
yield(result.Error[[]byte](err))
|
||||
return
|
||||
}
|
||||
if c != nil {
|
||||
defer c.Close()
|
||||
}
|
||||
bs := bufio.NewScanner(r)
|
||||
for bs.Scan() {
|
||||
if !yield(result.Value(bs.Bytes())) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err := bs.Err(); err != nil {
|
||||
yield(result.Error[[]byte](err))
|
||||
}
|
||||
}
|
||||
}
|
||||
37
vendor/tailscale.com/util/lineread/lineread.go
generated
vendored
37
vendor/tailscale.com/util/lineread/lineread.go
generated
vendored
@@ -1,37 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package lineread reads lines from files. It's not fancy, but it got repetitive.
|
||||
package lineread
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// File opens name and calls fn for each line. It returns an error if the Open failed
|
||||
// or once fn returns an error.
|
||||
func File(name string, fn func(line []byte) error) error {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return Reader(f, fn)
|
||||
}
|
||||
|
||||
// Reader calls fn for each line.
|
||||
// If fn returns an error, Reader stops reading and returns that error.
|
||||
// Reader may also return errors encountered reading and parsing from r.
|
||||
// To stop reading early, use a sentinel "stop" error value and ignore
|
||||
// it when returned from Reader.
|
||||
func Reader(r io.Reader, fn func(line []byte) error) error {
|
||||
bs := bufio.NewScanner(r)
|
||||
for bs.Scan() {
|
||||
if err := fn(bs.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return bs.Err()
|
||||
}
|
||||
6
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
6
vendor/tailscale.com/util/linuxfw/nftables.go
generated
vendored
@@ -8,6 +8,7 @@ package linuxfw
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
"github.com/google/nftables/xt"
|
||||
"github.com/josharian/native"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -235,8 +235,8 @@ func printMatchInfo(name string, info xt.InfoAny) string {
|
||||
break
|
||||
}
|
||||
|
||||
pkttype := int(native.Endian.Uint32(data[0:4]))
|
||||
invert := int(native.Endian.Uint32(data[4:8]))
|
||||
pkttype := int(binary.NativeEndian.Uint32(data[0:4]))
|
||||
invert := int(binary.NativeEndian.Uint32(data[4:8]))
|
||||
var invertPrefix string
|
||||
if invert != 0 {
|
||||
invertPrefix = "!"
|
||||
|
||||
4
vendor/tailscale.com/util/set/slice.go
generated
vendored
4
vendor/tailscale.com/util/set/slice.go
generated
vendored
@@ -67,7 +67,7 @@ func (ss *Slice[T]) Add(vs ...T) {
|
||||
|
||||
// AddSlice adds all elements in vs to the set.
|
||||
func (ss *Slice[T]) AddSlice(vs views.Slice[T]) {
|
||||
for i := range vs.Len() {
|
||||
ss.Add(vs.At(i))
|
||||
for _, v := range vs.All() {
|
||||
ss.Add(v)
|
||||
}
|
||||
}
|
||||
|
||||
51
vendor/tailscale.com/util/slicesx/slicesx.go
generated
vendored
51
vendor/tailscale.com/util/slicesx/slicesx.go
generated
vendored
@@ -95,6 +95,17 @@ func Filter[S ~[]T, T any](dst, src S, fn func(T) bool) S {
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendNonzero appends all non-zero elements of src to dst.
|
||||
func AppendNonzero[S ~[]T, T comparable](dst, src S) S {
|
||||
var zero T
|
||||
for _, v := range src {
|
||||
if v != zero {
|
||||
dst = append(dst, v)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMatching appends elements in ps to dst if f(x) is true.
|
||||
func AppendMatching[T any](dst, ps []T, f func(T) bool) []T {
|
||||
for _, p := range ps {
|
||||
@@ -148,3 +159,43 @@ func FirstEqual[T comparable](s []T, v T) bool {
|
||||
func LastEqual[T comparable](s []T, v T) bool {
|
||||
return len(s) > 0 && s[len(s)-1] == v
|
||||
}
|
||||
|
||||
// MapKeys returns the values of the map m.
|
||||
//
|
||||
// The keys will be in an indeterminate order.
|
||||
//
|
||||
// It's equivalent to golang.org/x/exp/maps.Keys, which
|
||||
// unfortunately has the package name "maps", shadowing
|
||||
// the std "maps" package. This version exists for clarity
|
||||
// when reading call sites.
|
||||
//
|
||||
// As opposed to slices.Collect(maps.Keys(m)), this allocates
|
||||
// the returned slice once to exactly the right size, rather than
|
||||
// appending larger backing arrays as it goes.
|
||||
func MapKeys[M ~map[K]V, K comparable, V any](m M) []K {
|
||||
r := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
r = append(r, k)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// MapValues returns the values of the map m.
|
||||
//
|
||||
// The values will be in an indeterminate order.
|
||||
//
|
||||
// It's equivalent to golang.org/x/exp/maps.Values, which
|
||||
// unfortunately has the package name "maps", shadowing
|
||||
// the std "maps" package. This version exists for clarity
|
||||
// when reading call sites.
|
||||
//
|
||||
// As opposed to slices.Collect(maps.Values(m)), this allocates
|
||||
// the returned slice once to exactly the right size, rather than
|
||||
// appending larger backing arrays as it goes.
|
||||
func MapValues[M ~map[K]V, K comparable, V any](m M) []V {
|
||||
r := make([]V, 0, len(m))
|
||||
for _, v := range m {
|
||||
r = append(r, v)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
122
vendor/tailscale.com/util/syspolicy/caching_handler.go
generated
vendored
122
vendor/tailscale.com/util/syspolicy/caching_handler.go
generated
vendored
@@ -1,122 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// CachingHandler is a handler that reads policies from an underlying handler the first time each key is requested
|
||||
// and permanently caches the result unless there is an error. If there is an ErrNoSuchKey error, that result is cached,
|
||||
// otherwise the actual error is returned and the next read for that key will retry using the handler.
|
||||
type CachingHandler struct {
|
||||
mu sync.Mutex
|
||||
strings map[string]string
|
||||
uint64s map[string]uint64
|
||||
bools map[string]bool
|
||||
strArrs map[string][]string
|
||||
notFound map[string]bool
|
||||
handler Handler
|
||||
}
|
||||
|
||||
// NewCachingHandler creates a CachingHandler given a handler.
|
||||
func NewCachingHandler(handler Handler) *CachingHandler {
|
||||
return &CachingHandler{
|
||||
handler: handler,
|
||||
strings: make(map[string]string),
|
||||
uint64s: make(map[string]uint64),
|
||||
bools: make(map[string]bool),
|
||||
strArrs: make(map[string][]string),
|
||||
notFound: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// ReadString reads the policy settings value string given the key.
|
||||
// ReadString first reads from the handler's cache before resorting to using the handler.
|
||||
func (ch *CachingHandler) ReadString(key string) (string, error) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
if val, ok := ch.strings[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
if notFound := ch.notFound[key]; notFound {
|
||||
return "", ErrNoSuchKey
|
||||
}
|
||||
val, err := ch.handler.ReadString(key)
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
ch.notFound[key] = true
|
||||
return "", err
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ch.strings[key] = val
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ReadUInt64 reads the policy settings uint64 value given the key.
|
||||
// ReadUInt64 first reads from the handler's cache before resorting to using the handler.
|
||||
func (ch *CachingHandler) ReadUInt64(key string) (uint64, error) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
if val, ok := ch.uint64s[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
if notFound := ch.notFound[key]; notFound {
|
||||
return 0, ErrNoSuchKey
|
||||
}
|
||||
val, err := ch.handler.ReadUInt64(key)
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
ch.notFound[key] = true
|
||||
return 0, err
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ch.uint64s[key] = val
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ReadBoolean reads the policy settings boolean value given the key.
|
||||
// ReadBoolean first reads from the handler's cache before resorting to using the handler.
|
||||
func (ch *CachingHandler) ReadBoolean(key string) (bool, error) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
if val, ok := ch.bools[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
if notFound := ch.notFound[key]; notFound {
|
||||
return false, ErrNoSuchKey
|
||||
}
|
||||
val, err := ch.handler.ReadBoolean(key)
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
ch.notFound[key] = true
|
||||
return false, err
|
||||
} else if err != nil {
|
||||
return false, err
|
||||
}
|
||||
ch.bools[key] = val
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// ReadBoolean reads the policy settings boolean value given the key.
|
||||
// ReadBoolean first reads from the handler's cache before resorting to using the handler.
|
||||
func (ch *CachingHandler) ReadStringArray(key string) ([]string, error) {
|
||||
ch.mu.Lock()
|
||||
defer ch.mu.Unlock()
|
||||
if val, ok := ch.strArrs[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
if notFound := ch.notFound[key]; notFound {
|
||||
return nil, ErrNoSuchKey
|
||||
}
|
||||
val, err := ch.handler.ReadStringArray(key)
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
ch.notFound[key] = true
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch.strArrs[key] = val
|
||||
return val, nil
|
||||
}
|
||||
134
vendor/tailscale.com/util/syspolicy/handler.go
generated
vendored
134
vendor/tailscale.com/util/syspolicy/handler.go
generated
vendored
@@ -4,16 +4,17 @@
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
var (
|
||||
handlerUsed atomic.Bool
|
||||
handler Handler = defaultHandler{}
|
||||
)
|
||||
// TODO(nickkhyl): delete this file once other repos are updated.
|
||||
|
||||
// Handler reads system policies from OS-specific storage.
|
||||
//
|
||||
// Deprecated: implementing a [source.Store] should be preferred.
|
||||
type Handler interface {
|
||||
// ReadString reads the policy setting's string value for the given key.
|
||||
// It should return ErrNoSuchKey if the key does not have a value set.
|
||||
@@ -29,55 +30,88 @@ type Handler interface {
|
||||
ReadStringArray(key string) ([]string, error)
|
||||
}
|
||||
|
||||
// ErrNoSuchKey is returned by a Handler when the specified key does not have a
|
||||
// value set.
|
||||
var ErrNoSuchKey = errors.New("no such key")
|
||||
|
||||
// defaultHandler is the catch all syspolicy type for anything that isn't windows or apple.
|
||||
type defaultHandler struct{}
|
||||
|
||||
func (defaultHandler) ReadString(_ string) (string, error) {
|
||||
return "", ErrNoSuchKey
|
||||
}
|
||||
|
||||
func (defaultHandler) ReadUInt64(_ string) (uint64, error) {
|
||||
return 0, ErrNoSuchKey
|
||||
}
|
||||
|
||||
func (defaultHandler) ReadBoolean(_ string) (bool, error) {
|
||||
return false, ErrNoSuchKey
|
||||
}
|
||||
|
||||
func (defaultHandler) ReadStringArray(_ string) ([]string, error) {
|
||||
return nil, ErrNoSuchKey
|
||||
}
|
||||
|
||||
// markHandlerInUse is called before handler methods are called.
|
||||
func markHandlerInUse() {
|
||||
handlerUsed.Store(true)
|
||||
}
|
||||
|
||||
// RegisterHandler initializes the policy handler and ensures registration will happen once.
|
||||
// RegisterHandler wraps and registers the specified handler as the device's
|
||||
// policy [source.Store] for the program's lifetime.
|
||||
//
|
||||
// Deprecated: using [RegisterStore] should be preferred.
|
||||
func RegisterHandler(h Handler) {
|
||||
// Technically this assignment is not concurrency safe, but in the
|
||||
// event that there was any risk of a data race, we will panic due to
|
||||
// the CompareAndSwap failing.
|
||||
handler = h
|
||||
if !handlerUsed.CompareAndSwap(false, true) {
|
||||
panic("handler was already used before registration")
|
||||
}
|
||||
rsop.RegisterStore("DeviceHandler", setting.DeviceScope, WrapHandler(h))
|
||||
}
|
||||
|
||||
// TB is a subset of testing.TB that we use to set up test helpers.
|
||||
// It's defined here to avoid pulling in the testing package.
|
||||
type TB interface {
|
||||
Helper()
|
||||
Cleanup(func())
|
||||
type TB = internal.TB
|
||||
|
||||
// SetHandlerForTest wraps and sets the specified handler as the device's policy
|
||||
// [source.Store] for the duration of tb.
|
||||
//
|
||||
// Deprecated: using [MustRegisterStoreForTest] should be preferred.
|
||||
func SetHandlerForTest(tb TB, h Handler) {
|
||||
RegisterWellKnownSettingsForTest(tb)
|
||||
MustRegisterStoreForTest(tb, "DeviceHandler-TestOnly", setting.DefaultScope(), WrapHandler(h))
|
||||
}
|
||||
|
||||
func SetHandlerForTest(tb TB, h Handler) {
|
||||
tb.Helper()
|
||||
oldHandler := handler
|
||||
handler = h
|
||||
tb.Cleanup(func() { handler = oldHandler })
|
||||
var _ source.Store = (*handlerStore)(nil)
|
||||
|
||||
// handlerStore is a [source.Store] that calls the underlying [Handler].
|
||||
//
|
||||
// TODO(nickkhyl): remove it when the corp and android repos are updated.
|
||||
type handlerStore struct {
|
||||
h Handler
|
||||
}
|
||||
|
||||
// WrapHandler returns a [source.Store] that wraps the specified [Handler].
|
||||
func WrapHandler(h Handler) source.Store {
|
||||
return handlerStore{h}
|
||||
}
|
||||
|
||||
// Lock implements [source.Lockable].
|
||||
func (s handlerStore) Lock() error {
|
||||
if lockable, ok := s.h.(source.Lockable); ok {
|
||||
return lockable.Lock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock implements [source.Lockable].
|
||||
func (s handlerStore) Unlock() {
|
||||
if lockable, ok := s.h.(source.Lockable); ok {
|
||||
lockable.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterChangeCallback implements [source.Changeable].
|
||||
func (s handlerStore) RegisterChangeCallback(callback func()) (unregister func(), err error) {
|
||||
if changeable, ok := s.h.(source.Changeable); ok {
|
||||
return changeable.RegisterChangeCallback(callback)
|
||||
}
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
// ReadString implements [source.Store].
|
||||
func (s handlerStore) ReadString(key setting.Key) (string, error) {
|
||||
return s.h.ReadString(string(key))
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [source.Store].
|
||||
func (s handlerStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
return s.h.ReadUInt64(string(key))
|
||||
}
|
||||
|
||||
// ReadBoolean implements [source.Store].
|
||||
func (s handlerStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
return s.h.ReadBoolean(string(key))
|
||||
}
|
||||
|
||||
// ReadStringArray implements [source.Store].
|
||||
func (s handlerStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
return s.h.ReadStringArray(string(key))
|
||||
}
|
||||
|
||||
// Done implements [source.Expirable].
|
||||
func (s handlerStore) Done() <-chan struct{} {
|
||||
if expirable, ok := s.h.(source.Expirable); ok {
|
||||
return expirable.Done()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
105
vendor/tailscale.com/util/syspolicy/handler_windows.go
generated
vendored
105
vendor/tailscale.com/util/syspolicy/handler_windows.go
generated
vendored
@@ -1,105 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
var (
|
||||
windowsErrors = clientmetric.NewCounter("windows_syspolicy_errors")
|
||||
windowsAny = clientmetric.NewGauge("windows_syspolicy_any")
|
||||
)
|
||||
|
||||
type windowsHandler struct{}
|
||||
|
||||
func init() {
|
||||
RegisterHandler(NewCachingHandler(windowsHandler{}))
|
||||
|
||||
keyList := []struct {
|
||||
isSet func(Key) bool
|
||||
keys []Key
|
||||
}{
|
||||
{
|
||||
isSet: func(k Key) bool {
|
||||
_, err := handler.ReadString(string(k))
|
||||
return err == nil
|
||||
},
|
||||
keys: stringKeys,
|
||||
},
|
||||
{
|
||||
isSet: func(k Key) bool {
|
||||
_, err := handler.ReadBoolean(string(k))
|
||||
return err == nil
|
||||
},
|
||||
keys: boolKeys,
|
||||
},
|
||||
{
|
||||
isSet: func(k Key) bool {
|
||||
_, err := handler.ReadUInt64(string(k))
|
||||
return err == nil
|
||||
},
|
||||
keys: uint64Keys,
|
||||
},
|
||||
}
|
||||
|
||||
var anySet bool
|
||||
for _, l := range keyList {
|
||||
for _, k := range l.keys {
|
||||
if !l.isSet(k) {
|
||||
continue
|
||||
}
|
||||
clientmetric.NewGauge(fmt.Sprintf("windows_syspolicy_%s", k)).Set(1)
|
||||
anySet = true
|
||||
}
|
||||
}
|
||||
if anySet {
|
||||
windowsAny.Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
func (windowsHandler) ReadString(key string) (string, error) {
|
||||
s, err := winutil.GetPolicyString(key)
|
||||
if errors.Is(err, winutil.ErrNoValue) {
|
||||
err = ErrNoSuchKey
|
||||
} else if err != nil {
|
||||
windowsErrors.Add(1)
|
||||
}
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (windowsHandler) ReadUInt64(key string) (uint64, error) {
|
||||
value, err := winutil.GetPolicyInteger(key)
|
||||
if errors.Is(err, winutil.ErrNoValue) {
|
||||
err = ErrNoSuchKey
|
||||
} else if err != nil {
|
||||
windowsErrors.Add(1)
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
|
||||
func (windowsHandler) ReadBoolean(key string) (bool, error) {
|
||||
value, err := winutil.GetPolicyInteger(key)
|
||||
if errors.Is(err, winutil.ErrNoValue) {
|
||||
err = ErrNoSuchKey
|
||||
} else if err != nil {
|
||||
windowsErrors.Add(1)
|
||||
}
|
||||
return value != 0, err
|
||||
}
|
||||
|
||||
func (windowsHandler) ReadStringArray(key string) ([]string, error) {
|
||||
value, err := winutil.GetPolicyStringArray(key)
|
||||
if errors.Is(err, winutil.ErrNoValue) {
|
||||
err = ErrNoSuchKey
|
||||
} else if err != nil {
|
||||
windowsErrors.Add(1)
|
||||
}
|
||||
return value, err
|
||||
}
|
||||
7
vendor/tailscale.com/util/syspolicy/internal/internal.go
generated
vendored
7
vendor/tailscale.com/util/syspolicy/internal/internal.go
generated
vendored
@@ -13,6 +13,9 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// Init facilitates deferred invocation of initializers.
|
||||
var Init lazy.DeferredInit
|
||||
|
||||
// OSForTesting is the operating system override used for testing.
|
||||
// It follows the same naming convention as [version.OS].
|
||||
var OSForTesting lazy.SyncValue[string]
|
||||
@@ -53,10 +56,10 @@ func EqualJSONForTest(tb TB, j1, j2 jsontext.Value) (s1, s2 string, equal bool)
|
||||
return "", "", true
|
||||
}
|
||||
// Otherwise, format the values for display and return false.
|
||||
if err := j1.Indent("", "\t"); err != nil {
|
||||
if err := j1.Indent(); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
if err := j2.Indent("", "\t"); err != nil {
|
||||
if err := j2.Indent(); err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return j1.String(), j2.String(), false
|
||||
|
||||
319
vendor/tailscale.com/util/syspolicy/internal/metrics/metrics.go
generated
vendored
Normal file
319
vendor/tailscale.com/util/syspolicy/internal/metrics/metrics.go
generated
vendored
Normal file
@@ -0,0 +1,319 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package metrics provides logging and reporting for policy settings and scopes.
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
var lazyReportMetrics lazy.SyncValue[bool] // used as a test hook
|
||||
|
||||
// ShouldReport reports whether metrics should be reported on the current environment.
|
||||
func ShouldReport() bool {
|
||||
return lazyReportMetrics.Get(func() bool {
|
||||
// macOS, iOS and tvOS create their own metrics,
|
||||
// and we don't have syspolicy on any other platforms.
|
||||
return setting.PlatformList{"android", "windows"}.HasCurrent()
|
||||
})
|
||||
}
|
||||
|
||||
// Reset metrics for the specified policy origin.
|
||||
func Reset(origin *setting.Origin) {
|
||||
scopeMetrics(origin).Reset()
|
||||
}
|
||||
|
||||
// ReportConfigured updates metrics and logs that the specified setting is
|
||||
// configured with the given value in the origin.
|
||||
func ReportConfigured(origin *setting.Origin, setting *setting.Definition, value any) {
|
||||
settingMetricsFor(setting).ReportValue(origin, value)
|
||||
}
|
||||
|
||||
// ReportError updates metrics and logs that the specified setting has an error
|
||||
// in the origin.
|
||||
func ReportError(origin *setting.Origin, setting *setting.Definition, err error) {
|
||||
settingMetricsFor(setting).ReportError(origin, err)
|
||||
}
|
||||
|
||||
// ReportNotConfigured updates metrics and logs that the specified setting is
|
||||
// not configured in the origin.
|
||||
func ReportNotConfigured(origin *setting.Origin, setting *setting.Definition) {
|
||||
settingMetricsFor(setting).Reset(origin)
|
||||
}
|
||||
|
||||
// metric is an interface implemented by [clientmetric.Metric] and [funcMetric].
|
||||
type metric interface {
|
||||
Add(v int64)
|
||||
Set(v int64)
|
||||
}
|
||||
|
||||
// policyScopeMetrics are metrics that apply to an entire policy scope rather
|
||||
// than a specific policy setting.
|
||||
type policyScopeMetrics struct {
|
||||
hasAny metric
|
||||
numErrored metric
|
||||
}
|
||||
|
||||
func newScopeMetrics(scope setting.Scope) *policyScopeMetrics {
|
||||
prefix := metricScopeName(scope)
|
||||
// {os}_syspolicy_{scope_unless_device}_any
|
||||
// Example: windows_syspolicy_any or windows_syspolicy_user_any.
|
||||
hasAny := newMetric([]string{prefix, "any"}, clientmetric.TypeGauge)
|
||||
// {os}_syspolicy_{scope_unless_device}_errors
|
||||
// Example: windows_syspolicy_errors or windows_syspolicy_user_errors.
|
||||
//
|
||||
// TODO(nickkhyl): maybe make the `{os}_syspolicy_errors` metric a gauge rather than a counter?
|
||||
// It was a counter prior to https://github.com/tailscale/tailscale/issues/12687, so I kept it as such.
|
||||
// But I think a gauge makes more sense: syspolicy errors indicate a mismatch between the expected
|
||||
// policy value type or format and the actual value read from the underlying store (like the Windows Registry).
|
||||
// We'll encounter the same error every time we re-read the policy setting from the backing store
|
||||
// until the policy value is corrected by the user, or until we fix the bug in the code or ADMX.
|
||||
// There's probably no reason to count and accumulate them over time.
|
||||
//
|
||||
// Brief discussion: https://github.com/tailscale/tailscale/pull/13113#discussion_r1723475136
|
||||
numErrored := newMetric([]string{prefix, "errors"}, clientmetric.TypeCounter)
|
||||
return &policyScopeMetrics{hasAny, numErrored}
|
||||
}
|
||||
|
||||
// ReportHasSettings is called when there's any configured policy setting in the scope.
|
||||
func (m *policyScopeMetrics) ReportHasSettings() {
|
||||
if m != nil {
|
||||
m.hasAny.Set(1)
|
||||
}
|
||||
}
|
||||
|
||||
// ReportError is called when there's any errored policy setting in the scope.
|
||||
func (m *policyScopeMetrics) ReportError() {
|
||||
if m != nil {
|
||||
m.numErrored.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset is called to reset the policy scope metrics, such as when the policy scope
|
||||
// is about to be reloaded.
|
||||
func (m *policyScopeMetrics) Reset() {
|
||||
if m != nil {
|
||||
m.hasAny.Set(0)
|
||||
// numErrored is a counter and cannot be (re-)set.
|
||||
}
|
||||
}
|
||||
|
||||
// settingMetrics are metrics for a single policy setting in one or more scopes.
|
||||
type settingMetrics struct {
|
||||
definition *setting.Definition
|
||||
isSet []metric // by scope
|
||||
hasErrors []metric // by scope
|
||||
}
|
||||
|
||||
// ReportValue is called when the policy setting is found to be configured in the specified source.
|
||||
func (m *settingMetrics) ReportValue(origin *setting.Origin, v any) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if scope := origin.Scope().Kind(); scope >= 0 && int(scope) < len(m.isSet) {
|
||||
m.isSet[scope].Set(1)
|
||||
m.hasErrors[scope].Set(0)
|
||||
}
|
||||
scopeMetrics(origin).ReportHasSettings()
|
||||
loggerx.Verbosef("%v(%q) = %v", origin, m.definition.Key(), v)
|
||||
}
|
||||
|
||||
// ReportError is called when there's an error with the policy setting in the specified source.
|
||||
func (m *settingMetrics) ReportError(origin *setting.Origin, err error) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if scope := origin.Scope().Kind(); int(scope) < len(m.hasErrors) {
|
||||
m.isSet[scope].Set(0)
|
||||
m.hasErrors[scope].Set(1)
|
||||
}
|
||||
scopeMetrics(origin).ReportError()
|
||||
loggerx.Errorf("%v(%q): %v", origin, m.definition.Key(), err)
|
||||
}
|
||||
|
||||
// Reset is called to reset the policy setting's metrics, such as when
|
||||
// the policy setting does not exist or the source containing the policy
|
||||
// is about to be reloaded.
|
||||
func (m *settingMetrics) Reset(origin *setting.Origin) {
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
if scope := origin.Scope().Kind(); scope >= 0 && int(scope) < len(m.isSet) {
|
||||
m.isSet[scope].Set(0)
|
||||
m.hasErrors[scope].Set(0)
|
||||
}
|
||||
}
|
||||
|
||||
// metricFn is a function that adds or sets a metric value.
|
||||
type metricFn func(name string, typ clientmetric.Type, v int64)
|
||||
|
||||
// funcMetric implements [metric] by calling the specified add and set functions.
|
||||
// Used for testing, and with nil functions on platforms that do not support
|
||||
// syspolicy, and on platforms that report policy metrics from the GUI.
|
||||
type funcMetric struct {
|
||||
name string
|
||||
typ clientmetric.Type
|
||||
add, set metricFn
|
||||
}
|
||||
|
||||
func (m funcMetric) Add(v int64) {
|
||||
if m.add != nil {
|
||||
m.add(m.name, m.typ, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (m funcMetric) Set(v int64) {
|
||||
if m.set != nil {
|
||||
m.set(m.name, m.typ, v)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
lazyDeviceMetrics lazy.SyncValue[*policyScopeMetrics]
|
||||
lazyProfileMetrics lazy.SyncValue[*policyScopeMetrics]
|
||||
lazyUserMetrics lazy.SyncValue[*policyScopeMetrics]
|
||||
)
|
||||
|
||||
func scopeMetrics(origin *setting.Origin) *policyScopeMetrics {
|
||||
switch origin.Scope().Kind() {
|
||||
case setting.DeviceSetting:
|
||||
return lazyDeviceMetrics.Get(func() *policyScopeMetrics {
|
||||
return newScopeMetrics(setting.DeviceSetting)
|
||||
})
|
||||
case setting.ProfileSetting:
|
||||
return lazyProfileMetrics.Get(func() *policyScopeMetrics {
|
||||
return newScopeMetrics(setting.ProfileSetting)
|
||||
})
|
||||
case setting.UserSetting:
|
||||
return lazyUserMetrics.Get(func() *policyScopeMetrics {
|
||||
return newScopeMetrics(setting.UserSetting)
|
||||
})
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
settingMetricsMu sync.RWMutex
|
||||
settingMetricsMap map[setting.Key]*settingMetrics
|
||||
)
|
||||
|
||||
func settingMetricsFor(setting *setting.Definition) *settingMetrics {
|
||||
settingMetricsMu.RLock()
|
||||
metrics, ok := settingMetricsMap[setting.Key()]
|
||||
settingMetricsMu.RUnlock()
|
||||
if ok {
|
||||
return metrics
|
||||
}
|
||||
return settingMetricsForSlow(setting)
|
||||
}
|
||||
|
||||
func settingMetricsForSlow(d *setting.Definition) *settingMetrics {
|
||||
settingMetricsMu.Lock()
|
||||
defer settingMetricsMu.Unlock()
|
||||
if metrics, ok := settingMetricsMap[d.Key()]; ok {
|
||||
return metrics
|
||||
}
|
||||
|
||||
// The loop below initializes metrics for each scope where a policy setting defined in 'd'
|
||||
// can be configured. The [setting.Definition.Scope] returns the narrowest scope at which the policy
|
||||
// setting may be configured, and more specific scopes always have higher numeric values.
|
||||
// In other words, [setting.UserSetting] > [setting.ProfileScope] > [setting.DeviceScope].
|
||||
// It's impossible for a policy setting to be configured in a scope with a higher numeric value than
|
||||
// the [setting.Definition.Scope] returns. Therefore, a policy setting can be configured in at
|
||||
// most d.Scope()+1 different scopes, and having d.Scope()+1 metrics for the corresponding scopes
|
||||
// is always sufficient for [settingMetrics]; it won't access elements past the end of the slice
|
||||
// or need to reallocate with a longer slice if one of those arrives.
|
||||
isSet := make([]metric, d.Scope()+1)
|
||||
hasErrors := make([]metric, d.Scope()+1)
|
||||
for i := range isSet {
|
||||
scope := setting.Scope(i)
|
||||
// {os}_syspolicy_{key}_{scope_unless_device}
|
||||
// Example: windows_syspolicy_AdminConsole or windows_syspolicy_AdminConsole_user.
|
||||
isSet[i] = newSettingMetric(d.Key(), scope, "", clientmetric.TypeGauge)
|
||||
// {os}_syspolicy_{key}_{scope_unless_device}_error
|
||||
// Example: windows_syspolicy_AdminConsole_error or windows_syspolicy_TestSetting01_user_error.
|
||||
hasErrors[i] = newSettingMetric(d.Key(), scope, "error", clientmetric.TypeGauge)
|
||||
}
|
||||
metrics := &settingMetrics{d, isSet, hasErrors}
|
||||
mak.Set(&settingMetricsMap, d.Key(), metrics)
|
||||
return metrics
|
||||
}
|
||||
|
||||
// hooks for testing
|
||||
var addMetricTestHook, setMetricTestHook syncs.AtomicValue[metricFn]
|
||||
|
||||
// SetHooksForTest sets the specified addMetric and setMetric functions
|
||||
// as the metric functions for the duration of tb and all its subtests.
|
||||
func SetHooksForTest(tb internal.TB, addMetric, setMetric metricFn) {
|
||||
oldAddMetric := addMetricTestHook.Swap(addMetric)
|
||||
oldSetMetric := setMetricTestHook.Swap(setMetric)
|
||||
tb.Cleanup(func() {
|
||||
addMetricTestHook.Store(oldAddMetric)
|
||||
setMetricTestHook.Store(oldSetMetric)
|
||||
})
|
||||
|
||||
settingMetricsMu.Lock()
|
||||
oldSettingMetricsMap := xmaps.Clone(settingMetricsMap)
|
||||
clear(settingMetricsMap)
|
||||
settingMetricsMu.Unlock()
|
||||
tb.Cleanup(func() {
|
||||
settingMetricsMu.Lock()
|
||||
settingMetricsMap = oldSettingMetricsMap
|
||||
settingMetricsMu.Unlock()
|
||||
})
|
||||
|
||||
// (re-)set the scope metrics to use the test hooks for the duration of tb.
|
||||
lazyDeviceMetrics.SetForTest(tb, newScopeMetrics(setting.DeviceSetting), nil)
|
||||
lazyProfileMetrics.SetForTest(tb, newScopeMetrics(setting.ProfileSetting), nil)
|
||||
lazyUserMetrics.SetForTest(tb, newScopeMetrics(setting.UserSetting), nil)
|
||||
}
|
||||
|
||||
func newSettingMetric(key setting.Key, scope setting.Scope, suffix string, typ clientmetric.Type) metric {
|
||||
name := strings.ReplaceAll(string(key), string(setting.KeyPathSeparator), "_")
|
||||
name = strings.ReplaceAll(name, ".", "_") // dots are not allowed in metric names
|
||||
return newMetric([]string{name, metricScopeName(scope), suffix}, typ)
|
||||
}
|
||||
|
||||
func newMetric(nameParts []string, typ clientmetric.Type) metric {
|
||||
name := strings.Join(slicesx.AppendNonzero([]string{internal.OS(), "syspolicy"}, nameParts), "_")
|
||||
switch {
|
||||
case !ShouldReport():
|
||||
return &funcMetric{name: name, typ: typ}
|
||||
case testenv.InTest():
|
||||
return &funcMetric{name, typ, addMetricTestHook.Load(), setMetricTestHook.Load()}
|
||||
case typ == clientmetric.TypeCounter:
|
||||
return clientmetric.NewCounter(name)
|
||||
case typ == clientmetric.TypeGauge:
|
||||
return clientmetric.NewGauge(name)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func metricScopeName(scope setting.Scope) string {
|
||||
switch scope {
|
||||
case setting.DeviceSetting:
|
||||
return ""
|
||||
case setting.ProfileSetting:
|
||||
return "profile"
|
||||
case setting.UserSetting:
|
||||
return "user"
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
88
vendor/tailscale.com/util/syspolicy/internal/metrics/test_handler.go
generated
vendored
Normal file
88
vendor/tailscale.com/util/syspolicy/internal/metrics/test_handler.go
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
)
|
||||
|
||||
// TestState represents a metric name and its expected value.
|
||||
type TestState struct {
|
||||
Name string // `$os` in the name will be replaced by the actual operating system name.
|
||||
Value int64
|
||||
}
|
||||
|
||||
// TestHandler facilitates testing of the code that uses metrics.
|
||||
type TestHandler struct {
|
||||
t internal.TB
|
||||
|
||||
m map[string]int64
|
||||
}
|
||||
|
||||
// NewTestHandler returns a new TestHandler.
|
||||
func NewTestHandler(t internal.TB) *TestHandler {
|
||||
return &TestHandler{t, make(map[string]int64)}
|
||||
}
|
||||
|
||||
// AddMetric increments the metric with the specified name and type by delta d.
|
||||
func (h *TestHandler) AddMetric(name string, typ clientmetric.Type, d int64) {
|
||||
h.t.Helper()
|
||||
if typ == clientmetric.TypeCounter && d < 0 {
|
||||
h.t.Fatalf("an attempt was made to decrement a counter metric %q", name)
|
||||
}
|
||||
if v, ok := h.m[name]; ok || d != 0 {
|
||||
h.m[name] = v + d
|
||||
}
|
||||
}
|
||||
|
||||
// SetMetric sets the metric with the specified name and type to the value v.
|
||||
func (h *TestHandler) SetMetric(name string, typ clientmetric.Type, v int64) {
|
||||
h.t.Helper()
|
||||
if typ == clientmetric.TypeCounter {
|
||||
h.t.Fatalf("an attempt was made to set a counter metric %q", name)
|
||||
}
|
||||
if _, ok := h.m[name]; ok || v != 0 {
|
||||
h.m[name] = v
|
||||
}
|
||||
}
|
||||
|
||||
// MustEqual fails the test if the actual metric state differs from the specified state.
|
||||
func (h *TestHandler) MustEqual(metrics ...TestState) {
|
||||
h.t.Helper()
|
||||
h.MustContain(metrics...)
|
||||
h.mustNoExtra(metrics...)
|
||||
}
|
||||
|
||||
// MustContain fails the test if the specified metrics are not set or have
|
||||
// different values than specified. It permits other metrics to be set in
|
||||
// addition to the ones being tested.
|
||||
func (h *TestHandler) MustContain(metrics ...TestState) {
|
||||
h.t.Helper()
|
||||
for _, m := range metrics {
|
||||
name := strings.ReplaceAll(m.Name, "$os", internal.OS())
|
||||
v, ok := h.m[name]
|
||||
if !ok {
|
||||
h.t.Errorf("%q: got (none), want %v", name, m.Value)
|
||||
} else if v != m.Value {
|
||||
h.t.Fatalf("%q: got %v, want %v", name, v, m.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TestHandler) mustNoExtra(metrics ...TestState) {
|
||||
h.t.Helper()
|
||||
s := make(set.Set[string])
|
||||
for i := range metrics {
|
||||
s.Add(strings.ReplaceAll(metrics[i].Name, "$os", internal.OS()))
|
||||
}
|
||||
for n, v := range h.m {
|
||||
if !s.Contains(n) {
|
||||
h.t.Errorf("%q: got %v, want (none)", n, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
138
vendor/tailscale.com/util/syspolicy/policy_keys.go
generated
vendored
138
vendor/tailscale.com/util/syspolicy/policy_keys.go
generated
vendored
@@ -3,15 +3,51 @@
|
||||
|
||||
package syspolicy
|
||||
|
||||
import "tailscale.com/util/syspolicy/setting"
|
||||
import (
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/testenv"
|
||||
)
|
||||
|
||||
// Key is a string that uniquely identifies a policy and must remain unchanged
|
||||
// once established and documented for a given policy setting. It may contain
|
||||
// alphanumeric characters and zero or more [KeyPathSeparator]s to group
|
||||
// individual policy settings into categories.
|
||||
type Key = setting.Key
|
||||
|
||||
// The const block below lists known policy keys.
|
||||
// When adding a key to this list, remember to add a corresponding
|
||||
// [setting.Definition] to [implicitDefinitions] below.
|
||||
// Otherwise, the [TestKnownKeysRegistered] test will fail as a reminder.
|
||||
|
||||
const (
|
||||
// Keys with a string value
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
Tailnet Key = "Tailnet" // default ""; if blank, no tailnet name is sent to the server.
|
||||
|
||||
// AlwaysOn is a boolean key that controls whether Tailscale
|
||||
// should always remain in a connected state, and the user should
|
||||
// not be able to disconnect at their discretion.
|
||||
//
|
||||
// Warning: This policy setting is experimental and may change or be removed in the future.
|
||||
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
|
||||
// See tailscale/corp#26247, tailscale/corp#26248 and tailscale/corp#26249 for more information.
|
||||
AlwaysOn Key = "AlwaysOn.Enabled"
|
||||
|
||||
// AlwaysOnOverrideWithReason is a boolean key that alters the behavior
|
||||
// of [AlwaysOn]. When true, the user is allowed to disconnect Tailscale
|
||||
// by providing a reason. The reason is logged and sent to the control
|
||||
// for auditing purposes. It has no effect when [AlwaysOn] is false.
|
||||
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
|
||||
|
||||
// ReconnectAfter is a string value formatted for use with time.ParseDuration()
|
||||
// that defines the duration after which the client should automatically reconnect
|
||||
// to the Tailscale network following a user-initiated disconnect.
|
||||
// An empty string or a zero duration disables automatic reconnection.
|
||||
ReconnectAfter Key = "ReconnectAfter"
|
||||
|
||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
||||
// Exit node ID takes precedence over exit node IP.
|
||||
// To find the node ID, go to /api.md#device.
|
||||
@@ -63,6 +99,9 @@ const (
|
||||
// SuggestedExitNodeVisibility controls the visibility of suggested exit nodes in the client GUI.
|
||||
// When this system policy is set to 'hide', an exit node suggestion won't be presented to the user as part of the exit nodes picker.
|
||||
SuggestedExitNodeVisibility Key = "SuggestedExitNode"
|
||||
// OnboardingFlowVisibility controls the visibility of the onboarding flow in the client GUI.
|
||||
// When this system policy is set to 'hide', the onboarding flow is never shown to the user.
|
||||
OnboardingFlowVisibility Key = "OnboardingFlow"
|
||||
|
||||
// Keys with a string value formatted for use with time.ParseDuration().
|
||||
KeyExpirationNoticeTime Key = "KeyExpirationNotice" // default 24 hours
|
||||
@@ -106,7 +145,104 @@ const (
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
MachineCertificateSubject Key = "MachineCertificateSubject"
|
||||
|
||||
// Hostname is the hostname of the device that is running Tailscale.
|
||||
// When this policy is set, it overrides the hostname that the client
|
||||
// would otherwise obtain from the OS, e.g. by calling os.Hostname().
|
||||
Hostname Key = "Hostname"
|
||||
|
||||
// Keys with a string array value.
|
||||
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
|
||||
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
|
||||
)
|
||||
|
||||
// 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(AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
|
||||
setting.NewDefinition(AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(AuthKey, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ControlURL, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(DeviceSerialNumber, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(EnableIncomingConnections, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableRunExitNode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableServerMode, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableTailscaleDNS, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(EnableTailscaleSubnets, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ExitNodeAllowLANAccess, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ExitNodeID, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(ExitNodeIP, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(FlushDNSOnSessionUnlock, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(Hostname, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(LogSCMInteractions, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||
|
||||
// User policy settings (can be configured on a user- or device-basis):
|
||||
setting.NewDefinition(AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(AutoUpdateVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(ExitNodeMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(KeyExpirationNoticeTime, setting.UserSetting, setting.DurationValue),
|
||||
setting.NewDefinition(ManagedByCaption, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(ManagedByOrganizationName, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(ManagedByURL, setting.UserSetting, setting.StringValue),
|
||||
setting.NewDefinition(NetworkDevicesVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(PreferencesMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(ResetToDefaultsVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(RunExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(SuggestedExitNodeVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
setting.NewDefinition(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
|
||||
})
|
||||
}
|
||||
|
||||
var implicitDefinitionMap lazy.SyncValue[setting.DefinitionMap]
|
||||
|
||||
// WellKnownSettingDefinition returns a well-known, implicit setting definition by its key,
|
||||
// or an [ErrNoSuchKey] if a policy setting with the specified key does not exist
|
||||
// among implicit policy definitions.
|
||||
func WellKnownSettingDefinition(k Key) (*setting.Definition, error) {
|
||||
m, err := implicitDefinitionMap.GetErr(func() (setting.DefinitionMap, error) {
|
||||
return setting.DefinitionMapOf(implicitDefinitions)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d, ok := m[k]; ok {
|
||||
return d, nil
|
||||
}
|
||||
return nil, ErrNoSuchKey
|
||||
}
|
||||
|
||||
// RegisterWellKnownSettingsForTest registers all implicit setting definitions
|
||||
// for the duration of the test.
|
||||
func RegisterWellKnownSettingsForTest(tb TB) {
|
||||
tb.Helper()
|
||||
err := setting.SetDefinitionsForTest(tb, implicitDefinitions...)
|
||||
if err != nil {
|
||||
tb.Fatalf("Failed to register well-known settings: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
38
vendor/tailscale.com/util/syspolicy/policy_keys_windows.go
generated
vendored
38
vendor/tailscale.com/util/syspolicy/policy_keys_windows.go
generated
vendored
@@ -1,38 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package syspolicy
|
||||
|
||||
var stringKeys = []Key{
|
||||
ControlURL,
|
||||
LogTarget,
|
||||
Tailnet,
|
||||
ExitNodeID,
|
||||
ExitNodeIP,
|
||||
EnableIncomingConnections,
|
||||
EnableServerMode,
|
||||
ExitNodeAllowLANAccess,
|
||||
EnableTailscaleDNS,
|
||||
EnableTailscaleSubnets,
|
||||
AdminConsoleVisibility,
|
||||
NetworkDevicesVisibility,
|
||||
TestMenuVisibility,
|
||||
UpdateMenuVisibility,
|
||||
RunExitNodeVisibility,
|
||||
PreferencesMenuVisibility,
|
||||
ExitNodeMenuVisibility,
|
||||
AutoUpdateVisibility,
|
||||
ResetToDefaultsVisibility,
|
||||
KeyExpirationNoticeTime,
|
||||
PostureChecking,
|
||||
ManagedByOrganizationName,
|
||||
ManagedByCaption,
|
||||
ManagedByURL,
|
||||
}
|
||||
|
||||
var boolKeys = []Key{
|
||||
LogSCMInteractions,
|
||||
FlushDNSOnSessionUnlock,
|
||||
}
|
||||
|
||||
var uint64Keys = []Key{}
|
||||
107
vendor/tailscale.com/util/syspolicy/rsop/change_callbacks.go
generated
vendored
Normal file
107
vendor/tailscale.com/util/syspolicy/rsop/change_callbacks.go
generated
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// Change represents a change from the Old to the New value of type T.
|
||||
type Change[T any] struct {
|
||||
New, Old T
|
||||
}
|
||||
|
||||
// PolicyChangeCallback is a function called whenever a policy changes.
|
||||
type PolicyChangeCallback func(*PolicyChange)
|
||||
|
||||
// PolicyChange describes a policy change.
|
||||
type PolicyChange struct {
|
||||
snapshots Change[*setting.Snapshot]
|
||||
}
|
||||
|
||||
// New returns the [setting.Snapshot] after the change.
|
||||
func (c PolicyChange) New() *setting.Snapshot {
|
||||
return c.snapshots.New
|
||||
}
|
||||
|
||||
// Old returns the [setting.Snapshot] before the change.
|
||||
func (c PolicyChange) Old() *setting.Snapshot {
|
||||
return c.snapshots.Old
|
||||
}
|
||||
|
||||
// HasChanged reports whether a policy setting with the specified [setting.Key], has changed.
|
||||
func (c PolicyChange) HasChanged(key setting.Key) bool {
|
||||
new, newErr := c.snapshots.New.GetErr(key)
|
||||
old, oldErr := c.snapshots.Old.GetErr(key)
|
||||
if newErr != nil && oldErr != nil {
|
||||
return false
|
||||
}
|
||||
if newErr != nil || oldErr != nil {
|
||||
return true
|
||||
}
|
||||
switch newVal := new.(type) {
|
||||
case bool, uint64, string, setting.Visibility, setting.PreferenceOption, time.Duration:
|
||||
return newVal != old
|
||||
case []string:
|
||||
oldVal, ok := old.([]string)
|
||||
return !ok || !slices.Equal(newVal, oldVal)
|
||||
default:
|
||||
loggerx.Errorf("[unexpected] %q has an unsupported value type: %T", key, newVal)
|
||||
return !reflect.DeepEqual(new, old)
|
||||
}
|
||||
}
|
||||
|
||||
// policyChangeCallbacks are the callbacks to invoke when the effective policy changes.
|
||||
// It is safe for concurrent use.
|
||||
type policyChangeCallbacks struct {
|
||||
mu sync.Mutex
|
||||
cbs set.HandleSet[PolicyChangeCallback]
|
||||
}
|
||||
|
||||
// Register adds the specified callback to be invoked whenever the policy changes.
|
||||
func (c *policyChangeCallbacks) Register(callback PolicyChangeCallback) (unregister func()) {
|
||||
c.mu.Lock()
|
||||
handle := c.cbs.Add(callback)
|
||||
c.mu.Unlock()
|
||||
return func() {
|
||||
c.mu.Lock()
|
||||
delete(c.cbs, handle)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke calls the registered callback functions with the specified policy change info.
|
||||
func (c *policyChangeCallbacks) Invoke(snapshots Change[*setting.Snapshot]) {
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
wg.Add(len(c.cbs))
|
||||
change := &PolicyChange{snapshots: snapshots}
|
||||
for _, cb := range c.cbs {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
cb(change)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Close awaits the completion of active callbacks and prevents any further invocations.
|
||||
func (c *policyChangeCallbacks) Close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.cbs != nil {
|
||||
clear(c.cbs)
|
||||
c.cbs = nil
|
||||
}
|
||||
}
|
||||
456
vendor/tailscale.com/util/syspolicy/rsop/resultant_policy.go
generated
vendored
Normal file
456
vendor/tailscale.com/util/syspolicy/rsop/resultant_policy.go
generated
vendored
Normal file
@@ -0,0 +1,456 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
// ErrPolicyClosed is returned by [Policy.Reload], [Policy.addSource],
|
||||
// [Policy.removeSource] and [Policy.replaceSource] if the policy has been closed.
|
||||
var ErrPolicyClosed = errors.New("effective policy closed")
|
||||
|
||||
// The minimum and maximum wait times after detecting a policy change
|
||||
// before reloading the policy. This only affects policy reloads triggered
|
||||
// by a change in the underlying [source.Store] and does not impact
|
||||
// synchronous, caller-initiated reloads, such as when [Policy.Reload] is called.
|
||||
//
|
||||
// Policy changes occurring within [policyReloadMinDelay] of each other
|
||||
// will be batched together, resulting in a single policy reload
|
||||
// no later than [policyReloadMaxDelay] after the first detected change.
|
||||
// In other words, the effective policy will be reloaded no more often than once
|
||||
// every 5 seconds, but at most 15 seconds after an underlying [source.Store]
|
||||
// has issued a policy change callback.
|
||||
//
|
||||
// See [Policy.watchReload].
|
||||
var (
|
||||
policyReloadMinDelay = 5 * time.Second
|
||||
policyReloadMaxDelay = 15 * time.Second
|
||||
)
|
||||
|
||||
// Policy provides access to the current effective [setting.Snapshot] for a given
|
||||
// scope and allows to reload it from the underlying [source.Store] list. It also allows to
|
||||
// subscribe and receive a callback whenever the effective [setting.Snapshot] is changed.
|
||||
//
|
||||
// It is safe for concurrent use.
|
||||
type Policy struct {
|
||||
scope setting.PolicyScope
|
||||
|
||||
reloadCh chan reloadRequest // 1-buffered; written to when a policy reload is required
|
||||
closeCh chan struct{} // closed to signal that the Policy is being closed
|
||||
doneCh chan struct{} // closed by [Policy.closeInternal]
|
||||
|
||||
// effective is the most recent version of the [setting.Snapshot]
|
||||
// containing policy settings merged from all applicable sources.
|
||||
effective atomic.Pointer[setting.Snapshot]
|
||||
|
||||
changeCallbacks policyChangeCallbacks
|
||||
|
||||
mu sync.Mutex
|
||||
watcherStarted bool // whether [Policy.watchReload] was started
|
||||
sources source.ReadableSources
|
||||
closing bool // whether [Policy.Close] was called (even if we're still closing)
|
||||
}
|
||||
|
||||
// newPolicy returns a new [Policy] for the specified [setting.PolicyScope]
|
||||
// that tracks changes and merges policy settings read from the specified sources.
|
||||
func newPolicy(scope setting.PolicyScope, sources ...*source.Source) (_ *Policy, err error) {
|
||||
readableSources := make(source.ReadableSources, 0, len(sources))
|
||||
defer func() {
|
||||
if err != nil {
|
||||
readableSources.Close()
|
||||
}
|
||||
}()
|
||||
for _, s := range sources {
|
||||
reader, err := s.Reader()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get a store reader: %w", err)
|
||||
}
|
||||
session, err := reader.OpenSession()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open a reading session: %w", err)
|
||||
}
|
||||
readableSources = append(readableSources, source.ReadableSource{Source: s, ReadingSession: session})
|
||||
}
|
||||
|
||||
// Sort policy sources by their precedence from lower to higher.
|
||||
// For example, {UserPolicy},{ProfilePolicy},{DevicePolicy}.
|
||||
readableSources.StableSort()
|
||||
|
||||
p := &Policy{
|
||||
scope: scope,
|
||||
sources: readableSources,
|
||||
reloadCh: make(chan reloadRequest, 1),
|
||||
closeCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
}
|
||||
if _, err := p.reloadNow(false); err != nil {
|
||||
p.Close()
|
||||
return nil, err
|
||||
}
|
||||
p.startWatchReloadIfNeeded()
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// IsValid reports whether p is in a valid state and has not been closed.
|
||||
//
|
||||
// Since p's state can be changed by other goroutines at any time, this should
|
||||
// only be used as an optimization.
|
||||
func (p *Policy) IsValid() bool {
|
||||
select {
|
||||
case <-p.closeCh:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Scope returns the [setting.PolicyScope] that this policy applies to.
|
||||
func (p *Policy) Scope() setting.PolicyScope {
|
||||
return p.scope
|
||||
}
|
||||
|
||||
// Get returns the effective [setting.Snapshot].
|
||||
func (p *Policy) Get() *setting.Snapshot {
|
||||
return p.effective.Load()
|
||||
}
|
||||
|
||||
// RegisterChangeCallback adds a function to be called whenever the effective
|
||||
// policy changes. The returned function can be used to unregister the callback.
|
||||
func (p *Policy) RegisterChangeCallback(callback PolicyChangeCallback) (unregister func()) {
|
||||
return p.changeCallbacks.Register(callback)
|
||||
}
|
||||
|
||||
// Reload synchronously re-reads policy settings from the underlying list of policy sources,
|
||||
// constructing a new merged [setting.Snapshot] even if the policy remains unchanged.
|
||||
// In most scenarios, there's no need to re-read the policy manually.
|
||||
// Instead, it is recommended to register a policy change callback, or to use
|
||||
// the most recent [setting.Snapshot] returned by the [Policy.Get] method.
|
||||
//
|
||||
// It must not be called with p.mu held.
|
||||
func (p *Policy) Reload() (*setting.Snapshot, error) {
|
||||
return p.reload(true)
|
||||
}
|
||||
|
||||
// reload is like Reload, but allows to specify whether to re-read policy settings
|
||||
// from unchanged policy sources.
|
||||
//
|
||||
// It must not be called with p.mu held.
|
||||
func (p *Policy) reload(force bool) (*setting.Snapshot, error) {
|
||||
if !p.startWatchReloadIfNeeded() {
|
||||
return p.Get(), nil
|
||||
}
|
||||
|
||||
respCh := make(chan reloadResponse, 1)
|
||||
select {
|
||||
case p.reloadCh <- reloadRequest{force: force, respCh: respCh}:
|
||||
// continue
|
||||
case <-p.closeCh:
|
||||
return nil, ErrPolicyClosed
|
||||
}
|
||||
select {
|
||||
case resp := <-respCh:
|
||||
return resp.policy, resp.err
|
||||
case <-p.closeCh:
|
||||
return nil, ErrPolicyClosed
|
||||
}
|
||||
}
|
||||
|
||||
// reloadAsync requests an asynchronous background policy reload.
|
||||
// The policy will be reloaded no later than in [policyReloadMaxDelay].
|
||||
//
|
||||
// It must not be called with p.mu held.
|
||||
func (p *Policy) reloadAsync() {
|
||||
if !p.startWatchReloadIfNeeded() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case p.reloadCh <- reloadRequest{}:
|
||||
// Sent.
|
||||
default:
|
||||
// A reload request is already en route.
|
||||
}
|
||||
}
|
||||
|
||||
// reloadNow loads and merges policies from all sources, updating the effective policy.
|
||||
// If the force parameter is true, it forcibly reloads policies
|
||||
// from the underlying policy store, even if no policy changes were detected.
|
||||
//
|
||||
// Except for the initial policy reload during the [Policy] creation,
|
||||
// this method should only be called from the [Policy.watchReload] goroutine.
|
||||
func (p *Policy) reloadNow(force bool) (*setting.Snapshot, error) {
|
||||
new, err := p.readAndMerge(force)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
old := p.effective.Swap(new)
|
||||
// A nil old value indicates the initial policy load rather than a policy change.
|
||||
// Additionally, we should not invoke the policy change callbacks unless the
|
||||
// policy items have actually changed.
|
||||
if old != nil && !old.EqualItems(new) {
|
||||
snapshots := Change[*setting.Snapshot]{New: new, Old: old}
|
||||
p.changeCallbacks.Invoke(snapshots)
|
||||
}
|
||||
return new, nil
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the [Policy] is closed.
|
||||
func (p *Policy) Done() <-chan struct{} {
|
||||
return p.doneCh
|
||||
}
|
||||
|
||||
// readAndMerge reads and merges policy settings from all applicable sources,
|
||||
// returning a [setting.Snapshot] with the merged result.
|
||||
// If the force parameter is true, it re-reads policy settings from each source
|
||||
// even if no policy change was observed, and returns an error if the read
|
||||
// operation fails.
|
||||
func (p *Policy) readAndMerge(force bool) (*setting.Snapshot, error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
// Start with an empty policy in the target scope.
|
||||
effective := setting.NewSnapshot(nil, setting.SummaryWith(p.scope))
|
||||
// Then merge policy settings from all sources.
|
||||
// Policy sources with the highest precedence (e.g., the device policy) are merged last,
|
||||
// overriding any conflicting policy settings with lower precedence.
|
||||
for _, s := range p.sources {
|
||||
var policy *setting.Snapshot
|
||||
if force {
|
||||
var err error
|
||||
if policy, err = s.ReadSettings(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
policy = s.GetSettings()
|
||||
}
|
||||
effective = setting.MergeSnapshots(effective, policy)
|
||||
}
|
||||
return effective, nil
|
||||
}
|
||||
|
||||
// addSource adds the specified source to the list of sources used by p,
|
||||
// and triggers a synchronous policy refresh. It returns an error
|
||||
// if the source is not a valid source for this effective policy,
|
||||
// or if the effective policy is being closed,
|
||||
// or if policy refresh fails with an error.
|
||||
func (p *Policy) addSource(source *source.Source) error {
|
||||
return p.applySourcesChange(source, nil)
|
||||
}
|
||||
|
||||
// removeSource removes the specified source from the list of sources used by p,
|
||||
// and triggers a synchronous policy refresh. It returns an error if the
|
||||
// effective policy is being closed, or if policy refresh fails with an error.
|
||||
func (p *Policy) removeSource(source *source.Source) error {
|
||||
return p.applySourcesChange(nil, source)
|
||||
}
|
||||
|
||||
// replaceSource replaces the old source with the new source atomically,
|
||||
// and triggers a synchronous policy refresh. It returns an error
|
||||
// if the source is not a valid source for this effective policy,
|
||||
// or if the effective policy is being closed,
|
||||
// or if policy refresh fails with an error.
|
||||
func (p *Policy) replaceSource(old, new *source.Source) error {
|
||||
return p.applySourcesChange(new, old)
|
||||
}
|
||||
|
||||
func (p *Policy) applySourcesChange(toAdd, toRemove *source.Source) error {
|
||||
if toAdd == toRemove {
|
||||
return nil
|
||||
}
|
||||
if toAdd != nil && !toAdd.Scope().Contains(p.scope) {
|
||||
return errors.New("scope mismatch")
|
||||
}
|
||||
|
||||
changed, err := func() (changed bool, err error) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if toAdd != nil && !p.sources.Contains(toAdd) {
|
||||
reader, err := toAdd.Reader()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get a store reader: %w", err)
|
||||
}
|
||||
session, err := reader.OpenSession()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to open a reading session: %w", err)
|
||||
}
|
||||
|
||||
addAt := p.sources.InsertionIndexOf(toAdd)
|
||||
toAdd := source.ReadableSource{
|
||||
Source: toAdd,
|
||||
ReadingSession: session,
|
||||
}
|
||||
p.sources = slices.Insert(p.sources, addAt, toAdd)
|
||||
go p.watchPolicyChanges(toAdd)
|
||||
changed = true
|
||||
}
|
||||
if toRemove != nil {
|
||||
if deleteAt := p.sources.IndexOf(toRemove); deleteAt != -1 {
|
||||
p.sources.DeleteAt(deleteAt)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed, nil
|
||||
}()
|
||||
if changed {
|
||||
_, err = p.reload(false)
|
||||
}
|
||||
return err // may be nil or non-nil
|
||||
}
|
||||
|
||||
func (p *Policy) watchPolicyChanges(s source.ReadableSource) {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-s.ReadingSession.PolicyChanged():
|
||||
if !ok {
|
||||
p.mu.Lock()
|
||||
abruptlyClosed := slices.Contains(p.sources, s)
|
||||
p.mu.Unlock()
|
||||
if abruptlyClosed {
|
||||
// The underlying [source.Source] was closed abruptly without
|
||||
// being properly removed or replaced by another policy source.
|
||||
// We can't keep this [Policy] up to date, so we should close it.
|
||||
p.Close()
|
||||
}
|
||||
return
|
||||
}
|
||||
// The PolicyChanged channel was signaled.
|
||||
// Request an asynchronous policy reload.
|
||||
p.reloadAsync()
|
||||
case <-p.closeCh:
|
||||
// The [Policy] is being closed.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startWatchReloadIfNeeded starts [Policy.watchReload] in a new goroutine
|
||||
// if the list of policy sources is not empty, it hasn't been started yet,
|
||||
// and the [Policy] is not being closed.
|
||||
// It reports whether [Policy.watchReload] has ever been started.
|
||||
//
|
||||
// It must not be called with p.mu held.
|
||||
func (p *Policy) startWatchReloadIfNeeded() bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if len(p.sources) != 0 && !p.watcherStarted && !p.closing {
|
||||
go p.watchReload()
|
||||
for i := range p.sources {
|
||||
go p.watchPolicyChanges(p.sources[i])
|
||||
}
|
||||
p.watcherStarted = true
|
||||
}
|
||||
return p.watcherStarted
|
||||
}
|
||||
|
||||
// reloadRequest describes a policy reload request.
|
||||
type reloadRequest struct {
|
||||
// force policy reload regardless of whether a policy change was detected.
|
||||
force bool
|
||||
// respCh is an optional channel. If non-nil, it makes the reload request
|
||||
// synchronous and receives the result.
|
||||
respCh chan<- reloadResponse
|
||||
}
|
||||
|
||||
// reloadResponse is a result of a synchronous policy reload.
|
||||
type reloadResponse struct {
|
||||
policy *setting.Snapshot
|
||||
err error
|
||||
}
|
||||
|
||||
// watchReload processes incoming synchronous and asynchronous policy reload requests.
|
||||
//
|
||||
// Synchronous requests (with a non-nil respCh) are served immediately.
|
||||
//
|
||||
// Asynchronous requests are debounced and throttled: they are executed at least
|
||||
// [policyReloadMinDelay] after the last request, but no later than [policyReloadMaxDelay]
|
||||
// after the first request in a batch.
|
||||
func (p *Policy) watchReload() {
|
||||
defer p.closeInternal()
|
||||
|
||||
force := false // whether a forced refresh was requested
|
||||
var delayCh, timeoutCh <-chan time.Time
|
||||
reload := func(respCh chan<- reloadResponse) {
|
||||
delayCh, timeoutCh = nil, nil
|
||||
policy, err := p.reloadNow(force)
|
||||
if err != nil {
|
||||
loggerx.Errorf("%v policy reload failed: %v\n", p.scope, err)
|
||||
}
|
||||
if respCh != nil {
|
||||
respCh <- reloadResponse{policy: policy, err: err}
|
||||
}
|
||||
force = false
|
||||
}
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case req := <-p.reloadCh:
|
||||
if req.force {
|
||||
force = true
|
||||
}
|
||||
if req.respCh != nil {
|
||||
reload(req.respCh)
|
||||
continue
|
||||
}
|
||||
if delayCh == nil {
|
||||
timeoutCh = time.After(policyReloadMinDelay)
|
||||
}
|
||||
delayCh = time.After(policyReloadMaxDelay)
|
||||
case <-delayCh:
|
||||
reload(nil)
|
||||
case <-timeoutCh:
|
||||
reload(nil)
|
||||
case <-p.closeCh:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Policy) closeInternal() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.sources.Close()
|
||||
p.changeCallbacks.Close()
|
||||
close(p.doneCh)
|
||||
deletePolicy(p)
|
||||
}
|
||||
|
||||
// Close initiates the closing of the policy.
|
||||
// The [Policy.Done] channel is closed to signal that the operation has been completed.
|
||||
func (p *Policy) Close() {
|
||||
p.mu.Lock()
|
||||
alreadyClosing := p.closing
|
||||
watcherStarted := p.watcherStarted
|
||||
p.closing = true
|
||||
p.mu.Unlock()
|
||||
|
||||
if alreadyClosing {
|
||||
return
|
||||
}
|
||||
|
||||
close(p.closeCh)
|
||||
if !watcherStarted {
|
||||
// Normally, closing p.closeCh signals [Policy.watchReload] to exit,
|
||||
// and [Policy.closeInternal] performs the actual closing when
|
||||
// [Policy.watchReload] returns. However, if the watcher was never
|
||||
// started, we need to call [Policy.closeInternal] manually.
|
||||
go p.closeInternal()
|
||||
}
|
||||
}
|
||||
|
||||
func setForTest[T any](tb internal.TB, target *T, newValue T) {
|
||||
oldValue := *target
|
||||
tb.Cleanup(func() { *target = oldValue })
|
||||
*target = newValue
|
||||
}
|
||||
174
vendor/tailscale.com/util/syspolicy/rsop/rsop.go
generated
vendored
Normal file
174
vendor/tailscale.com/util/syspolicy/rsop/rsop.go
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package rsop facilitates [source.Store] registration via [RegisterStore]
|
||||
// and provides access to the effective policy merged from all registered sources
|
||||
// via [PolicyFor].
|
||||
package rsop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
var (
|
||||
policyMu sync.Mutex // protects [policySources] and [effectivePolicies]
|
||||
policySources []*source.Source // all registered policy sources
|
||||
effectivePolicies []*Policy // all active (non-closed) effective policies returned by [PolicyFor]
|
||||
|
||||
// effectivePolicyLRU is an LRU cache of [Policy] by [setting.Scope].
|
||||
// Although there could be multiple [setting.PolicyScope] instances with the same [setting.Scope],
|
||||
// such as two user scopes for different users, there is only one [setting.DeviceScope], only one
|
||||
// [setting.CurrentProfileScope], and in most cases, only one active user scope.
|
||||
// Therefore, cache misses that require falling back to [effectivePolicies] are extremely rare.
|
||||
// It's a fixed-size array of atomic values and can be accessed without [policyMu] held.
|
||||
effectivePolicyLRU [setting.NumScopes]syncs.AtomicValue[*Policy]
|
||||
)
|
||||
|
||||
// PolicyFor returns the [Policy] for the specified scope,
|
||||
// creating it from the registered [source.Store]s if it doesn't already exist.
|
||||
func PolicyFor(scope setting.PolicyScope) (*Policy, error) {
|
||||
if err := internal.Init.Do(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
policy := effectivePolicyLRU[scope.Kind()].Load()
|
||||
if policy != nil && policy.Scope() == scope && policy.IsValid() {
|
||||
return policy, nil
|
||||
}
|
||||
return policyForSlow(scope)
|
||||
}
|
||||
|
||||
func policyForSlow(scope setting.PolicyScope) (policy *Policy, err error) {
|
||||
defer func() {
|
||||
// Always update the LRU cache on exit if we found (or created)
|
||||
// a policy for the specified scope.
|
||||
if policy != nil {
|
||||
effectivePolicyLRU[scope.Kind()].Store(policy)
|
||||
}
|
||||
}()
|
||||
|
||||
policyMu.Lock()
|
||||
defer policyMu.Unlock()
|
||||
if policy, ok := findPolicyByScopeLocked(scope); ok {
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// If there is no existing effective policy for the specified scope,
|
||||
// we need to create one using the policy sources registered for that scope.
|
||||
sources := slicesx.Filter(nil, policySources, func(source *source.Source) bool {
|
||||
return source.Scope().Contains(scope)
|
||||
})
|
||||
policy, err = newPolicy(scope, sources...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
effectivePolicies = append(effectivePolicies, policy)
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
// findPolicyByScopeLocked returns a policy with the specified scope and true if
|
||||
// one exists in the [effectivePolicies] list, otherwise it returns nil, false.
|
||||
// [policyMu] must be held.
|
||||
func findPolicyByScopeLocked(target setting.PolicyScope) (policy *Policy, ok bool) {
|
||||
for _, policy := range effectivePolicies {
|
||||
if policy.Scope() == target && policy.IsValid() {
|
||||
return policy, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// deletePolicy deletes the specified effective policy from [effectivePolicies]
|
||||
// and [effectivePolicyLRU].
|
||||
func deletePolicy(policy *Policy) {
|
||||
policyMu.Lock()
|
||||
defer policyMu.Unlock()
|
||||
if i := slices.Index(effectivePolicies, policy); i != -1 {
|
||||
effectivePolicies = slices.Delete(effectivePolicies, i, i+1)
|
||||
}
|
||||
effectivePolicyLRU[policy.Scope().Kind()].CompareAndSwap(policy, nil)
|
||||
}
|
||||
|
||||
// registerSource registers the specified [source.Source] to be used by the package.
|
||||
// It updates existing [Policy]s returned by [PolicyFor] to use this source if
|
||||
// they are within the source's [setting.PolicyScope].
|
||||
func registerSource(source *source.Source) error {
|
||||
policyMu.Lock()
|
||||
defer policyMu.Unlock()
|
||||
if slices.Contains(policySources, source) {
|
||||
// already registered
|
||||
return nil
|
||||
}
|
||||
policySources = append(policySources, source)
|
||||
return forEachEffectivePolicyLocked(func(policy *Policy) error {
|
||||
if !source.Scope().Contains(policy.Scope()) {
|
||||
// Policy settings in the specified source do not apply
|
||||
// to the scope of this effective policy.
|
||||
// For example, a user policy source is being registered
|
||||
// while the effective policy is for the device (or another user).
|
||||
return nil
|
||||
}
|
||||
return policy.addSource(source)
|
||||
})
|
||||
}
|
||||
|
||||
// replaceSource is like [unregisterSource](old) followed by [registerSource](new),
|
||||
// but performed atomically: the effective policy will contain settings
|
||||
// either from the old source or the new source, never both and never neither.
|
||||
func replaceSource(old, new *source.Source) error {
|
||||
policyMu.Lock()
|
||||
defer policyMu.Unlock()
|
||||
oldIndex := slices.Index(policySources, old)
|
||||
if oldIndex == -1 {
|
||||
return fmt.Errorf("the source is not registered: %v", old)
|
||||
}
|
||||
policySources[oldIndex] = new
|
||||
return forEachEffectivePolicyLocked(func(policy *Policy) error {
|
||||
if !old.Scope().Contains(policy.Scope()) || !new.Scope().Contains(policy.Scope()) {
|
||||
return nil
|
||||
}
|
||||
return policy.replaceSource(old, new)
|
||||
})
|
||||
}
|
||||
|
||||
// unregisterSource unregisters the specified [source.Source],
|
||||
// so that it won't be used by any new or existing [Policy].
|
||||
func unregisterSource(source *source.Source) error {
|
||||
policyMu.Lock()
|
||||
defer policyMu.Unlock()
|
||||
index := slices.Index(policySources, source)
|
||||
if index == -1 {
|
||||
return nil
|
||||
}
|
||||
policySources = slices.Delete(policySources, index, index+1)
|
||||
return forEachEffectivePolicyLocked(func(policy *Policy) error {
|
||||
if !source.Scope().Contains(policy.Scope()) {
|
||||
return nil
|
||||
}
|
||||
return policy.removeSource(source)
|
||||
})
|
||||
}
|
||||
|
||||
// forEachEffectivePolicyLocked calls fn for every non-closed [Policy] in [effectivePolicies].
|
||||
// It accumulates the returned errors and returns an error that wraps all errors returned by fn.
|
||||
// The [policyMu] mutex must be held while this function is executed.
|
||||
func forEachEffectivePolicyLocked(fn func(p *Policy) error) error {
|
||||
var errs []error
|
||||
for _, policy := range effectivePolicies {
|
||||
if policy.IsValid() {
|
||||
err := fn(policy)
|
||||
if err != nil && !errors.Is(err, ErrPolicyClosed) {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
98
vendor/tailscale.com/util/syspolicy/rsop/store_registration.go
generated
vendored
Normal file
98
vendor/tailscale.com/util/syspolicy/rsop/store_registration.go
generated
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package rsop
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
)
|
||||
|
||||
// ErrAlreadyConsumed is the error returned when [StoreRegistration.ReplaceStore]
|
||||
// or [StoreRegistration.Unregister] is called more than once.
|
||||
var ErrAlreadyConsumed = errors.New("the store registration is no longer valid")
|
||||
|
||||
// StoreRegistration is a [source.Store] registered for use in the specified scope.
|
||||
// It can be used to unregister the store, or replace it with another one.
|
||||
type StoreRegistration struct {
|
||||
source *source.Source
|
||||
m sync.Mutex // protects the [StoreRegistration.consumeSlow] path
|
||||
consumed atomic.Bool // can be read without holding m, but must be written with m held
|
||||
}
|
||||
|
||||
// RegisterStore registers a new policy [source.Store] with the specified name and [setting.PolicyScope].
|
||||
func RegisterStore(name string, scope setting.PolicyScope, store source.Store) (*StoreRegistration, error) {
|
||||
return newStoreRegistration(name, scope, store)
|
||||
}
|
||||
|
||||
// RegisterStoreForTest is like [RegisterStore], but unregisters the store when
|
||||
// tb and all its subtests complete.
|
||||
func RegisterStoreForTest(tb internal.TB, name string, scope setting.PolicyScope, store source.Store) (*StoreRegistration, error) {
|
||||
setForTest(tb, &policyReloadMinDelay, 10*time.Millisecond)
|
||||
setForTest(tb, &policyReloadMaxDelay, 500*time.Millisecond)
|
||||
|
||||
reg, err := RegisterStore(name, scope, store)
|
||||
if err == nil {
|
||||
tb.Cleanup(func() {
|
||||
if err := reg.Unregister(); err != nil && !errors.Is(err, ErrAlreadyConsumed) {
|
||||
tb.Fatalf("Unregister failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
return reg, err // may be nil or non-nil
|
||||
}
|
||||
|
||||
func newStoreRegistration(name string, scope setting.PolicyScope, store source.Store) (*StoreRegistration, error) {
|
||||
source := source.NewSource(name, scope, store)
|
||||
if err := registerSource(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &StoreRegistration{source: source}, nil
|
||||
}
|
||||
|
||||
// ReplaceStore replaces the registered store with the new one,
|
||||
// returning a new [StoreRegistration] or an error.
|
||||
func (r *StoreRegistration) ReplaceStore(new source.Store) (*StoreRegistration, error) {
|
||||
var res *StoreRegistration
|
||||
err := r.consume(func() error {
|
||||
newSource := source.NewSource(r.source.Name(), r.source.Scope(), new)
|
||||
if err := replaceSource(r.source, newSource); err != nil {
|
||||
return err
|
||||
}
|
||||
res = &StoreRegistration{source: newSource}
|
||||
return nil
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Unregister reverts the registration.
|
||||
func (r *StoreRegistration) Unregister() error {
|
||||
return r.consume(func() error { return unregisterSource(r.source) })
|
||||
}
|
||||
|
||||
// consume invokes fn, consuming r if no error is returned.
|
||||
// It returns [ErrAlreadyConsumed] on subsequent calls after the first successful call.
|
||||
func (r *StoreRegistration) consume(fn func() error) (err error) {
|
||||
if r.consumed.Load() {
|
||||
return ErrAlreadyConsumed
|
||||
}
|
||||
return r.consumeSlow(fn)
|
||||
}
|
||||
|
||||
func (r *StoreRegistration) consumeSlow(fn func() error) (err error) {
|
||||
r.m.Lock()
|
||||
defer r.m.Unlock()
|
||||
if r.consumed.Load() {
|
||||
return ErrAlreadyConsumed
|
||||
}
|
||||
if err = fn(); err == nil {
|
||||
r.consumed.Store(true)
|
||||
}
|
||||
return err // may be nil or non-nil
|
||||
}
|
||||
2
vendor/tailscale.com/util/syspolicy/setting/key.go
generated
vendored
2
vendor/tailscale.com/util/syspolicy/setting/key.go
generated
vendored
@@ -10,4 +10,4 @@ package setting
|
||||
type Key string
|
||||
|
||||
// KeyPathSeparator allows logical grouping of policy settings into categories.
|
||||
const KeyPathSeparator = "/"
|
||||
const KeyPathSeparator = '/'
|
||||
|
||||
21
vendor/tailscale.com/util/syspolicy/setting/origin.go
generated
vendored
21
vendor/tailscale.com/util/syspolicy/setting/origin.go
generated
vendored
@@ -50,22 +50,27 @@ func (s Origin) String() string {
|
||||
return s.Scope().String()
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (s Origin) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return jsonv2.MarshalEncode(out, &s.data, opts)
|
||||
var (
|
||||
_ jsonv2.MarshalerTo = (*Origin)(nil)
|
||||
_ jsonv2.UnmarshalerFrom = (*Origin)(nil)
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (s Origin) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(out, &s.data)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (s *Origin) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
return jsonv2.UnmarshalDecode(in, &s.data, opts)
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (s *Origin) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
return jsonv2.UnmarshalDecode(in, &s.data)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (s Origin) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(s) // uses MarshalJSONV2
|
||||
return jsonv2.Marshal(s) // uses MarshalJSONTo
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (s *Origin) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
|
||||
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
|
||||
}
|
||||
|
||||
3
vendor/tailscale.com/util/syspolicy/setting/policy_scope.go
generated
vendored
3
vendor/tailscale.com/util/syspolicy/setting/policy_scope.go
generated
vendored
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,6 +36,8 @@ type PolicyScope struct {
|
||||
// when querying policy settings.
|
||||
// It returns [DeviceScope], unless explicitly changed with [SetDefaultScope].
|
||||
func DefaultScope() PolicyScope {
|
||||
// Allow deferred package init functions to override the default scope.
|
||||
internal.Init.Do()
|
||||
return lazyDefaultScope.Get(func() PolicyScope { return DeviceScope })
|
||||
}
|
||||
|
||||
|
||||
133
vendor/tailscale.com/util/syspolicy/setting/raw_item.go
generated
vendored
133
vendor/tailscale.com/util/syspolicy/setting/raw_item.go
generated
vendored
@@ -5,7 +5,11 @@ package setting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
@@ -17,10 +21,15 @@ import (
|
||||
// or converted from strings, these setting types predate the typed policy
|
||||
// hierarchies, and must be supported at this layer.
|
||||
type RawItem struct {
|
||||
_ structs.Incomparable
|
||||
value any
|
||||
err *ErrorText
|
||||
origin *Origin // or nil
|
||||
_ structs.Incomparable
|
||||
data rawItemJSON
|
||||
}
|
||||
|
||||
// rawItemJSON holds JSON-marshallable data for [RawItem].
|
||||
type rawItemJSON struct {
|
||||
Value RawValue `json:",omitzero"`
|
||||
Error *ErrorText `json:",omitzero"` // or nil
|
||||
Origin *Origin `json:",omitzero"` // or nil
|
||||
}
|
||||
|
||||
// RawItemOf returns a [RawItem] with the specified value.
|
||||
@@ -30,20 +39,20 @@ func RawItemOf(value any) RawItem {
|
||||
|
||||
// RawItemWith returns a [RawItem] with the specified value, error and origin.
|
||||
func RawItemWith(value any, err *ErrorText, origin *Origin) RawItem {
|
||||
return RawItem{value: value, err: err, origin: origin}
|
||||
return RawItem{data: rawItemJSON{Value: RawValue{opt.ValueOf(value)}, Error: err, Origin: origin}}
|
||||
}
|
||||
|
||||
// Value returns the value of the policy setting, or nil if the policy setting
|
||||
// is not configured, or an error occurred while reading it.
|
||||
func (i RawItem) Value() any {
|
||||
return i.value
|
||||
return i.data.Value.Get()
|
||||
}
|
||||
|
||||
// Error returns the error that occurred when reading the policy setting,
|
||||
// or nil if no error occurred.
|
||||
func (i RawItem) Error() error {
|
||||
if i.err != nil {
|
||||
return i.err
|
||||
if i.data.Error != nil {
|
||||
return i.data.Error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -51,17 +60,113 @@ func (i RawItem) Error() error {
|
||||
// Origin returns an optional [Origin] indicating where the policy setting is
|
||||
// configured.
|
||||
func (i RawItem) Origin() *Origin {
|
||||
return i.origin
|
||||
return i.data.Origin
|
||||
}
|
||||
|
||||
// String implements [fmt.Stringer].
|
||||
func (i RawItem) String() string {
|
||||
var suffix string
|
||||
if i.origin != nil {
|
||||
suffix = fmt.Sprintf(" - {%v}", i.origin)
|
||||
if i.data.Origin != nil {
|
||||
suffix = fmt.Sprintf(" - {%v}", i.data.Origin)
|
||||
}
|
||||
if i.err != nil {
|
||||
return fmt.Sprintf("Error{%q}%s", i.err.Error(), suffix)
|
||||
if i.data.Error != nil {
|
||||
return fmt.Sprintf("Error{%q}%s", i.data.Error.Error(), suffix)
|
||||
}
|
||||
return fmt.Sprintf("%v%s", i.value, suffix)
|
||||
return fmt.Sprintf("%v%s", i.data.Value.Value, suffix)
|
||||
}
|
||||
|
||||
var (
|
||||
_ jsonv2.MarshalerTo = (*RawItem)(nil)
|
||||
_ jsonv2.UnmarshalerFrom = (*RawItem)(nil)
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (i RawItem) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(out, &i.data)
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (i *RawItem) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
return jsonv2.UnmarshalDecode(in, &i.data)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (i RawItem) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(i) // uses MarshalJSONTo
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (i *RawItem) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, i) // uses UnmarshalJSONFrom
|
||||
}
|
||||
|
||||
// RawValue represents a raw policy setting value read from a policy store.
|
||||
// It is JSON-marshallable and facilitates unmarshalling of JSON values
|
||||
// into corresponding policy setting types, with special handling for JSON numbers
|
||||
// (unmarshalled as float64) and JSON string arrays (unmarshalled as []string).
|
||||
// See also [RawValue.UnmarshalJSONFrom].
|
||||
type RawValue struct {
|
||||
opt.Value[any]
|
||||
}
|
||||
|
||||
// RawValueType is a constraint that permits raw setting value types.
|
||||
type RawValueType interface {
|
||||
bool | uint64 | string | []string
|
||||
}
|
||||
|
||||
// RawValueOf returns a new [RawValue] holding the specified value.
|
||||
func RawValueOf[T RawValueType](v T) RawValue {
|
||||
return RawValue{opt.ValueOf[any](v)}
|
||||
}
|
||||
|
||||
var (
|
||||
_ jsonv2.MarshalerTo = (*RawValue)(nil)
|
||||
_ jsonv2.UnmarshalerFrom = (*RawValue)(nil)
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (v RawValue) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(out, v.Value)
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom] by attempting to unmarshal
|
||||
// a JSON value as one of the supported policy setting value types (bool, string, uint64, or []string),
|
||||
// based on the JSON value type. It fails if the JSON value is an object, if it's a JSON number that
|
||||
// cannot be represented as a uint64, or if a JSON array contains anything other than strings.
|
||||
func (v *RawValue) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
var valPtr any
|
||||
switch k := in.PeekKind(); k {
|
||||
case 't', 'f':
|
||||
valPtr = new(bool)
|
||||
case '"':
|
||||
valPtr = new(string)
|
||||
case '0':
|
||||
valPtr = new(uint64) // unmarshal JSON numbers as uint64
|
||||
case '[', 'n':
|
||||
valPtr = new([]string) // unmarshal arrays as string slices
|
||||
case '{':
|
||||
return fmt.Errorf("unexpected token: %v", k)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
if err := jsonv2.UnmarshalDecode(in, valPtr); err != nil {
|
||||
v.Value.Clear()
|
||||
return err
|
||||
}
|
||||
value := reflect.ValueOf(valPtr).Elem().Interface()
|
||||
v.Value = opt.ValueOf(value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (v RawValue) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(v) // uses MarshalJSONTo
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (v *RawValue) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, v) // uses UnmarshalJSONFrom
|
||||
}
|
||||
|
||||
// RawValues is a map of keyed setting values that can be read from a JSON.
|
||||
type RawValues map[Key]RawValue
|
||||
|
||||
3
vendor/tailscale.com/util/syspolicy/setting/setting.go
generated
vendored
3
vendor/tailscale.com/util/syspolicy/setting/setting.go
generated
vendored
@@ -243,6 +243,9 @@ func registerLocked(d *Definition) {
|
||||
|
||||
func settingDefinitions() (DefinitionMap, error) {
|
||||
return definitions.GetErr(func() (DefinitionMap, error) {
|
||||
if err := internal.Init.Do(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
definitionsMu.Lock()
|
||||
defer definitionsMu.Unlock()
|
||||
definitionsUsed = true
|
||||
|
||||
50
vendor/tailscale.com/util/syspolicy/setting/snapshot.go
generated
vendored
50
vendor/tailscale.com/util/syspolicy/setting/snapshot.go
generated
vendored
@@ -4,11 +4,14 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"iter"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/util/deephash"
|
||||
)
|
||||
@@ -65,6 +68,9 @@ func (s *Snapshot) GetSetting(k Key) (setting RawItem, ok bool) {
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Snapshot) Equal(s2 *Snapshot) bool {
|
||||
if s == s2 {
|
||||
return true
|
||||
}
|
||||
if !s.EqualItems(s2) {
|
||||
return false
|
||||
}
|
||||
@@ -135,6 +141,50 @@ func (s *Snapshot) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// snapshotJSON holds JSON-marshallable data for [Snapshot].
|
||||
type snapshotJSON struct {
|
||||
Summary Summary `json:",omitzero"`
|
||||
Settings map[Key]RawItem `json:",omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
_ jsonv2.MarshalerTo = (*Snapshot)(nil)
|
||||
_ jsonv2.UnmarshalerFrom = (*Snapshot)(nil)
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (s *Snapshot) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
data := &snapshotJSON{}
|
||||
if s != nil {
|
||||
data.Summary = s.summary
|
||||
data.Settings = s.m
|
||||
}
|
||||
return jsonv2.MarshalEncode(out, data)
|
||||
}
|
||||
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (s *Snapshot) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
if s == nil {
|
||||
return errors.New("s must not be nil")
|
||||
}
|
||||
data := &snapshotJSON{}
|
||||
if err := jsonv2.UnmarshalDecode(in, data); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = Snapshot{m: data.Settings, sig: deephash.Hash(&data.Settings), summary: data.Summary}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(s) // uses MarshalJSONTo
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (s *Snapshot) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
|
||||
}
|
||||
|
||||
// MergeSnapshots returns a [Snapshot] that contains all [RawItem]s
|
||||
// from snapshot1 and snapshot2 and the [Summary] with the narrower [PolicyScope].
|
||||
// If there's a conflict between policy settings in the two snapshots,
|
||||
|
||||
21
vendor/tailscale.com/util/syspolicy/setting/summary.go
generated
vendored
21
vendor/tailscale.com/util/syspolicy/setting/summary.go
generated
vendored
@@ -54,24 +54,29 @@ func (s Summary) String() string {
|
||||
return s.data.Scope.String()
|
||||
}
|
||||
|
||||
// MarshalJSONV2 implements [jsonv2.MarshalerV2].
|
||||
func (s Summary) MarshalJSONV2(out *jsontext.Encoder, opts jsonv2.Options) error {
|
||||
return jsonv2.MarshalEncode(out, &s.data, opts)
|
||||
var (
|
||||
_ jsonv2.MarshalerTo = (*Summary)(nil)
|
||||
_ jsonv2.UnmarshalerFrom = (*Summary)(nil)
|
||||
)
|
||||
|
||||
// MarshalJSONTo implements [jsonv2.MarshalerTo].
|
||||
func (s Summary) MarshalJSONTo(out *jsontext.Encoder) error {
|
||||
return jsonv2.MarshalEncode(out, &s.data)
|
||||
}
|
||||
|
||||
// UnmarshalJSONV2 implements [jsonv2.UnmarshalerV2].
|
||||
func (s *Summary) UnmarshalJSONV2(in *jsontext.Decoder, opts jsonv2.Options) error {
|
||||
return jsonv2.UnmarshalDecode(in, &s.data, opts)
|
||||
// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
|
||||
func (s *Summary) UnmarshalJSONFrom(in *jsontext.Decoder) error {
|
||||
return jsonv2.UnmarshalDecode(in, &s.data)
|
||||
}
|
||||
|
||||
// MarshalJSON implements [json.Marshaler].
|
||||
func (s Summary) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.Marshal(s) // uses MarshalJSONV2
|
||||
return jsonv2.Marshal(s) // uses MarshalJSONTo
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements [json.Unmarshaler].
|
||||
func (s *Summary) UnmarshalJSON(b []byte) error {
|
||||
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONV2
|
||||
return jsonv2.Unmarshal(b, s) // uses UnmarshalJSONFrom
|
||||
}
|
||||
|
||||
// SummaryOption is an option that configures [Summary]
|
||||
|
||||
159
vendor/tailscale.com/util/syspolicy/source/env_policy_store.go
generated
vendored
Normal file
159
vendor/tailscale.com/util/syspolicy/source/env_policy_store.go
generated
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
var lookupEnv = os.LookupEnv // test hook
|
||||
|
||||
var _ Store = (*EnvPolicyStore)(nil)
|
||||
|
||||
// EnvPolicyStore is a [Store] that reads policy settings from environment variables.
|
||||
type EnvPolicyStore struct{}
|
||||
|
||||
// ReadString implements [Store].
|
||||
func (s *EnvPolicyStore) ReadString(key setting.Key) (string, error) {
|
||||
_, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [Store].
|
||||
func (s *EnvPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
name, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if str == "" {
|
||||
return 0, setting.ErrNotConfigured
|
||||
}
|
||||
value, err := strconv.ParseUint(str, 0, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s: %w: %q is not a valid uint64", name, setting.ErrTypeMismatch, str)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// ReadBoolean implements [Store].
|
||||
func (s *EnvPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
name, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if str == "" {
|
||||
return false, setting.ErrNotConfigured
|
||||
}
|
||||
value, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("%s: %w: %q is not a valid bool", name, setting.ErrTypeMismatch, str)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// ReadStringArray implements [Store].
|
||||
func (s *EnvPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
_, str, err := s.lookupSettingVariable(key)
|
||||
if err != nil || str == "" {
|
||||
return nil, err
|
||||
}
|
||||
var dst int
|
||||
res := strings.Split(str, ",")
|
||||
for src := range res {
|
||||
res[dst] = strings.TrimSpace(res[src])
|
||||
if res[dst] != "" {
|
||||
dst++
|
||||
}
|
||||
}
|
||||
return res[0:dst], nil
|
||||
}
|
||||
|
||||
func (s *EnvPolicyStore) lookupSettingVariable(key setting.Key) (name, value string, err error) {
|
||||
name, err = keyToEnvVarName(key)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
value, ok := lookupEnv(name)
|
||||
if !ok {
|
||||
return name, "", setting.ErrNotConfigured
|
||||
}
|
||||
return name, value, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyKey = errors.New("key must not be empty")
|
||||
errInvalidKey = errors.New("key must consist of alphanumeric characters and slashes")
|
||||
)
|
||||
|
||||
// keyToEnvVarName returns the environment variable name for a given policy
|
||||
// setting key, or an error if the key is invalid. It converts CamelCase keys into
|
||||
// underscore-separated words and prepends the variable name with the TS prefix.
|
||||
// For example: AuthKey => TS_AUTH_KEY, ExitNodeAllowLANAccess => TS_EXIT_NODE_ALLOW_LAN_ACCESS, etc.
|
||||
//
|
||||
// It's fine to use this in [EnvPolicyStore] without caching variable names since it's not a hot path.
|
||||
// [EnvPolicyStore] is not a [Changeable] policy store, so the conversion will only happen once.
|
||||
func keyToEnvVarName(key setting.Key) (string, error) {
|
||||
if len(key) == 0 {
|
||||
return "", errEmptyKey
|
||||
}
|
||||
|
||||
isLower := func(c byte) bool { return 'a' <= c && c <= 'z' }
|
||||
isUpper := func(c byte) bool { return 'A' <= c && c <= 'Z' }
|
||||
isLetter := func(c byte) bool { return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') }
|
||||
isDigit := func(c byte) bool { return '0' <= c && c <= '9' }
|
||||
|
||||
words := make([]string, 0, 8)
|
||||
words = append(words, "TS_DEBUGSYSPOLICY")
|
||||
var currentWord strings.Builder
|
||||
for i := 0; i < len(key); i++ {
|
||||
c := key[i]
|
||||
if c >= utf8.RuneSelf {
|
||||
return "", errInvalidKey
|
||||
}
|
||||
|
||||
var split bool
|
||||
switch {
|
||||
case isLower(c):
|
||||
c -= 'a' - 'A' // make upper
|
||||
split = currentWord.Len() > 0 && !isLetter(key[i-1])
|
||||
case isUpper(c):
|
||||
if currentWord.Len() > 0 {
|
||||
prevUpper := isUpper(key[i-1])
|
||||
nextLower := i < len(key)-1 && isLower(key[i+1])
|
||||
split = !prevUpper || nextLower // split on case transition
|
||||
}
|
||||
case isDigit(c):
|
||||
split = currentWord.Len() > 0 && !isDigit(key[i-1])
|
||||
case c == setting.KeyPathSeparator:
|
||||
words = append(words, currentWord.String())
|
||||
currentWord.Reset()
|
||||
continue
|
||||
default:
|
||||
return "", errInvalidKey
|
||||
}
|
||||
|
||||
if split {
|
||||
words = append(words, currentWord.String())
|
||||
currentWord.Reset()
|
||||
}
|
||||
|
||||
currentWord.WriteByte(c)
|
||||
}
|
||||
|
||||
if currentWord.Len() > 0 {
|
||||
words = append(words, currentWord.String())
|
||||
}
|
||||
|
||||
return strings.Join(words, "_"), nil
|
||||
}
|
||||
394
vendor/tailscale.com/util/syspolicy/source/policy_reader.go
generated
vendored
Normal file
394
vendor/tailscale.com/util/syspolicy/source/policy_reader.go
generated
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/internal/metrics"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// Reader reads all configured policy settings from a given [Store].
|
||||
// It registers a change callback with the [Store] and maintains the current version
|
||||
// of the [setting.Snapshot] by lazily re-reading policy settings from the [Store]
|
||||
// whenever a new settings snapshot is requested with [Reader.GetSettings].
|
||||
// It is safe for concurrent use.
|
||||
type Reader struct {
|
||||
store Store
|
||||
origin *setting.Origin
|
||||
settings []*setting.Definition
|
||||
unregisterChangeNotifier func()
|
||||
doneCh chan struct{} // closed when [Reader] is closed.
|
||||
|
||||
mu sync.Mutex
|
||||
closing bool
|
||||
upToDate bool
|
||||
lastPolicy *setting.Snapshot
|
||||
sessions set.HandleSet[*ReadingSession]
|
||||
}
|
||||
|
||||
// newReader returns a new [Reader] that reads policy settings from a given [Store].
|
||||
// The returned reader takes ownership of the store. If the store implements [io.Closer],
|
||||
// the returned reader will close the store when it is closed.
|
||||
func newReader(store Store, origin *setting.Origin) (*Reader, error) {
|
||||
settings, err := setting.Definitions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expirable, ok := store.(Expirable); ok {
|
||||
select {
|
||||
case <-expirable.Done():
|
||||
return nil, ErrStoreClosed
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
reader := &Reader{store: store, origin: origin, settings: settings, doneCh: make(chan struct{})}
|
||||
if changeable, ok := store.(Changeable); ok {
|
||||
// We should subscribe to policy change notifications first before reading
|
||||
// the policy settings from the store. This way we won't miss any notifications.
|
||||
if reader.unregisterChangeNotifier, err = changeable.RegisterChangeCallback(reader.onPolicyChange); err != nil {
|
||||
// Errors registering policy change callbacks are non-fatal.
|
||||
// TODO(nickkhyl): implement a background policy refresh every X minutes?
|
||||
loggerx.Errorf("failed to register %v policy change callback: %v", origin, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := reader.reload(true); err != nil {
|
||||
if reader.unregisterChangeNotifier != nil {
|
||||
reader.unregisterChangeNotifier()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expirable, ok := store.(Expirable); ok {
|
||||
if waitCh := expirable.Done(); waitCh != nil {
|
||||
go func() {
|
||||
select {
|
||||
case <-waitCh:
|
||||
reader.Close()
|
||||
case <-reader.doneCh:
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// GetSettings returns the current [*setting.Snapshot],
|
||||
// re-reading it from 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 {
|
||||
r.mu.Lock()
|
||||
upToDate, lastPolicy := r.upToDate, r.lastPolicy
|
||||
r.mu.Unlock()
|
||||
if upToDate {
|
||||
return lastPolicy
|
||||
}
|
||||
|
||||
policy, err := r.reload(false)
|
||||
if err != nil {
|
||||
// If the policy fails to reload completely, log an error and return the last cached version.
|
||||
// However, errors related to individual policy items are always
|
||||
// propagated to callers when they fetch those settings.
|
||||
loggerx.Errorf("failed to reload %v policy: %v", r.origin, err)
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
// ReadSettings reads policy settings from the underlying [Store] even if no
|
||||
// changes were detected. It returns the new [*setting.Snapshot],nil on
|
||||
// success or an undefined snapshot (possibly `nil`) along with a non-`nil`
|
||||
// error in case of failure.
|
||||
func (r *Reader) ReadSettings() (*setting.Snapshot, error) {
|
||||
return r.reload(true)
|
||||
}
|
||||
|
||||
// reload is like [Reader.ReadSettings], but allows specifying whether to re-read
|
||||
// an unchanged policy, and returns the last [*setting.Snapshot] if the read fails.
|
||||
func (r *Reader) reload(force bool) (*setting.Snapshot, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.upToDate && !force {
|
||||
return r.lastPolicy, nil
|
||||
}
|
||||
|
||||
if lockable, ok := r.store.(Lockable); ok {
|
||||
if err := lockable.Lock(); err != nil {
|
||||
return r.lastPolicy, err
|
||||
}
|
||||
defer lockable.Unlock()
|
||||
}
|
||||
|
||||
r.upToDate = true
|
||||
|
||||
metrics.Reset(r.origin)
|
||||
|
||||
var m map[setting.Key]setting.RawItem
|
||||
if lastPolicyCount := r.lastPolicy.Len(); lastPolicyCount > 0 {
|
||||
m = make(map[setting.Key]setting.RawItem, lastPolicyCount)
|
||||
}
|
||||
for _, s := range r.settings {
|
||||
if !r.origin.Scope().IsConfigurableSetting(s) {
|
||||
// Skip settings that cannot be configured in the current scope.
|
||||
continue
|
||||
}
|
||||
|
||||
val, err := readPolicySettingValue(r.store, s)
|
||||
if err != nil && (errors.Is(err, setting.ErrNoSuchKey) || errors.Is(err, setting.ErrNotConfigured)) {
|
||||
metrics.ReportNotConfigured(r.origin, s)
|
||||
continue
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
metrics.ReportConfigured(r.origin, s, val)
|
||||
} else {
|
||||
metrics.ReportError(r.origin, s, err)
|
||||
}
|
||||
|
||||
// If there's an error reading a single policy, such as a value type mismatch,
|
||||
// we'll wrap the error to preserve its text and return it
|
||||
// whenever someone attempts to fetch the value.
|
||||
// Otherwise, the errorText will be nil.
|
||||
errorText := setting.MaybeErrorText(err)
|
||||
item := setting.RawItemWith(val, errorText, r.origin)
|
||||
mak.Set(&m, s.Key(), item)
|
||||
}
|
||||
|
||||
newPolicy := setting.NewSnapshot(m, setting.SummaryWith(r.origin))
|
||||
if r.lastPolicy == nil || !newPolicy.EqualItems(r.lastPolicy) {
|
||||
r.lastPolicy = newPolicy
|
||||
}
|
||||
return r.lastPolicy, nil
|
||||
}
|
||||
|
||||
// ReadingSession is like [Reader], but with a channel that's written
|
||||
// to when there's a policy change, and closed when the session is terminated.
|
||||
type ReadingSession struct {
|
||||
reader *Reader
|
||||
policyChangedCh chan struct{} // 1-buffered channel
|
||||
handle set.Handle // in the reader.sessions
|
||||
closeInternal func()
|
||||
}
|
||||
|
||||
// OpenSession opens and returns a new session to r, allowing the caller
|
||||
// to get notified whenever a policy change is reported by the [source.Store],
|
||||
// or an [ErrStoreClosed] if the reader has already been closed.
|
||||
func (r *Reader) OpenSession() (*ReadingSession, error) {
|
||||
session := &ReadingSession{
|
||||
reader: r,
|
||||
policyChangedCh: make(chan struct{}, 1),
|
||||
}
|
||||
session.closeInternal = sync.OnceFunc(func() { close(session.policyChangedCh) })
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.closing {
|
||||
return nil, ErrStoreClosed
|
||||
}
|
||||
session.handle = r.sessions.Add(session)
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// GetSettings is like [Reader.GetSettings].
|
||||
func (s *ReadingSession) GetSettings() *setting.Snapshot {
|
||||
return s.reader.GetSettings()
|
||||
}
|
||||
|
||||
// ReadSettings is like [Reader.ReadSettings].
|
||||
func (s *ReadingSession) ReadSettings() (*setting.Snapshot, error) {
|
||||
return s.reader.ReadSettings()
|
||||
}
|
||||
|
||||
// PolicyChanged returns a channel that's written to when
|
||||
// there's a policy change, closed when the session is terminated.
|
||||
func (s *ReadingSession) PolicyChanged() <-chan struct{} {
|
||||
return s.policyChangedCh
|
||||
}
|
||||
|
||||
// Close unregisters this session with the [Reader].
|
||||
func (s *ReadingSession) Close() {
|
||||
s.reader.mu.Lock()
|
||||
delete(s.reader.sessions, s.handle)
|
||||
s.closeInternal()
|
||||
s.reader.mu.Unlock()
|
||||
}
|
||||
|
||||
// onPolicyChange handles a policy change notification from the [Store],
|
||||
// invalidating the current [setting.Snapshot] in r,
|
||||
// and notifying the active [ReadingSession]s.
|
||||
func (r *Reader) onPolicyChange() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.upToDate = false
|
||||
for _, s := range r.sessions {
|
||||
select {
|
||||
case s.policyChangedCh <- struct{}{}:
|
||||
// Notified.
|
||||
default:
|
||||
// 1-buffered channel is full, meaning that another policy change
|
||||
// notification is already en route.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the store reader and the underlying store.
|
||||
func (r *Reader) Close() error {
|
||||
r.mu.Lock()
|
||||
if r.closing {
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
r.closing = true
|
||||
r.mu.Unlock()
|
||||
|
||||
if r.unregisterChangeNotifier != nil {
|
||||
r.unregisterChangeNotifier()
|
||||
r.unregisterChangeNotifier = nil
|
||||
}
|
||||
|
||||
if closer, ok := r.store.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.store = nil
|
||||
|
||||
close(r.doneCh)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
for _, c := range r.sessions {
|
||||
c.closeInternal()
|
||||
}
|
||||
r.sessions = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the reader is closed.
|
||||
func (r *Reader) Done() <-chan struct{} {
|
||||
return r.doneCh
|
||||
}
|
||||
|
||||
// ReadableSource is a [Source] open for reading.
|
||||
type ReadableSource struct {
|
||||
*Source
|
||||
*ReadingSession
|
||||
}
|
||||
|
||||
// Close closes the underlying [ReadingSession].
|
||||
func (s ReadableSource) Close() {
|
||||
s.ReadingSession.Close()
|
||||
}
|
||||
|
||||
// ReadableSources is a slice of [ReadableSource].
|
||||
type ReadableSources []ReadableSource
|
||||
|
||||
// Contains reports whether s contains the specified source.
|
||||
func (s ReadableSources) Contains(source *Source) bool {
|
||||
return s.IndexOf(source) != -1
|
||||
}
|
||||
|
||||
// IndexOf returns position of the specified source in s, or -1
|
||||
// if the source does not exist.
|
||||
func (s ReadableSources) IndexOf(source *Source) int {
|
||||
return slices.IndexFunc(s, func(rs ReadableSource) bool {
|
||||
return rs.Source == source
|
||||
})
|
||||
}
|
||||
|
||||
// InsertionIndexOf returns the position at which source can be inserted
|
||||
// to maintain the sorted order of the readableSources.
|
||||
// The return value is unspecified if s is not sorted on entry to InsertionIndexOf.
|
||||
func (s ReadableSources) InsertionIndexOf(source *Source) int {
|
||||
// Insert new sources after any existing sources with the same precedence,
|
||||
// and just before the first source with higher precedence.
|
||||
// Just like stable sort, but for insertion.
|
||||
// It's okay to use linear search as insertions are rare
|
||||
// and we never have more than just a few policy sources.
|
||||
higherPrecedence := func(rs ReadableSource) bool { return rs.Compare(source) > 0 }
|
||||
if i := slices.IndexFunc(s, higherPrecedence); i != -1 {
|
||||
return i
|
||||
}
|
||||
return len(s)
|
||||
}
|
||||
|
||||
// StableSort sorts [ReadableSource] in s by precedence, so that policy
|
||||
// settings from sources with higher precedence (e.g., [DeviceScope])
|
||||
// will be read and merged last, overriding any policy settings with
|
||||
// the same keys configured in sources with lower precedence
|
||||
// (e.g., [CurrentUserScope]).
|
||||
func (s *ReadableSources) StableSort() {
|
||||
sort.SliceStable(*s, func(i, j int) bool {
|
||||
return (*s)[i].Source.Compare((*s)[j].Source) < 0
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAt closes and deletes the i-th source from s.
|
||||
func (s *ReadableSources) DeleteAt(i int) {
|
||||
(*s)[i].Close()
|
||||
*s = slices.Delete(*s, i, i+1)
|
||||
}
|
||||
|
||||
// Close closes and deletes all sources in s.
|
||||
func (s *ReadableSources) Close() {
|
||||
for _, s := range *s {
|
||||
s.Close()
|
||||
}
|
||||
*s = nil
|
||||
}
|
||||
|
||||
func readPolicySettingValue(store Store, s *setting.Definition) (value any, err error) {
|
||||
switch key := s.Key(); s.Type() {
|
||||
case setting.BooleanValue:
|
||||
return store.ReadBoolean(key)
|
||||
case setting.IntegerValue:
|
||||
return store.ReadUInt64(key)
|
||||
case setting.StringValue:
|
||||
return store.ReadString(key)
|
||||
case setting.StringListValue:
|
||||
return store.ReadStringArray(key)
|
||||
case setting.PreferenceOptionValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
var value setting.PreferenceOption
|
||||
if err = value.UnmarshalText([]byte(s)); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return setting.ShowChoiceByPolicy, err
|
||||
case setting.VisibilityValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
var value setting.Visibility
|
||||
if err = value.UnmarshalText([]byte(s)); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return setting.VisibleByPolicy, err
|
||||
case setting.DurationValue:
|
||||
s, err := store.ReadString(key)
|
||||
if err == nil {
|
||||
var value time.Duration
|
||||
if value, err = time.ParseDuration(s); err == nil {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: unsupported setting type: %v", setting.ErrTypeMismatch, s.Type())
|
||||
}
|
||||
}
|
||||
146
vendor/tailscale.com/util/syspolicy/source/policy_source.go
generated
vendored
Normal file
146
vendor/tailscale.com/util/syspolicy/source/policy_source.go
generated
vendored
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package source defines interfaces for policy stores,
|
||||
// facilitates the creation of policy sources, and provides
|
||||
// functionality for reading policy settings from these sources.
|
||||
package source
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
// ErrStoreClosed is an error returned when attempting to use a [Store] after it
|
||||
// has been closed.
|
||||
var ErrStoreClosed = errors.New("the policy store has been closed")
|
||||
|
||||
// Store provides methods to read system policy settings from OS-specific storage.
|
||||
// Implementations must be concurrency-safe, and may also implement
|
||||
// [Lockable], [Changeable], [Expirable] and [io.Closer].
|
||||
//
|
||||
// If a [Store] implementation also implements [io.Closer],
|
||||
// it will be called by the package to release the resources
|
||||
// when the store is no longer needed.
|
||||
type Store interface {
|
||||
// ReadString returns the value of a [setting.StringValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadString(key setting.Key) (string, error)
|
||||
// ReadUInt64 returns the value of a [setting.IntegerValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadUInt64(key setting.Key) (uint64, error)
|
||||
// ReadBoolean returns the value of a [setting.BooleanValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadBoolean(key setting.Key) (bool, error)
|
||||
// ReadStringArray returns the value of a [setting.StringListValue] with the specified key,
|
||||
// an [setting.ErrNotConfigured] if the policy setting is not configured, or
|
||||
// an error on failure.
|
||||
ReadStringArray(key setting.Key) ([]string, error)
|
||||
}
|
||||
|
||||
// Lockable is an optional interface that [Store] implementations may support.
|
||||
// Locking a [Store] is not mandatory as [Store] must be concurrency-safe,
|
||||
// but is recommended to avoid issues where consecutive read calls for related
|
||||
// policies might return inconsistent results if a policy change occurs between
|
||||
// the calls. Implementations may use locking to pre-read policies or for
|
||||
// similar performance optimizations.
|
||||
type Lockable interface {
|
||||
// Lock acquires a read lock on the policy store,
|
||||
// ensuring the store's state remains unchanged while locked.
|
||||
// Multiple readers can hold the lock simultaneously.
|
||||
// It returns an error if the store cannot be locked.
|
||||
Lock() error
|
||||
// Unlock unlocks the policy store.
|
||||
// It is a run-time error if the store is not locked on entry to Unlock.
|
||||
Unlock()
|
||||
}
|
||||
|
||||
// Changeable is an optional interface that [Store] implementations may support
|
||||
// if the policy settings they contain can be externally changed after being initially read.
|
||||
type Changeable interface {
|
||||
// RegisterChangeCallback adds a function that will be called
|
||||
// whenever there's a policy change in the [Store].
|
||||
// The returned function can be used to unregister the callback.
|
||||
RegisterChangeCallback(callback func()) (unregister func(), err error)
|
||||
}
|
||||
|
||||
// Expirable is an optional interface that [Store] implementations may support
|
||||
// if they can be externally closed or otherwise become invalid while in use.
|
||||
type Expirable interface {
|
||||
// Done returns a channel that is closed when the policy [Store] should no longer be used.
|
||||
// It should return nil if the store never expires.
|
||||
Done() <-chan struct{}
|
||||
}
|
||||
|
||||
// Source represents a named source of policy settings for a given [setting.PolicyScope].
|
||||
type Source struct {
|
||||
name string
|
||||
scope setting.PolicyScope
|
||||
store Store
|
||||
origin *setting.Origin
|
||||
|
||||
lazyReader lazy.SyncValue[*Reader]
|
||||
}
|
||||
|
||||
// NewSource returns a new [Source] with the specified name, scope, and store.
|
||||
func NewSource(name string, scope setting.PolicyScope, store Store) *Source {
|
||||
return &Source{name: name, scope: scope, store: store, origin: setting.NewNamedOrigin(name, scope)}
|
||||
}
|
||||
|
||||
// Name reports the name of the policy source.
|
||||
func (s *Source) Name() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// Scope reports the management scope of the policy source.
|
||||
func (s *Source) Scope() setting.PolicyScope {
|
||||
return s.scope
|
||||
}
|
||||
|
||||
// Reader returns a [Reader] that reads from this source's [Store].
|
||||
func (s *Source) Reader() (*Reader, error) {
|
||||
return s.lazyReader.GetErr(func() (*Reader, error) {
|
||||
return newReader(s.store, s.origin)
|
||||
})
|
||||
}
|
||||
|
||||
// Description returns a formatted string with the scope and name of this policy source.
|
||||
// It can be used for logging or display purposes.
|
||||
func (s *Source) Description() string {
|
||||
if s.name != "" {
|
||||
return fmt.Sprintf("%s (%v)", s.name, s.Scope())
|
||||
}
|
||||
return s.Scope().String()
|
||||
}
|
||||
|
||||
// Compare returns an integer comparing s and s2
|
||||
// by their precedence, following the "last-wins" model.
|
||||
// The result will be:
|
||||
//
|
||||
// -1 if policy settings from s should be processed before policy settings from s2;
|
||||
// +1 if policy settings from s should be processed after policy settings from s2, overriding s2;
|
||||
// 0 if the relative processing order of policy settings in s and s2 is unspecified.
|
||||
func (s *Source) Compare(s2 *Source) int {
|
||||
return cmp.Compare(s2.Scope().Kind(), s.Scope().Kind())
|
||||
}
|
||||
|
||||
// Close closes the [Source] and the underlying [Store].
|
||||
func (s *Source) Close() error {
|
||||
// The [Reader], if any, owns the [Store].
|
||||
if reader, _ := s.lazyReader.GetErr(func() (*Reader, error) { return nil, ErrStoreClosed }); reader != nil {
|
||||
return reader.Close()
|
||||
}
|
||||
// Otherwise, it is our responsibility to close it.
|
||||
if closer, ok := s.store.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
528
vendor/tailscale.com/util/syspolicy/source/policy_store_windows.go
generated
vendored
Normal file
528
vendor/tailscale.com/util/syspolicy/source/policy_store_windows.go
generated
vendored
Normal file
@@ -0,0 +1,528 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/winutil/gp"
|
||||
)
|
||||
|
||||
const (
|
||||
softwareKeyName = `Software`
|
||||
tsPoliciesSubkey = `Policies\Tailscale`
|
||||
tsIPNSubkey = `Tailscale IPN` // the legacy key we need to fallback to
|
||||
)
|
||||
|
||||
var (
|
||||
_ Store = (*PlatformPolicyStore)(nil)
|
||||
_ Lockable = (*PlatformPolicyStore)(nil)
|
||||
_ Changeable = (*PlatformPolicyStore)(nil)
|
||||
_ Expirable = (*PlatformPolicyStore)(nil)
|
||||
)
|
||||
|
||||
// lockableCloser is a [Lockable] that can also be closed.
|
||||
// It is implemented by [gp.PolicyLock] and [optionalPolicyLock].
|
||||
type lockableCloser interface {
|
||||
Lockable
|
||||
Close() error
|
||||
}
|
||||
|
||||
var (
|
||||
_ lockableCloser = (*gp.PolicyLock)(nil)
|
||||
_ lockableCloser = (*optionalPolicyLock)(nil)
|
||||
)
|
||||
|
||||
// PlatformPolicyStore implements [Store] by providing read access to
|
||||
// Registry-based Tailscale policies, such as those configured via Group Policy or MDM.
|
||||
// For better performance and consistency, it is recommended to lock it when
|
||||
// reading multiple policy settings sequentially.
|
||||
// It also allows subscribing to policy change notifications.
|
||||
type PlatformPolicyStore struct {
|
||||
scope gp.Scope // [gp.MachinePolicy] or [gp.UserPolicy]
|
||||
|
||||
// The softwareKey can be HKLM\Software, HKCU\Software, or
|
||||
// HKU\{SID}\Software. Anything below the Software subkey, including
|
||||
// Software\Policies, may not yet exist or could be deleted throughout the
|
||||
// [PlatformPolicyStore]'s lifespan, invalidating the handle. We also prefer
|
||||
// to always use a real registry key (rather than a predefined HKLM or HKCU)
|
||||
// to simplify bookkeeping (predefined keys should never be closed).
|
||||
// Finally, this will allow us to watch for any registry changes directly
|
||||
// should we need this in the future in addition to gp.ChangeWatcher.
|
||||
softwareKey registry.Key
|
||||
watcher *gp.ChangeWatcher
|
||||
|
||||
done chan struct{} // done is closed when Close call completes
|
||||
|
||||
// The policyLock can be locked by the caller when reading multiple policy settings
|
||||
// to prevent the Group Policy Client service from modifying policies while
|
||||
// they are being read.
|
||||
//
|
||||
// When both policyLock and mu need to be taken, mu must be taken before policyLock.
|
||||
policyLock lockableCloser
|
||||
|
||||
mu sync.Mutex
|
||||
tsKeys []registry.Key // or nil if the [PlatformPolicyStore] hasn't been locked.
|
||||
cbs set.HandleSet[func()] // policy change callbacks
|
||||
lockCnt int
|
||||
locked sync.WaitGroup
|
||||
closing bool
|
||||
closed bool
|
||||
}
|
||||
|
||||
type registryValueGetter[T any] func(key registry.Key, name string) (T, error)
|
||||
|
||||
// NewMachinePlatformPolicyStore returns a new [PlatformPolicyStore] for the machine.
|
||||
func NewMachinePlatformPolicyStore() (*PlatformPolicyStore, error) {
|
||||
softwareKey, err := registry.OpenKey(registry.LOCAL_MACHINE, softwareKeyName, windows.KEY_READ)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open the %s key: %w", softwareKeyName, err)
|
||||
}
|
||||
return newPlatformPolicyStore(gp.MachinePolicy, softwareKey, gp.NewMachinePolicyLock()), nil
|
||||
}
|
||||
|
||||
// NewUserPlatformPolicyStore returns a new [PlatformPolicyStore] for the user specified by its token.
|
||||
// User's profile must be loaded, and the token handle must have [windows.TOKEN_QUERY]
|
||||
// and [windows.TOKEN_DUPLICATE] access. The caller retains ownership of the token.
|
||||
func NewUserPlatformPolicyStore(token windows.Token) (*PlatformPolicyStore, error) {
|
||||
var err error
|
||||
var softwareKey registry.Key
|
||||
if token != 0 {
|
||||
var user *windows.Tokenuser
|
||||
if user, err = token.GetTokenUser(); err != nil {
|
||||
return nil, fmt.Errorf("failed to get token user: %w", err)
|
||||
}
|
||||
userSid := user.User.Sid
|
||||
softwareKey, err = registry.OpenKey(registry.USERS, userSid.String()+`\`+softwareKeyName, windows.KEY_READ)
|
||||
} else {
|
||||
softwareKey, err = registry.OpenKey(registry.CURRENT_USER, softwareKeyName, windows.KEY_READ)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open the %s key: %w", softwareKeyName, err)
|
||||
}
|
||||
policyLock, err := gp.NewUserPolicyLock(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create a user policy lock: %w", err)
|
||||
}
|
||||
return newPlatformPolicyStore(gp.UserPolicy, softwareKey, policyLock), nil
|
||||
}
|
||||
|
||||
func newPlatformPolicyStore(scope gp.Scope, softwareKey registry.Key, policyLock *gp.PolicyLock) *PlatformPolicyStore {
|
||||
return &PlatformPolicyStore{
|
||||
scope: scope,
|
||||
softwareKey: softwareKey,
|
||||
done: make(chan struct{}),
|
||||
policyLock: &optionalPolicyLock{PolicyLock: policyLock},
|
||||
}
|
||||
}
|
||||
|
||||
// Lock locks the policy store, preventing the system from modifying the policies
|
||||
// while they are being read. It is a read lock that may be acquired by multiple goroutines.
|
||||
// Each Lock call must be balanced by exactly one Unlock call.
|
||||
func (ps *PlatformPolicyStore) Lock() (err error) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
if ps.closing {
|
||||
return ErrStoreClosed
|
||||
}
|
||||
|
||||
ps.lockCnt += 1
|
||||
if ps.lockCnt != 1 {
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ps.lockCnt -= 1
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure ps remains open while the lock is held.
|
||||
ps.locked.Add(1)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ps.locked.Done()
|
||||
}
|
||||
}()
|
||||
|
||||
// Acquire the GP lock to prevent the system from modifying policy settings
|
||||
// while they are being read.
|
||||
if err := ps.policyLock.Lock(); err != nil {
|
||||
if errors.Is(err, gp.ErrInvalidLockState) {
|
||||
// The policy store is being closed and we've lost the race.
|
||||
return ErrStoreClosed
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
ps.policyLock.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep the Tailscale's registry keys open for the duration of the lock.
|
||||
keyNames := tailscaleKeyNamesFor(ps.scope)
|
||||
ps.tsKeys = make([]registry.Key, 0, len(keyNames))
|
||||
for _, keyName := range keyNames {
|
||||
var tsKey registry.Key
|
||||
tsKey, err = registry.OpenKey(ps.softwareKey, keyName, windows.KEY_READ)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
ps.tsKeys = append(ps.tsKeys, tsKey)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock decrements the lock counter and unlocks the policy store once the counter reaches 0.
|
||||
// It panics if ps is not locked on entry to Unlock.
|
||||
func (ps *PlatformPolicyStore) Unlock() {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
ps.lockCnt -= 1
|
||||
if ps.lockCnt < 0 {
|
||||
panic("negative lockCnt")
|
||||
} else if ps.lockCnt != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, key := range ps.tsKeys {
|
||||
key.Close()
|
||||
}
|
||||
ps.tsKeys = nil
|
||||
ps.policyLock.Unlock()
|
||||
ps.locked.Done()
|
||||
}
|
||||
|
||||
// RegisterChangeCallback adds a function that will be called whenever there's a policy change.
|
||||
// It returns a function that can be used to unregister the specified callback or an error.
|
||||
// The error is [ErrStoreClosed] if ps has already been closed.
|
||||
func (ps *PlatformPolicyStore) RegisterChangeCallback(cb func()) (unregister func(), err error) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
if ps.closing {
|
||||
return nil, ErrStoreClosed
|
||||
}
|
||||
|
||||
handle := ps.cbs.Add(cb)
|
||||
if len(ps.cbs) == 1 {
|
||||
if ps.watcher, err = gp.NewChangeWatcher(ps.scope, ps.onChange); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return func() {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
delete(ps.cbs, handle)
|
||||
if len(ps.cbs) == 0 {
|
||||
if ps.watcher != nil {
|
||||
ps.watcher.Close()
|
||||
ps.watcher = nil
|
||||
}
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ps *PlatformPolicyStore) onChange() {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
if ps.closing {
|
||||
return
|
||||
}
|
||||
for _, callback := range ps.cbs {
|
||||
go callback()
|
||||
}
|
||||
}
|
||||
|
||||
// ReadString retrieves a string policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadString(key setting.Key) (val string, err error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (string, error) {
|
||||
val, _, err := key.GetStringValue(valueName)
|
||||
return val, err
|
||||
})
|
||||
}
|
||||
|
||||
// ReadUInt64 retrieves an integer policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (uint64, error) {
|
||||
val, _, err := key.GetIntegerValue(valueName)
|
||||
return val, err
|
||||
})
|
||||
}
|
||||
|
||||
// ReadBoolean retrieves a boolean policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) (bool, error) {
|
||||
val, _, err := key.GetIntegerValue(valueName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return val != 0, nil
|
||||
})
|
||||
}
|
||||
|
||||
// ReadString retrieves a multi-string policy with the specified key.
|
||||
// It returns [setting.ErrNotConfigured] if the policy setting does not exist.
|
||||
func (ps *PlatformPolicyStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
return getPolicyValue(ps, key,
|
||||
func(key registry.Key, valueName string) ([]string, error) {
|
||||
val, _, err := key.GetStringsValue(valueName)
|
||||
if err != registry.ErrNotExist {
|
||||
return val, err // the err may be nil or non-nil
|
||||
}
|
||||
|
||||
// The idiomatic way to store multiple string values in Group Policy
|
||||
// and MDM for Windows is to have multiple REG_SZ (or REG_EXPAND_SZ)
|
||||
// values under a subkey rather than in a single REG_MULTI_SZ value.
|
||||
//
|
||||
// See the Group Policy: Registry Extension Encoding specification,
|
||||
// and specifically the ListElement and ListBox types.
|
||||
// https://web.archive.org/web/20240721033657/https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-GPREG/%5BMS-GPREG%5D.pdf
|
||||
valKey, err := registry.OpenKey(key, valueName, windows.KEY_READ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valNames, err := valKey.ReadValueNames(0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val = make([]string, 0, len(valNames))
|
||||
for _, name := range valNames {
|
||||
switch item, _, err := valKey.GetStringValue(name); {
|
||||
case err == registry.ErrNotExist:
|
||||
continue
|
||||
case err != nil:
|
||||
return nil, err
|
||||
default:
|
||||
val = append(val, item)
|
||||
}
|
||||
}
|
||||
return val, nil
|
||||
})
|
||||
}
|
||||
|
||||
// splitSettingKey extracts the registry key name and value name from a [setting.Key].
|
||||
// The [setting.Key] format allows grouping settings into nested categories using one
|
||||
// or more [setting.KeyPathSeparator]s in the path. How individual policy settings are
|
||||
// stored is an implementation detail of each [Store]. In the [PlatformPolicyStore]
|
||||
// for Windows, we map nested policy categories onto the Registry key hierarchy.
|
||||
// The last component after a [setting.KeyPathSeparator] is treated as the value name,
|
||||
// while everything preceding it is considered a subpath (relative to the {HKLM,HKCU}\Software\Policies\Tailscale key).
|
||||
// If there are no [setting.KeyPathSeparator]s in the key, the policy setting value
|
||||
// is meant to be stored directly under {HKLM,HKCU}\Software\Policies\Tailscale.
|
||||
func splitSettingKey(key setting.Key) (path, valueName string) {
|
||||
if idx := strings.LastIndexByte(string(key), setting.KeyPathSeparator); idx != -1 {
|
||||
path = strings.ReplaceAll(string(key[:idx]), string(setting.KeyPathSeparator), `\`)
|
||||
valueName = string(key[idx+1:])
|
||||
return path, valueName
|
||||
}
|
||||
return "", string(key)
|
||||
}
|
||||
|
||||
func getPolicyValue[T any](ps *PlatformPolicyStore, key setting.Key, getter registryValueGetter[T]) (T, error) {
|
||||
var zero T
|
||||
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
if ps.closed {
|
||||
return zero, ErrStoreClosed
|
||||
}
|
||||
|
||||
path, valueName := splitSettingKey(key)
|
||||
getValue := func(key registry.Key) (T, error) {
|
||||
var err error
|
||||
if path != "" {
|
||||
key, err = registry.OpenKey(key, path, windows.KEY_READ)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
defer key.Close()
|
||||
}
|
||||
return getter(key, valueName)
|
||||
}
|
||||
|
||||
if ps.tsKeys != nil {
|
||||
// A non-nil tsKeys indicates that ps has been locked.
|
||||
// The slice may be empty if Tailscale policy keys do not exist.
|
||||
for _, tsKey := range ps.tsKeys {
|
||||
val, err := getValue(tsKey)
|
||||
if err == nil || err != registry.ErrNotExist {
|
||||
return val, err
|
||||
}
|
||||
}
|
||||
return zero, setting.ErrNotConfigured
|
||||
}
|
||||
|
||||
// The ps has not been locked, so we don't have any pre-opened keys.
|
||||
for _, tsKeyName := range tailscaleKeyNamesFor(ps.scope) {
|
||||
var tsKey registry.Key
|
||||
tsKey, err := registry.OpenKey(ps.softwareKey, tsKeyName, windows.KEY_READ)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
continue
|
||||
}
|
||||
return zero, err
|
||||
}
|
||||
val, err := getValue(tsKey)
|
||||
tsKey.Close()
|
||||
if err == nil || err != registry.ErrNotExist {
|
||||
return val, err
|
||||
}
|
||||
}
|
||||
|
||||
return zero, setting.ErrNotConfigured
|
||||
}
|
||||
|
||||
// Close closes the policy store and releases any associated resources.
|
||||
// It cancels pending locks and prevents any new lock attempts,
|
||||
// but waits for existing locks to be released.
|
||||
func (ps *PlatformPolicyStore) Close() error {
|
||||
// Request to close the Group Policy read lock.
|
||||
// Existing held locks will remain valid, but any new or pending locks
|
||||
// will fail. In certain scenarios, the corresponding write lock may be held
|
||||
// by the Group Policy service for extended periods (minutes rather than
|
||||
// seconds or milliseconds). In such cases, we prefer not to wait that long
|
||||
// if the ps is being closed anyway.
|
||||
if ps.policyLock != nil {
|
||||
ps.policyLock.Close()
|
||||
}
|
||||
|
||||
// Mark ps as closing to fast-fail any new lock attempts.
|
||||
// Callers that have already locked it can finish their reading.
|
||||
ps.mu.Lock()
|
||||
if ps.closing {
|
||||
ps.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
ps.closing = true
|
||||
if ps.watcher != nil {
|
||||
ps.watcher.Close()
|
||||
ps.watcher = nil
|
||||
}
|
||||
ps.mu.Unlock()
|
||||
|
||||
// Signal to the external code that ps should no longer be used.
|
||||
close(ps.done)
|
||||
|
||||
// Wait for any outstanding locks to be released.
|
||||
ps.locked.Wait()
|
||||
|
||||
// Deny any further read attempts and release remaining resources.
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
ps.cbs = nil
|
||||
ps.policyLock = nil
|
||||
ps.closed = true
|
||||
if ps.softwareKey != 0 {
|
||||
ps.softwareKey.Close()
|
||||
ps.softwareKey = 0
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the Close method is called.
|
||||
func (ps *PlatformPolicyStore) Done() <-chan struct{} {
|
||||
return ps.done
|
||||
}
|
||||
|
||||
func tailscaleKeyNamesFor(scope gp.Scope) []string {
|
||||
switch scope {
|
||||
case gp.MachinePolicy:
|
||||
// If a computer-side policy value does not exist under Software\Policies\Tailscale,
|
||||
// we need to fallback and use the legacy Software\Tailscale IPN key.
|
||||
return []string{tsPoliciesSubkey, tsIPNSubkey}
|
||||
case gp.UserPolicy:
|
||||
// However, we've never used the legacy key with user-side policies,
|
||||
// and we should never do so. Unlike HKLM\Software\Tailscale IPN,
|
||||
// its HKCU counterpart is user-writable.
|
||||
return []string{tsPoliciesSubkey}
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
type gpLockState int
|
||||
|
||||
const (
|
||||
gpUnlocked = gpLockState(iota)
|
||||
gpLocked
|
||||
gpLockRestricted // the lock could not be acquired due to a restriction in place
|
||||
)
|
||||
|
||||
// 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,
|
||||
// and calling [optionalPolicyLock.Unlock] is a no-op.
|
||||
//
|
||||
// The underlying GP lock is kinda optional: it is safe to read policy settings
|
||||
// from the Registry without acquiring it, but it is recommended to lock it anyway
|
||||
// when reading multiple policy settings to avoid potentially inconsistent results.
|
||||
//
|
||||
// It is not safe for concurrent use.
|
||||
type optionalPolicyLock struct {
|
||||
*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.
|
||||
// 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:
|
||||
o.state = gpLocked
|
||||
return nil
|
||||
case gp.ErrLockRestricted:
|
||||
loggerx.Errorf("GP lock not acquired: %v", err)
|
||||
o.state = gpLockRestricted
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock releases the underlying [gp.PolicyLock], if it was previously acquired.
|
||||
// It is a runtime error to call Unlock when the lock is not held.
|
||||
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.
|
||||
case gpUnlocked:
|
||||
panic("not locked")
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
o.state = gpUnlocked
|
||||
}
|
||||
449
vendor/tailscale.com/util/syspolicy/source/test_store.go
generated
vendored
Normal file
449
vendor/tailscale.com/util/syspolicy/source/test_store.go
generated
vendored
Normal file
@@ -0,0 +1,449 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
xmaps "golang.org/x/exp/maps"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/util/syspolicy/internal"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
)
|
||||
|
||||
var (
|
||||
_ Store = (*TestStore)(nil)
|
||||
_ Lockable = (*TestStore)(nil)
|
||||
_ Changeable = (*TestStore)(nil)
|
||||
_ Expirable = (*TestStore)(nil)
|
||||
)
|
||||
|
||||
// TestValueType is a constraint that allows types supported by [TestStore].
|
||||
type TestValueType interface {
|
||||
bool | uint64 | string | []string
|
||||
}
|
||||
|
||||
// TestSetting is a policy setting in a [TestStore].
|
||||
type TestSetting[T TestValueType] struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
// Error is the error to be returned by the [TestStore] when reading
|
||||
// a policy setting with the specified key.
|
||||
Error error
|
||||
// Value is the value to be returned by the [TestStore] when reading
|
||||
// a policy setting with the specified key.
|
||||
// It is only used if the Error is nil.
|
||||
Value T
|
||||
}
|
||||
|
||||
// TestSettingOf returns a [TestSetting] representing a policy setting
|
||||
// configured with the specified key and value.
|
||||
func TestSettingOf[T TestValueType](key setting.Key, value T) TestSetting[T] {
|
||||
return TestSetting[T]{Key: key, Value: value}
|
||||
}
|
||||
|
||||
// TestSettingWithError returns a [TestSetting] representing a policy setting
|
||||
// with the specified key and error.
|
||||
func TestSettingWithError[T TestValueType](key setting.Key, err error) TestSetting[T] {
|
||||
return TestSetting[T]{Key: key, Error: err}
|
||||
}
|
||||
|
||||
// testReadOperation describes a single policy setting read operation.
|
||||
type testReadOperation struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
// Type is a value type of a read operation.
|
||||
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
|
||||
Type setting.Type
|
||||
}
|
||||
|
||||
// TestExpectedReads is the number of read operations with the specified details.
|
||||
type TestExpectedReads struct {
|
||||
// Key is the setting's unique identifier.
|
||||
Key setting.Key
|
||||
// Type is a value type of a read operation.
|
||||
// [setting.BooleanValue], [setting.IntegerValue], [setting.StringValue] or [setting.StringListValue]
|
||||
Type setting.Type
|
||||
// NumTimes is how many times a setting with the specified key and type should have been read.
|
||||
NumTimes int
|
||||
}
|
||||
|
||||
func (r TestExpectedReads) operation() testReadOperation {
|
||||
return testReadOperation{r.Key, r.Type}
|
||||
}
|
||||
|
||||
// TestStore is a [Store] that can be used in tests.
|
||||
type TestStore struct {
|
||||
tb internal.TB
|
||||
|
||||
done chan struct{}
|
||||
|
||||
storeLock sync.RWMutex // its RLock is exposed via [Store.Lock]/[Store.Unlock].
|
||||
storeLockCount atomic.Int32
|
||||
|
||||
mu sync.RWMutex
|
||||
suspendCount int // change callback are suspended if > 0
|
||||
mr, mw map[setting.Key]any // maps for reading and writing; they're the same unless the store is suspended.
|
||||
cbs set.HandleSet[func()]
|
||||
closed bool
|
||||
|
||||
readsMu sync.Mutex
|
||||
reads map[testReadOperation]int // how many times a policy setting was read
|
||||
}
|
||||
|
||||
// NewTestStore returns a new [TestStore].
|
||||
// The tb will be used to report coding errors detected by the [TestStore].
|
||||
func NewTestStore(tb internal.TB) *TestStore {
|
||||
m := make(map[setting.Key]any)
|
||||
store := &TestStore{
|
||||
tb: tb,
|
||||
done: make(chan struct{}),
|
||||
mr: m,
|
||||
mw: m,
|
||||
}
|
||||
tb.Cleanup(store.Close)
|
||||
return store
|
||||
}
|
||||
|
||||
// NewTestStoreOf is a shorthand for [NewTestStore] followed by [TestStore.SetBooleans],
|
||||
// [TestStore.SetUInt64s], [TestStore.SetStrings] or [TestStore.SetStringLists].
|
||||
func NewTestStoreOf[T TestValueType](tb internal.TB, settings ...TestSetting[T]) *TestStore {
|
||||
store := NewTestStore(tb)
|
||||
switch settings := any(settings).(type) {
|
||||
case []TestSetting[bool]:
|
||||
store.SetBooleans(settings...)
|
||||
case []TestSetting[uint64]:
|
||||
store.SetUInt64s(settings...)
|
||||
case []TestSetting[string]:
|
||||
store.SetStrings(settings...)
|
||||
case []TestSetting[[]string]:
|
||||
store.SetStringLists(settings...)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Lock implements [Lockable].
|
||||
func (s *TestStore) Lock() error {
|
||||
s.storeLock.RLock()
|
||||
s.storeLockCount.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unlock implements [Lockable].
|
||||
func (s *TestStore) Unlock() {
|
||||
if s.storeLockCount.Add(-1) < 0 {
|
||||
s.tb.Fatal("negative storeLockCount")
|
||||
}
|
||||
s.storeLock.RUnlock()
|
||||
}
|
||||
|
||||
// RegisterChangeCallback implements [Changeable].
|
||||
func (s *TestStore) RegisterChangeCallback(callback func()) (unregister func(), err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
handle := s.cbs.Add(callback)
|
||||
return func() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
delete(s.cbs, handle)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReadString implements [Store].
|
||||
func (s *TestStore) ReadString(key setting.Key) (string, error) {
|
||||
defer s.recordRead(key, setting.StringValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
v, ok := s.mr[key]
|
||||
if !ok {
|
||||
return "", setting.ErrNotConfigured
|
||||
}
|
||||
if err, ok := v.(error); ok {
|
||||
return "", err
|
||||
}
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("%w in ReadString: got %T", setting.ErrTypeMismatch, v)
|
||||
}
|
||||
return str, nil
|
||||
}
|
||||
|
||||
// ReadUInt64 implements [Store].
|
||||
func (s *TestStore) ReadUInt64(key setting.Key) (uint64, error) {
|
||||
defer s.recordRead(key, setting.IntegerValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
v, ok := s.mr[key]
|
||||
if !ok {
|
||||
return 0, setting.ErrNotConfigured
|
||||
}
|
||||
if err, ok := v.(error); ok {
|
||||
return 0, err
|
||||
}
|
||||
u64, ok := v.(uint64)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("%w in ReadUInt64: got %T", setting.ErrTypeMismatch, v)
|
||||
}
|
||||
return u64, nil
|
||||
}
|
||||
|
||||
// ReadBoolean implements [Store].
|
||||
func (s *TestStore) ReadBoolean(key setting.Key) (bool, error) {
|
||||
defer s.recordRead(key, setting.BooleanValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
v, ok := s.mr[key]
|
||||
if !ok {
|
||||
return false, setting.ErrNotConfigured
|
||||
}
|
||||
if err, ok := v.(error); ok {
|
||||
return false, err
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("%w in ReadBoolean: got %T", setting.ErrTypeMismatch, v)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// ReadStringArray implements [Store].
|
||||
func (s *TestStore) ReadStringArray(key setting.Key) ([]string, error) {
|
||||
defer s.recordRead(key, setting.StringListValue)
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
v, ok := s.mr[key]
|
||||
if !ok {
|
||||
return nil, setting.ErrNotConfigured
|
||||
}
|
||||
if err, ok := v.(error); ok {
|
||||
return nil, err
|
||||
}
|
||||
slice, ok := v.([]string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w in ReadStringArray: got %T", setting.ErrTypeMismatch, v)
|
||||
}
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func (s *TestStore) recordRead(key setting.Key, typ setting.Type) {
|
||||
s.readsMu.Lock()
|
||||
op := testReadOperation{key, typ}
|
||||
num := s.reads[op]
|
||||
num++
|
||||
mak.Set(&s.reads, op, num)
|
||||
s.readsMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *TestStore) ResetCounters() {
|
||||
s.readsMu.Lock()
|
||||
clear(s.reads)
|
||||
s.readsMu.Unlock()
|
||||
}
|
||||
|
||||
// ReadsMustEqual fails the test if the actual reads differs from the specified reads.
|
||||
func (s *TestStore) ReadsMustEqual(reads ...TestExpectedReads) {
|
||||
s.tb.Helper()
|
||||
s.readsMu.Lock()
|
||||
defer s.readsMu.Unlock()
|
||||
s.readsMustContainLocked(reads...)
|
||||
s.readMustNoExtraLocked(reads...)
|
||||
}
|
||||
|
||||
// ReadsMustContain fails the test if the specified reads have not been made,
|
||||
// or have been made a different number of times. It permits other values to be
|
||||
// read in addition to the ones being tested.
|
||||
func (s *TestStore) ReadsMustContain(reads ...TestExpectedReads) {
|
||||
s.tb.Helper()
|
||||
s.readsMu.Lock()
|
||||
defer s.readsMu.Unlock()
|
||||
s.readsMustContainLocked(reads...)
|
||||
}
|
||||
|
||||
func (s *TestStore) readsMustContainLocked(reads ...TestExpectedReads) {
|
||||
s.tb.Helper()
|
||||
for _, r := range reads {
|
||||
if numTimes := s.reads[r.operation()]; numTimes != r.NumTimes {
|
||||
s.tb.Errorf("%q (%v) reads: got %v, want %v", r.Key, r.Type, numTimes, r.NumTimes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestStore) readMustNoExtraLocked(reads ...TestExpectedReads) {
|
||||
s.tb.Helper()
|
||||
rs := make(set.Set[testReadOperation])
|
||||
for i := range reads {
|
||||
rs.Add(reads[i].operation())
|
||||
}
|
||||
for ro, num := range s.reads {
|
||||
if !rs.Contains(ro) {
|
||||
s.tb.Errorf("%q (%v) reads: got %v, want 0", ro.Key, ro.Type, num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Suspend suspends the store, batching changes and notifications
|
||||
// until [TestStore.Resume] is called the same number of times as Suspend.
|
||||
func (s *TestStore) Suspend() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.suspendCount++; s.suspendCount == 1 {
|
||||
s.mw = xmaps.Clone(s.mr)
|
||||
}
|
||||
}
|
||||
|
||||
// Resume resumes the store, applying the changes and invoking
|
||||
// the change callbacks.
|
||||
func (s *TestStore) Resume() {
|
||||
s.storeLock.Lock()
|
||||
s.mu.Lock()
|
||||
switch s.suspendCount--; {
|
||||
case s.suspendCount == 0:
|
||||
s.mr = s.mw
|
||||
s.mu.Unlock()
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
case s.suspendCount < 0:
|
||||
s.tb.Fatal("negative suspendCount")
|
||||
default:
|
||||
s.mu.Unlock()
|
||||
s.storeLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SetBooleans sets the specified boolean settings in s.
|
||||
func (s *TestStore) SetBooleans(settings ...TestSetting[bool]) {
|
||||
s.storeLock.Lock()
|
||||
for _, setting := range settings {
|
||||
if setting.Key == "" {
|
||||
s.tb.Fatal("empty keys disallowed")
|
||||
}
|
||||
s.mu.Lock()
|
||||
if setting.Error != nil {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Error))
|
||||
} else {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Value))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
// SetUInt64s sets the specified integer settings in s.
|
||||
func (s *TestStore) SetUInt64s(settings ...TestSetting[uint64]) {
|
||||
s.storeLock.Lock()
|
||||
for _, setting := range settings {
|
||||
if setting.Key == "" {
|
||||
s.tb.Fatal("empty keys disallowed")
|
||||
}
|
||||
s.mu.Lock()
|
||||
if setting.Error != nil {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Error))
|
||||
} else {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Value))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
// SetStrings sets the specified string settings in s.
|
||||
func (s *TestStore) SetStrings(settings ...TestSetting[string]) {
|
||||
s.storeLock.Lock()
|
||||
for _, setting := range settings {
|
||||
if setting.Key == "" {
|
||||
s.tb.Fatal("empty keys disallowed")
|
||||
}
|
||||
s.mu.Lock()
|
||||
if setting.Error != nil {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Error))
|
||||
} else {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Value))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
// SetStrings sets the specified string list settings in s.
|
||||
func (s *TestStore) SetStringLists(settings ...TestSetting[[]string]) {
|
||||
s.storeLock.Lock()
|
||||
for _, setting := range settings {
|
||||
if setting.Key == "" {
|
||||
s.tb.Fatal("empty keys disallowed")
|
||||
}
|
||||
s.mu.Lock()
|
||||
if setting.Error != nil {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Error))
|
||||
} else {
|
||||
mak.Set(&s.mw, setting.Key, any(setting.Value))
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
// Delete deletes the specified settings from s.
|
||||
func (s *TestStore) Delete(keys ...setting.Key) {
|
||||
s.storeLock.Lock()
|
||||
for _, key := range keys {
|
||||
s.mu.Lock()
|
||||
delete(s.mw, key)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
// Clear deletes all settings from s.
|
||||
func (s *TestStore) Clear() {
|
||||
s.storeLock.Lock()
|
||||
s.mu.Lock()
|
||||
clear(s.mw)
|
||||
s.mu.Unlock()
|
||||
s.storeLock.Unlock()
|
||||
s.NotifyPolicyChanged()
|
||||
}
|
||||
|
||||
func (s *TestStore) NotifyPolicyChanged() {
|
||||
s.mu.RLock()
|
||||
if s.suspendCount != 0 {
|
||||
s.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
cbs := slicesx.MapValues(s.cbs)
|
||||
s.mu.RUnlock()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(cbs))
|
||||
for _, cb := range cbs {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
cb()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Close closes s, notifying its users that it has expired.
|
||||
func (s *TestStore) Close() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.closed {
|
||||
close(s.done)
|
||||
s.closed = true
|
||||
}
|
||||
}
|
||||
|
||||
// Done implements [Expirable].
|
||||
func (s *TestStore) Done() <-chan struct{} {
|
||||
return s.done
|
||||
}
|
||||
152
vendor/tailscale.com/util/syspolicy/syspolicy.go
generated
vendored
152
vendor/tailscale.com/util/syspolicy/syspolicy.go
generated
vendored
@@ -1,51 +1,82 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package syspolicy provides functions to retrieve system settings of a device.
|
||||
// Package syspolicy facilitates retrieval of the current policy settings
|
||||
// applied to the device or user and receiving notifications when the policy
|
||||
// changes.
|
||||
//
|
||||
// It provides functions that return specific policy settings by their unique
|
||||
// [setting.Key]s, such as [GetBoolean], [GetUint64], [GetString],
|
||||
// [GetStringArray], [GetPreferenceOption], [GetVisibility] and [GetDuration].
|
||||
package syspolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/syspolicy/internal/loggerx"
|
||||
"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)
|
||||
}
|
||||
|
||||
// MustRegisterStoreForTest is like [rsop.RegisterStoreForTest], but it fails the test if the store could not be registered.
|
||||
func MustRegisterStoreForTest(tb TB, name string, scope setting.PolicyScope, store source.Store) *rsop.StoreRegistration {
|
||||
tb.Helper()
|
||||
reg, err := rsop.RegisterStoreForTest(tb, name, scope, store)
|
||||
if err != nil {
|
||||
tb.Fatalf("Failed to register policy store %q as a %v policy source: %v", name, scope, err)
|
||||
}
|
||||
return reg
|
||||
}
|
||||
|
||||
// GetString returns a string policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetString(key Key, defaultValue string) (string, error) {
|
||||
markHandlerInUse()
|
||||
v, err := handler.ReadString(string(key))
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return v, err
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetUint64 returns a numeric policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetUint64(key Key, defaultValue uint64) (uint64, error) {
|
||||
markHandlerInUse()
|
||||
v, err := handler.ReadUInt64(string(key))
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return v, err
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetBoolean returns a boolean policy setting with the specified key,
|
||||
// or defaultValue if it does not exist.
|
||||
func GetBoolean(key Key, defaultValue bool) (bool, error) {
|
||||
markHandlerInUse()
|
||||
v, err := handler.ReadBoolean(string(key))
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return v, err
|
||||
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 Key, defaultValue []string) ([]string, error) {
|
||||
markHandlerInUse()
|
||||
v, err := handler.ReadStringArray(string(key))
|
||||
if errors.Is(err, ErrNoSuchKey) {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return v, err
|
||||
return getCurrentPolicySettingValue(key, defaultValue)
|
||||
}
|
||||
|
||||
// GetPreferenceOption loads a policy from the registry that can be
|
||||
@@ -55,13 +86,7 @@ func GetStringArray(key Key, defaultValue []string) ([]string, error) {
|
||||
// "always" and "never" remove the user's ability to make a selection. If not
|
||||
// present or set to a different value, "user-decides" is the default.
|
||||
func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
|
||||
s, err := GetString(name, "user-decides")
|
||||
if err != nil {
|
||||
return setting.ShowChoiceByPolicy, err
|
||||
}
|
||||
var opt setting.PreferenceOption
|
||||
err = opt.UnmarshalText([]byte(s))
|
||||
return opt, err
|
||||
return getCurrentPolicySettingValue(name, setting.ShowChoiceByPolicy)
|
||||
}
|
||||
|
||||
// GetVisibility loads a policy from the registry that can be managed
|
||||
@@ -70,13 +95,7 @@ func GetPreferenceOption(name Key) (setting.PreferenceOption, error) {
|
||||
// true) or "hide" (return true). If not present or set to a different value,
|
||||
// "show" (return false) is the default.
|
||||
func GetVisibility(name Key) (setting.Visibility, error) {
|
||||
s, err := GetString(name, "show")
|
||||
if err != nil {
|
||||
return setting.VisibleByPolicy, err
|
||||
}
|
||||
var visibility setting.Visibility
|
||||
visibility.UnmarshalText([]byte(s))
|
||||
return visibility, nil
|
||||
return getCurrentPolicySettingValue(name, setting.VisibleByPolicy)
|
||||
}
|
||||
|
||||
// GetDuration loads a policy from the registry that can be managed
|
||||
@@ -85,15 +104,58 @@ func GetVisibility(name Key) (setting.Visibility, error) {
|
||||
// understands. If the registry value is "" or can not be processed,
|
||||
// defaultValue is returned instead.
|
||||
func GetDuration(name Key, defaultValue time.Duration) (time.Duration, error) {
|
||||
opt, err := GetString(name, "")
|
||||
if opt == "" || err != nil {
|
||||
return defaultValue, err
|
||||
d, err := getCurrentPolicySettingValue(name, defaultValue)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
v, err := time.ParseDuration(opt)
|
||||
if err != nil || v < 0 {
|
||||
if d < 0 {
|
||||
return defaultValue, nil
|
||||
}
|
||||
return v, 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 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
|
||||
|
||||
92
vendor/tailscale.com/util/syspolicy/syspolicy_windows.go
generated
vendored
Normal file
92
vendor/tailscale.com/util/syspolicy/syspolicy_windows.go
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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 internal.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
|
||||
}
|
||||
62
vendor/tailscale.com/util/uniq/slice.go
generated
vendored
62
vendor/tailscale.com/util/uniq/slice.go
generated
vendored
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package uniq provides removal of adjacent duplicate elements in slices.
|
||||
// It is similar to the unix command uniq.
|
||||
package uniq
|
||||
|
||||
// ModifySlice removes adjacent duplicate elements from the given slice. It
|
||||
// adjusts the length of the slice appropriately and zeros the tail.
|
||||
//
|
||||
// ModifySlice does O(len(*slice)) operations.
|
||||
func ModifySlice[E comparable](slice *[]E) {
|
||||
// Remove duplicates
|
||||
dst := 0
|
||||
for i := 1; i < len(*slice); i++ {
|
||||
if (*slice)[i] == (*slice)[dst] {
|
||||
continue
|
||||
}
|
||||
dst++
|
||||
(*slice)[dst] = (*slice)[i]
|
||||
}
|
||||
|
||||
// Zero out the elements we removed at the end of the slice
|
||||
end := dst + 1
|
||||
var zero E
|
||||
for i := end; i < len(*slice); i++ {
|
||||
(*slice)[i] = zero
|
||||
}
|
||||
|
||||
// Truncate the slice
|
||||
if end < len(*slice) {
|
||||
*slice = (*slice)[:end]
|
||||
}
|
||||
}
|
||||
|
||||
// ModifySliceFunc is the same as ModifySlice except that it allows using a
|
||||
// custom comparison function.
|
||||
//
|
||||
// eq should report whether the two provided elements are equal.
|
||||
func ModifySliceFunc[E any](slice *[]E, eq func(i, j E) bool) {
|
||||
// Remove duplicates
|
||||
dst := 0
|
||||
for i := 1; i < len(*slice); i++ {
|
||||
if eq((*slice)[dst], (*slice)[i]) {
|
||||
continue
|
||||
}
|
||||
dst++
|
||||
(*slice)[dst] = (*slice)[i]
|
||||
}
|
||||
|
||||
// Zero out the elements we removed at the end of the slice
|
||||
end := dst + 1
|
||||
var zero E
|
||||
for i := end; i < len(*slice); i++ {
|
||||
(*slice)[i] = zero
|
||||
}
|
||||
|
||||
// Truncate the slice
|
||||
if end < len(*slice) {
|
||||
*slice = (*slice)[:end]
|
||||
}
|
||||
}
|
||||
85
vendor/tailscale.com/util/usermetric/metrics.go
generated
vendored
Normal file
85
vendor/tailscale.com/util/usermetric/metrics.go
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This file contains user-facing metrics that are used by multiple packages.
|
||||
// Use it to define more common metrics. Any changes to the registry and
|
||||
// metric types should be in usermetric.go.
|
||||
|
||||
package usermetric
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"tailscale.com/metrics"
|
||||
)
|
||||
|
||||
// Metrics contains user-facing metrics that are used by multiple packages.
|
||||
type Metrics struct {
|
||||
initOnce sync.Once
|
||||
|
||||
droppedPacketsInbound *metrics.MultiLabelMap[DropLabels]
|
||||
droppedPacketsOutbound *metrics.MultiLabelMap[DropLabels]
|
||||
}
|
||||
|
||||
// DropReason is the reason why a packet was dropped.
|
||||
type DropReason string
|
||||
|
||||
const (
|
||||
// ReasonACL means that the packet was not permitted by ACL.
|
||||
ReasonACL DropReason = "acl"
|
||||
|
||||
// ReasonMulticast means that the packet was dropped because it was a multicast packet.
|
||||
ReasonMulticast DropReason = "multicast"
|
||||
|
||||
// ReasonLinkLocalUnicast means that the packet was dropped because it was a link-local unicast packet.
|
||||
ReasonLinkLocalUnicast DropReason = "link_local_unicast"
|
||||
|
||||
// ReasonTooShort means that the packet was dropped because it was a bad packet,
|
||||
// this could be due to a short packet.
|
||||
ReasonTooShort DropReason = "too_short"
|
||||
|
||||
// ReasonFragment means that the packet was dropped because it was an IP fragment.
|
||||
ReasonFragment DropReason = "fragment"
|
||||
|
||||
// ReasonUnknownProtocol means that the packet was dropped because it was an unknown protocol.
|
||||
ReasonUnknownProtocol DropReason = "unknown_protocol"
|
||||
|
||||
// ReasonError means that the packet was dropped because of an error.
|
||||
ReasonError DropReason = "error"
|
||||
)
|
||||
|
||||
// DropLabels contains common label(s) for dropped packet counters.
|
||||
type DropLabels struct {
|
||||
Reason DropReason
|
||||
}
|
||||
|
||||
// initOnce initializes the common metrics.
|
||||
func (r *Registry) initOnce() {
|
||||
r.m.initOnce.Do(func() {
|
||||
r.m.droppedPacketsInbound = NewMultiLabelMapWithRegistry[DropLabels](
|
||||
r,
|
||||
"tailscaled_inbound_dropped_packets_total",
|
||||
"counter",
|
||||
"Counts the number of dropped packets received by the node from other peers",
|
||||
)
|
||||
r.m.droppedPacketsOutbound = NewMultiLabelMapWithRegistry[DropLabels](
|
||||
r,
|
||||
"tailscaled_outbound_dropped_packets_total",
|
||||
"counter",
|
||||
"Counts the number of packets dropped while being sent to other peers",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// DroppedPacketsOutbound returns the outbound dropped packet metric, creating it
|
||||
// if necessary.
|
||||
func (r *Registry) DroppedPacketsOutbound() *metrics.MultiLabelMap[DropLabels] {
|
||||
r.initOnce()
|
||||
return r.m.droppedPacketsOutbound
|
||||
}
|
||||
|
||||
// DroppedPacketsInbound returns the inbound dropped packet metric.
|
||||
func (r *Registry) DroppedPacketsInbound() *metrics.MultiLabelMap[DropLabels] {
|
||||
r.initOnce()
|
||||
return r.m.droppedPacketsInbound
|
||||
}
|
||||
14
vendor/tailscale.com/util/usermetric/usermetric.go
generated
vendored
14
vendor/tailscale.com/util/usermetric/usermetric.go
generated
vendored
@@ -14,11 +14,15 @@ import (
|
||||
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// Registry tracks user-facing metrics of various Tailscale subsystems.
|
||||
type Registry struct {
|
||||
vars expvar.Map
|
||||
|
||||
// m contains common metrics owned by the registry.
|
||||
m Metrics
|
||||
}
|
||||
|
||||
// NewMultiLabelMapWithRegistry creates and register a new
|
||||
@@ -103,3 +107,13 @@ func (r *Registry) String() string {
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Metrics returns the name of all the metrics in the registry.
|
||||
func (r *Registry) MetricNames() []string {
|
||||
ret := make(set.Set[string])
|
||||
r.vars.Do(func(kv expvar.KeyValue) {
|
||||
ret.Add(kv.Key)
|
||||
})
|
||||
|
||||
return ret.Slice()
|
||||
}
|
||||
|
||||
38
vendor/tailscale.com/util/winutil/gp/policylock_windows.go
generated
vendored
38
vendor/tailscale.com/util/winutil/gp/policylock_windows.go
generated
vendored
@@ -48,10 +48,35 @@ type policyLockResult struct {
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrInvalidLockState is returned by (*PolicyLock).Lock if the lock has a zero value or has already been closed.
|
||||
// ErrInvalidLockState is returned by [PolicyLock.Lock] if the lock has a zero value or has already been closed.
|
||||
ErrInvalidLockState = errors.New("the lock has not been created or has already been closed")
|
||||
// ErrLockRestricted is returned by [PolicyLock.Lock] if the lock cannot be acquired due to a restriction in place,
|
||||
// such as when [RestrictPolicyLocks] has been called.
|
||||
ErrLockRestricted = errors.New("the lock cannot be acquired due to a restriction in place")
|
||||
)
|
||||
|
||||
var policyLockRestricted atomic.Int32
|
||||
|
||||
// RestrictPolicyLocks forces all [PolicyLock.Lock] calls to return [ErrLockRestricted]
|
||||
// until the returned function is called to remove the restriction.
|
||||
//
|
||||
// It is safe to call the returned function multiple times, but the restriction will only
|
||||
// be removed once. If [RestrictPolicyLocks] is called multiple times, each call must be
|
||||
// matched by a corresponding call to the returned function to fully remove the restrictions.
|
||||
//
|
||||
// It is primarily used to prevent certain deadlocks, such as when tailscaled attempts to acquire
|
||||
// a policy lock during startup. If the service starts due to Tailscale being installed by GPSI,
|
||||
// the write lock will be held by the Group Policy service throughout the installation,
|
||||
// preventing tailscaled from acquiring the read lock. Since Group Policy waits for the installation
|
||||
// to complete, and therefore for tailscaled to start, before releasing the write lock, this scenario
|
||||
// would result in a deadlock. See tailscale/tailscale#14416 for more information.
|
||||
func RestrictPolicyLocks() (removeRestriction func()) {
|
||||
policyLockRestricted.Add(1)
|
||||
return sync.OnceFunc(func() {
|
||||
policyLockRestricted.Add(-1)
|
||||
})
|
||||
}
|
||||
|
||||
// NewMachinePolicyLock creates a PolicyLock that facilitates pausing the
|
||||
// application of computer policy. To avoid deadlocks when acquiring both
|
||||
// machine and user locks, acquire the user lock before the machine lock.
|
||||
@@ -103,13 +128,18 @@ func NewUserPolicyLock(token windows.Token) (*PolicyLock, error) {
|
||||
}
|
||||
|
||||
// Lock locks l.
|
||||
// It returns ErrNotInitialized if l has a zero value or has already been closed,
|
||||
// or an Errno if the underlying Group Policy lock cannot be acquired.
|
||||
// It returns [ErrInvalidLockState] if l has a zero value or has already been closed,
|
||||
// [ErrLockRestricted] if the lock cannot be acquired due to a restriction in place,
|
||||
// or a [syscall.Errno] if the underlying Group Policy lock cannot be acquired.
|
||||
//
|
||||
// As a special case, it fails with windows.ERROR_ACCESS_DENIED
|
||||
// As a special case, it fails with [windows.ERROR_ACCESS_DENIED]
|
||||
// if l is a user policy lock, and the corresponding user is not logged in
|
||||
// interactively at the time of the call.
|
||||
func (l *PolicyLock) Lock() error {
|
||||
if policyLockRestricted.Load() > 0 {
|
||||
return ErrLockRestricted
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.lockCnt.Add(2)&1 == 0 {
|
||||
|
||||
Reference in New Issue
Block a user