WIP: rebase fork onto upstream/main (v1.103.0) #15

Closed
codinget wants to merge 670 commits from webnet into save/webnet-2026-07-29
11 changed files with 284 additions and 15 deletions
Showing only changes of commit 07cefc083d - Show all commits
+1 -1
View File
@@ -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+
+21
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+150
View File
@@ -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
+1 -1
View File
@@ -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{}
+18 -4
View File
@@ -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
}
+5 -1
View File
@@ -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
}
+20 -4
View File
@@ -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
}