WIP: rebase fork onto upstream/main (v1.103.0) #15
@@ -166,7 +166,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/util/syspolicy/pkey from tailscale.com/ipn+
|
||||
tailscale.com/util/syspolicy/policyclient from tailscale.com/ipn
|
||||
tailscale.com/util/syspolicy/ptype from tailscale.com/util/syspolicy/policyclient+
|
||||
tailscale.com/util/syspolicy/setting from tailscale.com/client/local
|
||||
tailscale.com/util/syspolicy/setting from tailscale.com/client/local+
|
||||
tailscale.com/util/testenv from tailscale.com/net/bakedroots+
|
||||
tailscale.com/util/usermetric from tailscale.com/health
|
||||
tailscale.com/util/vizerror from tailscale.com/tailcfg+
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
)
|
||||
|
||||
type State int
|
||||
@@ -173,6 +174,18 @@ const (
|
||||
// not request it.
|
||||
NotifyInProcessNoDisconnect NotifyWatchOpt = 1 << 16
|
||||
|
||||
// NotifySysPolicyChanges, if set, causes the first Notify message, which is sent
|
||||
// immediately, to contain the current effective [setting.Snapshot] in
|
||||
// [Notify.Policy]. [Notify.Policy] is included in subsequent messages whenever
|
||||
// the effective policy changes.
|
||||
//
|
||||
// The snapshot is scoped to the connected user's identity (on Windows,
|
||||
// derived from the named-pipe token's SID).
|
||||
//
|
||||
// The [setting.Snapshot] that is delivered is a full snapshot on every
|
||||
// change.
|
||||
NotifySysPolicyChanges NotifyWatchOpt = 1 << 17
|
||||
|
||||
// NotifyPeerWireGuardState, if set, opts the watcher into
|
||||
// WireGuard session state notifications via [Notify.PeerState].
|
||||
// The first Notify sent to the watcher includes a dump of current
|
||||
@@ -220,6 +233,7 @@ func (o NotifyWatchOpt) String() string {
|
||||
try(NotifyInitialStatus, "NotifyInitialStatus")
|
||||
try(NotifyPeerPatches, "NotifyPeerPatches")
|
||||
try(NotifyInProcessNoDisconnect, "NotifyInProcessNoDisconnect")
|
||||
try(NotifySysPolicyChanges, "NotifySysPolicyChanges")
|
||||
try(NotifyPeerWireGuardState, "NotifyPeerWireGuardState")
|
||||
|
||||
if mask != o {
|
||||
@@ -453,6 +467,13 @@ type Notify struct {
|
||||
// be the best exit node for the current network conditions.
|
||||
SuggestedExitNode *tailcfg.StableNodeID `json:",omitzero"`
|
||||
|
||||
// Policy, if non-nil, is the effective policy snapshot for the
|
||||
// connected user. It is scoped per-user: per-user policy settings
|
||||
// are merged with device-wide settings, with device-wide taking
|
||||
// precedence. Sent initially when [NotifySysPolicyChanges] is set,
|
||||
// and on change thereafter.
|
||||
Policy *policyclient.PolicySnapshot `json:",omitzero"`
|
||||
|
||||
// type is mirrored in xcode/IPN/Core/LocalAPI/Model/LocalAPIModel.swift
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -247,5 +247,6 @@ func isNotableNotify(n *ipn.Notify) bool {
|
||||
len(n.IncomingFiles) > 0 ||
|
||||
len(n.OutgoingFiles) > 0 ||
|
||||
n.FilesWaiting != nil ||
|
||||
n.SuggestedExitNode != nil
|
||||
n.SuggestedExitNode != nil ||
|
||||
n.Policy != nil
|
||||
}
|
||||
|
||||
+45
-3
@@ -2330,8 +2330,8 @@ func (b *LocalBackend) applyExitNodeSysPolicyLocked(prefs *ipn.Prefs) (anyChange
|
||||
// registerSysPolicyWatch subscribes to syspolicy change notifications
|
||||
// and immediately applies the effective syspolicy settings to the current profile.
|
||||
func (b *LocalBackend) registerSysPolicyWatch() (unregister func(), err error) {
|
||||
if unregister, err = b.polc.RegisterChangeCallback(b.sysPolicyChanged); err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: LocalBacked failed to register policy change callback: %v", err)
|
||||
if unregister, err = b.polc.RegisterChangeCallback("", b.sysPolicyChanged); err != nil {
|
||||
return nil, fmt.Errorf("syspolicy: LocalBackend failed to register policy change callback: %v", err)
|
||||
}
|
||||
if prefs, anyChange := b.reconcilePrefs(); anyChange {
|
||||
b.logf("syspolicy: changed initial profile prefs: %v", prefs.Pretty())
|
||||
@@ -2392,6 +2392,28 @@ func (b *LocalBackend) sysPolicyChanged(policy policyclient.PolicyChange) {
|
||||
}
|
||||
}
|
||||
|
||||
// sysPolicyChangedForSession is called when the effective policy for a session's
|
||||
// user changes. It fetches the new snapshot and sends it to that session.
|
||||
func (b *LocalBackend) sysPolicyChangedForSession(sess *watchSession) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
snapshot, err := b.polc.GetPolicySnapshot("")
|
||||
if err != nil || snapshot == nil {
|
||||
return
|
||||
}
|
||||
|
||||
n := ipn.Notify{Policy: snapshot, Version: version.Long()}
|
||||
for _, f := range b.extHost.Hooks().MutateNotifyLocked {
|
||||
f(&n)
|
||||
}
|
||||
nForSess := b.notifyForSessionLocked(sess, &n)
|
||||
select {
|
||||
case sess.ch <- nForSess:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ controlclient.NetmapDeltaUpdater = (*LocalBackend)(nil)
|
||||
_ controlclient.PacketFilterUpdater = (*LocalBackend)(nil)
|
||||
@@ -3711,7 +3733,10 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
deadlockDone := b.CheckDeadlocks()
|
||||
b.mu.Lock()
|
||||
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap | ipn.NotifyInitialStatus | ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode | ipn.NotifyInitialClientVersion | ipn.NotifyPeerWireGuardState
|
||||
const initialBits = ipn.NotifyInitialState | ipn.NotifyInitialPrefs |
|
||||
ipn.NotifyInitialNetMap | ipn.NotifyInitialStatus |
|
||||
ipn.NotifyInitialDriveShares | ipn.NotifyInitialSuggestedExitNode |
|
||||
ipn.NotifyInitialClientVersion | ipn.NotifySysPolicyChanges | ipn.NotifyPeerWireGuardState
|
||||
if mask&initialBits != 0 {
|
||||
cn := b.currentNode()
|
||||
ini = &ipn.Notify{Version: version.Long()}
|
||||
@@ -3755,6 +3780,13 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
ini.ClientVersion = b.lastClientVersion
|
||||
}
|
||||
}
|
||||
if mask&ipn.NotifySysPolicyChanges != 0 {
|
||||
var err error
|
||||
ini.Policy, err = b.polc.GetPolicySnapshot("")
|
||||
if err != nil {
|
||||
b.logf("syspolicy: GetPolicySnapshot(\"\"): %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
@@ -3775,6 +3807,16 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
||||
b.mu.Unlock()
|
||||
deadlockDone()
|
||||
|
||||
if mask&ipn.NotifySysPolicyChanges != 0 {
|
||||
if unreg, err := b.polc.RegisterChangeCallback("", func(_ policyclient.PolicyChange) {
|
||||
b.sysPolicyChangedForSession(session)
|
||||
}); err == nil {
|
||||
defer unreg()
|
||||
} else {
|
||||
b.logf("syspolicy: RegisterChangeCallback(\"\"): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
metricCurrentWatchIPNBus.Add(1)
|
||||
defer metricCurrentWatchIPNBus.Add(-1)
|
||||
|
||||
|
||||
@@ -71,7 +71,10 @@ import (
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/syspolicy/pkey"
|
||||
"tailscale.com/util/syspolicy/policyclient"
|
||||
"tailscale.com/util/syspolicy/policytest"
|
||||
"tailscale.com/util/syspolicy/rsop"
|
||||
"tailscale.com/util/syspolicy/setting"
|
||||
"tailscale.com/util/syspolicy/source"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -8481,6 +8484,153 @@ func toStrings[T ~string](in []T) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func TestWatchNotificationsInitialPolicy(t *testing.T) {
|
||||
setting.SetDefinitionsForTest(t,
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
)
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
|
||||
nw := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nw.check()
|
||||
|
||||
nw2 := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw2.watch(0, nil, unexpectedPolicy)
|
||||
nw2.check()
|
||||
}
|
||||
|
||||
func TestPolicyChangeNotifiesWatcher(t *testing.T) {
|
||||
setting.SetDefinitionsForTest(t,
|
||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||
)
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
if err := lb.Start(ipn.Options{}); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
nw := newNotificationWatcher(t, lb, &ipnauth.TestActor{})
|
||||
nw.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
wantPolicyWithSetting(pkey.AdminConsoleVisibility, "hide"),
|
||||
})
|
||||
|
||||
store.SetStrings(source.TestSettingOf(pkey.AdminConsoleVisibility, "hide"))
|
||||
|
||||
nw.check()
|
||||
}
|
||||
|
||||
func TestPolicyNotifyPerUser(t *testing.T) {
|
||||
store := source.NewTestStore(t)
|
||||
rsop.RegisterStoreForTest(t, "TestStore", setting.DeviceScope, store)
|
||||
|
||||
sys := tsd.NewSystem()
|
||||
sys.PolicyClient.Set(testPolicyClient{})
|
||||
lb := newTestLocalBackendWithSys(t, sys)
|
||||
|
||||
actorA := &ipnauth.TestActor{UID: "S-1-5-21-1001"}
|
||||
actorB := &ipnauth.TestActor{UID: "S-1-5-21-1002"}
|
||||
|
||||
nwA := newNotificationWatcher(t, lb, actorA)
|
||||
nwA.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nwA.check()
|
||||
|
||||
nwB := newNotificationWatcher(t, lb, actorB)
|
||||
nwB.watch(ipn.NotifySysPolicyChanges, []wantedNotification{
|
||||
wantPolicyNotify(),
|
||||
})
|
||||
nwB.check()
|
||||
|
||||
nwNoPolicy := newNotificationWatcher(t, lb, actorA)
|
||||
nwNoPolicy.watch(0, nil, unexpectedPolicy)
|
||||
|
||||
store.SetStrings(source.TestSettingOf(pkey.AdminConsoleVisibility, "hide"))
|
||||
|
||||
nwNoPolicy.check()
|
||||
}
|
||||
|
||||
func wantPolicyNotify() wantedNotification {
|
||||
return wantedNotification{
|
||||
name: "Policy",
|
||||
cond: func(_ testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
return n.Policy != nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func wantPolicyWithSetting(key pkey.Key, value string) wantedNotification {
|
||||
return wantedNotification{
|
||||
name: fmt.Sprintf("Policy-%s=%s", key, value),
|
||||
cond: func(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
if n.Policy == nil {
|
||||
return false
|
||||
}
|
||||
if n.Policy == nil {
|
||||
return false
|
||||
}
|
||||
got := n.Policy.Get(key)
|
||||
if got == nil {
|
||||
return false
|
||||
}
|
||||
if fmt.Sprint(got) != value {
|
||||
t.Errorf("Policy[%s] = %v (%T); want %q", key, got, got, value)
|
||||
}
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func unexpectedPolicy(t testing.TB, _ ipnauth.Actor, n *ipn.Notify) bool {
|
||||
if n.Policy != nil {
|
||||
t.Errorf("unexpected Policy notification")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type testPolicyClient struct {
|
||||
policyclient.NoPolicyClient
|
||||
}
|
||||
|
||||
func (testPolicyClient) GetPolicySnapshot(uid string) (*policyclient.PolicySnapshot, error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
p, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Get(), nil
|
||||
}
|
||||
|
||||
func (testPolicyClient) RegisterChangeCallback(uid string, cb func(policyclient.PolicyChange)) (func(), error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
p, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return func() {}, err
|
||||
}
|
||||
return p.RegisterChangeCallback(func(change policyclient.PolicyChange) {
|
||||
cb(change)
|
||||
}), nil
|
||||
}
|
||||
|
||||
type textUpdate struct {
|
||||
Advertise []string
|
||||
Unadvertise []string
|
||||
|
||||
@@ -78,7 +78,7 @@ func NewOSConfigurator(logf logger.Logf, health *health.Tracker, bus *eventbus.B
|
||||
}
|
||||
|
||||
var err error
|
||||
if ret.unregisterPolicyChangeCb, err = polc.RegisterChangeCallback(ret.sysPolicyChanged); err != nil {
|
||||
if ret.unregisterPolicyChangeCb, err = polc.RegisterChangeCallback("", ret.sysPolicyChanged); err != nil {
|
||||
logf("error registering policy change callback: %v", err) // non-fatal
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_syspolicy
|
||||
|
||||
package policyclient
|
||||
|
||||
import "tailscale.com/util/syspolicy/setting"
|
||||
|
||||
// PolicySnapshot is an alias for [settings.Snapshot] unless syspolicy is omitted
|
||||
// from the build.
|
||||
type policySnapshot = setting.Snapshot
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & contributors
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_syspolicy
|
||||
|
||||
package policyclient
|
||||
|
||||
// PolicySnapshot is a stub when syspolicy is omitted from the build.
|
||||
type policySnapshot struct{}
|
||||
@@ -62,11 +62,21 @@ type Client interface {
|
||||
HasAnyOf(keys ...pkey.Key) (bool, error)
|
||||
|
||||
// RegisterChangeCallback registers a callback function that will be called
|
||||
// whenever a policy change is detected. It returns a function to unregister
|
||||
// the callback and an error if the registration fails.
|
||||
RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error)
|
||||
// whenever a policy change is detected. If uid is empty, the callback fires
|
||||
// for device-scope policy changes. If uid is non-empty, it fires for changes
|
||||
// to that user's effective policy (device + user policies merged). It returns
|
||||
// a function to unregister the callback and an error if the registration fails.
|
||||
RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error)
|
||||
|
||||
// GetPolicySnapshot returns the effective policy snapshot for the given user ID.
|
||||
// If uid is empty, returns the default-scope policy. Returns nil, nil if policy
|
||||
// snapshots are not supported.
|
||||
GetPolicySnapshot(uid string) (*PolicySnapshot, error)
|
||||
}
|
||||
|
||||
// PolicySnapshot is an alias for [setting.Snapshot] unless syspolicy is omitted from the build.
|
||||
type PolicySnapshot = policySnapshot
|
||||
|
||||
// Get returns a non-nil [Client] implementation as a function of the
|
||||
// build tags. It returns a no-op implementation if the full syspolicy
|
||||
// package is omitted from the build, or in tests.
|
||||
@@ -140,6 +150,10 @@ func (NoPolicyClient) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
|
||||
func (NoPolicyClient) SetDebugLoggingEnabled(enabled bool) {}
|
||||
|
||||
func (NoPolicyClient) RegisterChangeCallback(cb func(PolicyChange)) (unregister func(), err error) {
|
||||
func (NoPolicyClient) RegisterChangeCallback(uid string, cb func(PolicyChange)) (unregister func(), err error) {
|
||||
return func() {}, nil
|
||||
}
|
||||
|
||||
func (NoPolicyClient) GetPolicySnapshot(uid string) (*PolicySnapshot, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func (c Config) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c Config) RegisterChangeCallback(callback func(policyclient.PolicyChange)) (func(), error) {
|
||||
func (c Config) RegisterChangeCallback(uid string, callback func(policyclient.PolicyChange)) (func(), error) {
|
||||
w, ok := c[watchersKey].(*watchers)
|
||||
if !ok {
|
||||
return func() {}, nil
|
||||
@@ -242,3 +242,7 @@ func (c Config) RegisterChangeCallback(callback func(policyclient.PolicyChange))
|
||||
}
|
||||
|
||||
func (sp Config) SetDebugLoggingEnabled(enabled bool) {}
|
||||
|
||||
func (c Config) GetPolicySnapshot(uid string) (*policyclient.PolicySnapshot, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -129,8 +129,12 @@ func getDuration(name pkey.Key, defaultValue time.Duration) (time.Duration, erro
|
||||
|
||||
// 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())
|
||||
func registerChangeCallback(uid string, cb rsop.PolicyChangeCallback) (unregister func(), err error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
effective, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -259,6 +263,18 @@ func (globalSyspolicy) HasAnyOf(keys ...pkey.Key) (bool, error) {
|
||||
return hasAnyOf(keys...)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) RegisterChangeCallback(cb func(policyclient.PolicyChange)) (unregister func(), err error) {
|
||||
return registerChangeCallback(cb)
|
||||
func (globalSyspolicy) RegisterChangeCallback(uid string, cb func(policyclient.PolicyChange)) (unregister func(), err error) {
|
||||
return registerChangeCallback(uid, cb)
|
||||
}
|
||||
|
||||
func (globalSyspolicy) GetPolicySnapshot(uid string) (*policyclient.PolicySnapshot, error) {
|
||||
scope := setting.DefaultScope()
|
||||
if uid != "" {
|
||||
scope = setting.UserScopeOf(uid)
|
||||
}
|
||||
p, err := rsop.PolicyFor(scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.Get(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user