mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-12 11:12:06 +01:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
530 lines
16 KiB
Go
530 lines
16 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// 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/pkey"
|
|
"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 pkey.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 pkey.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 pkey.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 [pkey.ErrNotConfigured] if the policy setting does not exist.
|
|
func (ps *PlatformPolicyStore) ReadStringArray(key pkey.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 [pkey.Key].
|
|
// The [pkey.Key] format allows grouping settings into nested categories using one
|
|
// or more [pkey.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 [pkey.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 [pkey.KeyPathSeparator]s in the key, the policy setting value
|
|
// is meant to be stored directly under {HKLM,HKCU}\Software\Policies\Tailscale.
|
|
func splitSettingKey(key pkey.Key) (path, valueName string) {
|
|
if idx := strings.LastIndexByte(string(key), pkey.KeyPathSeparator); idx != -1 {
|
|
path = strings.ReplaceAll(string(key[:idx]), string(pkey.KeyPathSeparator), `\`)
|
|
valueName = string(key[idx+1:])
|
|
return path, valueName
|
|
}
|
|
return "", string(key)
|
|
}
|
|
|
|
func getPolicyValue[T any](ps *PlatformPolicyStore, key pkey.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
|
|
}
|