Merge bd0dadd771cb67f9d344e820c7111a51cd951e17 into b3455fa99a5e8d07133d5140017ec7c49f032a07

This commit is contained in:
Nick Khyl 2025-03-24 17:10:28 -05:00 committed by GitHub
commit 90f892250a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 416 additions and 72 deletions

View File

@ -814,7 +814,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/internal/client/tailscale from tailscale.com/cmd/k8s-operator
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/local+
tailscale.com/ipn/auditlog from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn/conffile from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+

View File

@ -271,7 +271,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
tailscale.com/ipn from tailscale.com/client/local+
tailscale.com/ipn/auditlog from tailscale.com/ipn/ipnlocal+
W tailscale.com/ipn/auditlog from tailscale.com/cmd/tailscaled
tailscale.com/ipn/conffile from tailscale.com/cmd/tailscaled+
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+

View File

@ -44,6 +44,7 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/drive/driveimpl"
"tailscale.com/envknob"
_ "tailscale.com/ipn/auditlog"
"tailscale.com/ipn/desktop"
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"

View File

@ -112,7 +112,7 @@ func NewLogger(opts Opts) *Logger {
al := &Logger{
retryLimit: opts.RetryLimit,
logf: logger.WithPrefix(opts.Logf, "auditlog: "),
logf: opts.Logf,
store: opts.Store,
flusher: make(chan struct{}, 1),
done: make(chan struct{}),
@ -138,7 +138,9 @@ func (al *Logger) FlushAndStop(ctx context.Context) {
func (al *Logger) SetProfileID(profileID ipn.ProfileID) error {
al.mu.Lock()
defer al.mu.Unlock()
if al.profileID != "" {
// It's not an error to call SetProfileID more than once
// with the same [ipn.ProfileID].
if al.profileID != "" && al.profileID != profileID {
return errors.New("profileID already set")
}

View File

@ -184,8 +184,11 @@ func TestChangeProfileId(t *testing.T) {
})
c.Assert(al.SetProfileID("test"), qt.IsNil)
// Changing a profile ID must fail
c.Assert(al.SetProfileID("test"), qt.IsNotNil)
// Calling SetProfileID with the same profile ID must not fail.
c.Assert(al.SetProfileID("test"), qt.IsNil)
// Changing a profile ID must fail.
c.Assert(al.SetProfileID("test2"), qt.IsNotNil)
}
// TestSendOnRestore pushes a n logs to the persistent store, and ensures they

187
ipn/auditlog/extension.go Normal file
View File

@ -0,0 +1,187 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package auditlog
import (
"context"
"errors"
"fmt"
"sync"
"time"
"tailscale.com/control/controlclient"
"tailscale.com/feature"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
)
func init() {
feature.Register("auditlog")
ipnlocal.RegisterExtension("auditlog", newExtension)
}
// extension is an [ipnlocal.Extension] managing audit logging
// on platforms that import the "tailscale.com/ipn/auditlog" package.
type extension struct {
logf logger.Logf
// cleanup are functions to call on shutdown.
cleanup []func()
// store is the log store shared by all loggers.
// It is created when the first logger is started.
store lazy.SyncValue[LogStore]
// mu protects all following fields.
mu sync.Mutex
// logger is the current audit logger, or nil if it is not set up,
// such as before the first control client is created, or after
// a profile change and before the new control client is created.
//
// It queues, persists, and sends audit logs to the control client.
logger *Logger
}
// newExtension is a [ipnlocal.NewExtensionFn] that creates a new audit log extension.
func newExtension(logf logger.Logf, _ *tsd.System) (ipnlocal.Extension, error) {
return &extension{logf: logger.WithPrefix(logf, "auditlog: ")}, nil
}
// Init implements [ipnlocal.Extension] by registering callbacks and providers
// for the duration of the extension's lifetime.
func (e *extension) Init(lb *ipnlocal.LocalBackend) error {
e.cleanup = []func(){
lb.RegisterControlClientCallback(e.controlClientChanged),
lb.RegisterProfileChangeCallback(e.profileChanged, false),
lb.RegisterAuditLogProvider(e.getCurrentLogger),
}
return nil
}
// [controlclient.Auto] implements [Transport].
var _ Transport = (*controlclient.Auto)(nil)
// startNewLogger creates and starts a new logger for the specified profile
// using the specified [controlclient.Client] as the transport.
// The profileID may be "" if the profile has not been persisted yet.
func (e *extension) startNewLogger(cc controlclient.Client, profileID ipn.ProfileID) (*Logger, error) {
transport, ok := cc.(Transport)
if !ok {
return nil, fmt.Errorf("%T cannot be used as transport", cc)
}
// Create a new log store if this is the first logger.
// Otherwise, get the existing log store.
store, err := e.store.GetErr(func() (LogStore, error) {
return newDefaultLogStore(e.logf)
})
if err != nil {
return nil, fmt.Errorf("failed to create audit log store: %w", err)
}
logger := NewLogger(Opts{
Logf: e.logf,
RetryLimit: 32,
Store: store,
})
if err := logger.SetProfileID(profileID); err != nil {
return nil, fmt.Errorf("set profile failed: %w", err)
}
if err := logger.Start(transport); err != nil {
return nil, fmt.Errorf("start failed: %w", err)
}
return logger, nil
}
func (e *extension) controlClientChanged(cc controlclient.Client, profile ipn.LoginProfileView, _ ipn.PrefsView) (cleanup func()) {
logger, err := e.startNewLogger(cc, profile.ID())
e.mu.Lock()
e.logger = logger // nil on error
e.mu.Unlock()
if err != nil {
// If we fail to create or start the logger, log the error
// and return a nil cleanup function. There's nothing more
// we can do here.
//
// But [extension.getCurrentLogger] returns [noCurrentLogger]
// when the logger is nil. Since [noCurrentLogger] always
// fails with [errNoLogger], operations that must be audited
// but cannot will fail on platforms where the audit logger
// is enabled (i.e., the auditlog package is imported).
e.logf("[unexpected] %v", err)
return nil
}
return func() {
// Stop the logger when the control client shuts down.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
logger.FlushAndStop(ctx)
}
}
func (e *extension) profileChanged(profile ipn.LoginProfileView, _ ipn.PrefsView, sameNode bool) {
e.mu.Lock()
defer e.mu.Unlock()
switch {
case e.logger == nil:
// No-op if we don't have an audit logger.
case sameNode:
// The profile info has changed, but it represents the same node.
// This includes the case where the login has just been completed
// and the profile's [ipn.ProfileID] has been set for the first time.
if err := e.logger.SetProfileID(profile.ID()); err != nil {
e.logf("[unexpected] failed to set profile ID: %v", err)
}
default:
// The profile info has changed, and it represents a different node.
// We won't have an audit logger for the new profile until the new
// control client is created.
//
// We don't expect any auditable actions to be attempted in this state.
// But if they are, they will fail with [errNoLogger].
e.logger = nil
}
}
// errNoLogger is an error returned by [noCurrentLogger]. It indicates that
// the logger was unavailable when [ipnlocal.LocalBackend] requested it,
// such as when an auditable action was attempted before [LocalBackend.Start]
// was called for the first time or immediately after a profile change
// and before the new control client was created.
//
// This error is unexpected and should not occur in normal operation.
var errNoLogger = errors.New("[unexpected] no audit logger")
// noCurrentLogger is an [ipnauth.AuditLogFunc] returned by [extension.getCurrentLogger]
// when the logger is not available. It fails with [errNoLogger] on every call.
func noCurrentLogger(_ tailcfg.ClientAuditAction, _ string) error {
return errNoLogger
}
// getCurrentLogger is an [ipnlocal.AuditLogProvider] registered with [ipnlocal.LocalBackend].
// It is called when [ipnlocal.LocalBackend] needs to audit an action.
//
// It returns a function that enqueues the audit log for the current profile,
// or [noCurrentLogger] if the logger is unavailable.
func (e *extension) getCurrentLogger() ipnauth.AuditLogFunc {
e.mu.Lock()
defer e.mu.Unlock()
if e.logger == nil {
return noCurrentLogger
}
return e.logger.Enqueue
}
// Shutdown implements [ipnlocal.Extension].
func (e *extension) Shutdown() error {
for _, f := range e.cleanup {
f()
}
e.cleanup = nil
return nil
}

62
ipn/auditlog/store.go Normal file
View File

@ -0,0 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package auditlog
import (
"fmt"
"os"
"path/filepath"
"runtime"
"tailscale.com/ipn/store"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/util/must"
)
var storeFilePath lazy.SyncValue[string]
// SetStoreFilePath sets the audit log store file path.
// It is optional on platforms with a default store path,
// but required on platforms without one (e.g., macOS).
// It panics if called more than once or after the store has been created.
func SetStoreFilePath(path string) {
if !storeFilePath.Set(path) {
panic("store file path already set or used")
}
}
// DefaultStoreFilePath returns the default audit log store file path
// for the current platform, or an error if the platform does not have one.
func DefaultStoreFilePath() (string, error) {
switch runtime.GOOS {
case "windows":
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "audit-log.json"), nil
default:
// The auditlog package must either be omitted from the build,
// have the platform-specific store path set with [SetStoreFilePath] (e.g., on macOS),
// or have the default store path available on the current platform.
return "", fmt.Errorf("[unexpected] no default store path available on %s", runtime.GOOS)
}
}
// newDefaultLogStore returns a new [LogStore] for the current platform.
func newDefaultLogStore(logf logger.Logf) (LogStore, error) {
path, err := storeFilePath.GetErr(DefaultStoreFilePath)
if err != nil {
// This indicates that the auditlog package was not omitted from the build
// on a platform without a default store path and that [SetStoreFilePath]
// was not called to set a platform-specific store path.
//
// This is not expected to happen, but if it does, let's log it
// and use an in-memory store as a fallback.
logf("[unexpected] failed to get audit log store path: %v", err)
return NewLogStore(must.Get(store.New(logf, "mem:auditlog"))), nil
}
fs, err := store.New(logf, path)
if err != nil {
return nil, fmt.Errorf("failed to create audit log store at %q: %w", path, err)
}
return NewLogStore(fs), nil
}

View File

@ -28,8 +28,8 @@ func init() {
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
}
// desktopSessionsExt implements [localBackendExtension].
var _ localBackendExtension = (*desktopSessionsExt)(nil)
// desktopSessionsExt implements [Extension].
var _ Extension = (*desktopSessionsExt)(nil)
// desktopSessionsExt extends [LocalBackend] with desktop session management.
// It keeps Tailscale running in the background if Always-On mode is enabled,
@ -51,7 +51,7 @@ type desktopSessionsExt struct {
// newDesktopSessionsExt returns a new [desktopSessionsExt],
// or an error if [desktop.SessionManager] is not available.
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (localBackendExtension, error) {
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (Extension, error) {
sm, ok := sys.SessionManager.GetOK()
if !ok {
return nil, errors.New("session manager is not available")

View File

@ -57,12 +57,10 @@ import (
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/auditlog"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
memstore "tailscale.com/ipn/store/mem"
"tailscale.com/log/sockstatlog"
"tailscale.com/logpolicy"
"tailscale.com/net/captivedetection"
@ -170,8 +168,8 @@ type watchSession struct {
cancel context.CancelFunc // to shut down the session
}
// localBackendExtension extends [LocalBackend] with additional functionality.
type localBackendExtension interface {
// Extension extends [LocalBackend] with additional functionality.
type Extension interface {
// Init is called to initialize the extension when the [LocalBackend] is created
// and before it starts running. If the extension cannot be initialized,
// it must return an error, and the Shutdown method will not be called.
@ -185,17 +183,17 @@ type localBackendExtension interface {
Shutdown() error
}
// newLocalBackendExtension is a function that instantiates a [localBackendExtension].
type newLocalBackendExtension func(logger.Logf, *tsd.System) (localBackendExtension, error)
// NewExtensionFn is a function that instantiates an [Extension].
type NewExtensionFn func(logger.Logf, *tsd.System) (Extension, error)
// registeredExtensions is a map of registered local backend extensions,
// where the key is the name of the extension and the value is the function
// that instantiates the extension.
var registeredExtensions map[string]newLocalBackendExtension
var registeredExtensions map[string]NewExtensionFn
// RegisterExtension registers a function that creates a [localBackendExtension].
// It panics if newExt is nil or if an extension with the same name has already been registered.
func RegisterExtension(name string, newExt newLocalBackendExtension) {
func RegisterExtension(name string, newExt NewExtensionFn) {
if newExt == nil {
panic(fmt.Sprintf("lb: newExt is nil: %q", name))
}
@ -213,6 +211,36 @@ func RegisterExtension(name string, newExt newLocalBackendExtension) {
// It is called with [LocalBackend.mu] held.
type profileResolver func() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool)
// NewControlClientCallback is a function to be called when a new [controlclient.Client]
// is created and before it is first used. The login profile and prefs represent
// the profile for which the cc is created and are always valid; however, the
// profile's [ipn.LoginProfileView.ID] returns a zero [ipn.ProfileID] if the profile
// is new and has not been persisted yet.
//
// The callback is called with [LocalBackend.mu] held and must not call
// any [LocalBackend] methods.
//
// It returns a function to be called when the cc is being shut down,
// or nil if no cleanup is needed.
type NewControlClientCallback func(controlclient.Client, ipn.LoginProfileView, ipn.PrefsView) (cleanup func())
// ProfileChangeCallback is a function to be called when the current login profile changes.
// The sameNode parameter indicates whether the profile represents the same node as before,
// such as when only the profile metadata is updated but the node ID remains the same,
// or when a new profile is persisted and assigned an [ipn.ProfileID] for the first time.
// The subscribers can use this information to decide whether to reset their state.
//
// The profile and prefs are always valid, but the profile's [ipn.LoginProfileView.ID]
// returns a zero [ipn.ProfileID] if the profile is new and has not been persisted yet.
//
// The callback is called with [LocalBackend.mu] held and must not call
// any [LocalBackend] methods.
type ProfileChangeCallback func(_ ipn.LoginProfileView, _ ipn.PrefsView, sameNode bool)
// AuditLogProvider is a function that returns an [ipnauth.AuditLogFunc] for
// logging auditable actions.
type AuditLogProvider func() ipnauth.AuditLogFunc
// LocalBackend is the glue between the major pieces of the Tailscale
// network software: the cloud control plane (via controlclient), the
// network data plane (via wgengine), and the user-facing UIs and CLIs
@ -453,11 +481,19 @@ type LocalBackend struct {
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
shutdownCbs set.HandleSet[func() error]
// auditLogger, if non-nil, manages audit logging for the backend.
//
// It queues, persists, and sends audit logs
// to the control client. auditLogger has the same lifespan as b.cc.
auditLogger *auditlog.Logger
// newControlClientCbs are the functions to be called when a new control client is created.
newControlClientCbs set.HandleSet[NewControlClientCallback]
// profileChangeCbs are the callbacks to be called when the current login profile changes,
// either because of a profile switch, or because the profile information was updated
// by [LocalBackend.SetControlClientStatus], including when the profile is first populated
// and persisted.
profileChangeCbs set.HandleSet[ProfileChangeCallback]
// auditLoggers is a collection of registered audit log providers.
// Each [AuditLogProvider] is called to get an [ipnauth.AuditLogFunc] when an auditable action
// is about to be performed. If an audit logger returns an error, the action is denied.
auditLoggers set.HandleSet[AuditLogProvider]
}
// HealthTracker returns the health tracker for the backend.
@ -1679,6 +1715,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
// Perform all mutations of prefs based on the netmap here.
if prefsChanged {
profile := b.pm.CurrentProfile()
// Prefs will be written out if stale; this is not safe unless locked or cloned.
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
MagicDNSName: curNetMap.MagicDNSSuffix(),
@ -1686,13 +1723,16 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
}); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
// Update the audit logger with the current profile ID.
if b.auditLogger != nil && prefsChanged {
pid := b.pm.CurrentProfile().ID()
if err := b.auditLogger.SetProfileID(pid); err != nil {
b.logf("Failed to set profile ID in audit logger: %v", err)
// Updating profile prefs may have resulted in a change to the current [ipn.LoginProfile],
// either because the user completed a login, which populated and persisted their profile
// for the first time, or because of an [ipn.NetworkProfile] or [tailcfg.UserProfile] change.
// Theoretically, a completed login could also result in a switch to a different existing
// profile representing a different node (see tailscale/tailscale#8816).
// Check if the current profile has changed, and invoke all registered [ProfileChangeCallback]
// if necessary.
if cp := b.pm.CurrentProfile(); *cp.AsStruct() != *profile.AsStruct() {
nodeChanged := profile.ID() != "" && profile.ID() != cp.ID()
b.notifyProfileChangeLocked(profile, prefs.View(), !nodeChanged)
}
}
@ -2401,27 +2441,12 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
debugFlags = append([]string{"netstack"}, debugFlags...)
}
var auditLogShutdown func()
// Audit logging is only available if the client has set up a proper persistent
// store for the logs in sys.
store, ok := b.sys.AuditLogStore.GetOK()
if !ok {
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
store = auditlog.NewLogStore(&memstore.Store{})
var ccShutdownCbs []func()
ccShutdown := func() {
for _, cb := range ccShutdownCbs {
cb()
}
}
al := auditlog.NewLogger(auditlog.Opts{
Logf: b.logf,
RetryLimit: 32,
Store: store,
})
b.auditLogger = al
auditLogShutdown = func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
al.FlushAndStop(ctx)
}
// TODO(apenwarr): The only way to change the ServerURL is to
// re-run b.Start, because this is the only place we create a
// new controlclient. EditPrefs allows you to overwrite ServerURL,
@ -2447,7 +2472,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
C2NHandler: http.HandlerFunc(b.handleC2N),
DialPlan: &b.dialPlan, // pointer because it can't be copied
ControlKnobs: b.sys.ControlKnobs(),
Shutdown: auditLogShutdown,
Shutdown: ccShutdown,
// Don't warn about broken Linux IP forwarding when
// netstack is being used.
@ -2456,6 +2481,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
if err != nil {
return err
}
for _, cb := range b.newControlClientCbs {
if cleanup := cb(cc, b.pm.CurrentProfile(), prefs); cleanup != nil {
ccShutdownCbs = append(ccShutdownCbs, cleanup)
}
}
b.setControlClientLocked(cc)
endpoints := b.endpoints
@ -4302,16 +4332,42 @@ func (b *LocalBackend) MaybeClearAppConnector(mp *ipn.MaskedPrefs) error {
return err
}
var errNoAuditLogger = errors.New("no audit logger configured")
// RegisterAuditLogProvider registers an audit log provider, which returns a function
// to be called when an auditable action is about to be performed.
// The returned function unregisters the provider.
// It panics if the provider is nil.
func (b *LocalBackend) RegisterAuditLogProvider(provider AuditLogProvider) (unregister func()) {
if provider == nil {
panic("nil audit log provider")
}
b.mu.Lock()
defer b.mu.Unlock()
handle := b.auditLoggers.Add(provider)
return func() {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.auditLoggers, handle)
}
}
// getAuditLoggerLocked returns a function that calls all currently registered
// audit loggers, failing as soon as any of them returns an error.
//
// b.mu must be held.
func (b *LocalBackend) getAuditLoggerLocked() ipnauth.AuditLogFunc {
logger := b.auditLogger
return func(action tailcfg.ClientAuditAction, details string) error {
if logger == nil {
return errNoAuditLogger
var loggers []ipnauth.AuditLogFunc
if len(b.auditLoggers) != 0 {
loggers = make([]ipnauth.AuditLogFunc, 0, len(b.auditLoggers))
for _, getLogger := range b.auditLoggers {
loggers = append(loggers, getLogger())
}
if err := logger.Enqueue(action, details); err != nil {
return fmt.Errorf("failed to enqueue audit log %v %q: %w", action, details, err)
}
return func(action tailcfg.ClientAuditAction, details string) error {
b.logf("auditlog: %v: %v", action, details)
for _, logger := range loggers {
if err := logger(action, details); err != nil {
return err
}
}
return nil
}
@ -5920,8 +5976,22 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
b.logf("requestEngineStatusAndWait: got status update.")
}
// [controlclient.Auto] implements [auditlog.Transport].
var _ auditlog.Transport = (*controlclient.Auto)(nil)
// RegisterControlClientCallback registers a function to be called every time a new
// control client is created, until the returned unregister function is called.
// It panics if the cb is nil.
func (b *LocalBackend) RegisterControlClientCallback(cb NewControlClientCallback) (unregister func()) {
if cb == nil {
panic("nil control client callback")
}
b.mu.Lock()
defer b.mu.Unlock()
handle := b.newControlClientCbs.Add(cb)
return func() {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.newControlClientCbs, handle)
}
}
// setControlClientLocked sets the control client to cc,
// which may be nil.
@ -5930,15 +6000,6 @@ var _ auditlog.Transport = (*controlclient.Auto)(nil)
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
b.cc = cc
b.ccAuto, _ = cc.(*controlclient.Auto)
if t, ok := b.cc.(auditlog.Transport); ok && b.auditLogger != nil {
if err := b.auditLogger.SetProfileID(b.pm.CurrentProfile().ID()); err != nil {
b.logf("audit logger set profile ID failure: %v", err)
}
if err := b.auditLogger.Start(t); err != nil {
b.logf("audit logger start failure: %v", err)
}
}
}
// resetControlClientLocked sets b.cc to nil and returns the old value. If the
@ -7521,6 +7582,37 @@ func (b *LocalBackend) resetDialPlan() {
}
}
// RegisterProfileChangeCallback registers a function to be called when the current [ipn.LoginProfile] changes.
// If includeCurrent is true, the callback is called immediately with the current profile.
// The returned function unregisters the callback.
// It panics if the cb is nil.
func (b *LocalBackend) RegisterProfileChangeCallback(cb ProfileChangeCallback, includeCurrent bool) (unregister func()) {
if cb == nil {
panic("nil profile change callback")
}
b.mu.Lock()
defer b.mu.Unlock()
handle := b.profileChangeCbs.Add(cb)
if includeCurrent {
cb(b.pm.CurrentProfile(), stripKeysFromPrefs(b.pm.CurrentPrefs()), false)
}
return func() {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.profileChangeCbs, handle)
}
}
// notifyProfileChangeLocked invokes all registered profile change callbacks.
//
// b.mu must be held.
func (b *LocalBackend) notifyProfileChangeLocked(profile ipn.LoginProfileView, prefs ipn.PrefsView, sameNode bool) {
prefs = stripKeysFromPrefs(prefs)
for _, cb := range b.profileChangeCbs {
cb(profile, prefs, sameNode)
}
}
// resetForProfileChangeLockedOnEntry resets the backend for a profile change.
//
// b.mu must held on entry. It is released on exit.
@ -7549,6 +7641,7 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err
b.lastSuggestedExitNode = ""
b.keyExpired = false
b.resetAlwaysOnOverrideLocked()
b.notifyProfileChangeLocked(b.pm.CurrentProfile(), b.pm.CurrentPrefs(), false)
b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs())
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
b.health.SetLocalLogConfigHealth(nil)

View File

@ -25,7 +25,6 @@ import (
"tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/ipn/auditlog"
"tailscale.com/ipn/conffile"
"tailscale.com/ipn/desktop"
"tailscale.com/net/dns"
@ -51,7 +50,6 @@ type System struct {
Router SubSystem[router.Router]
Tun SubSystem[*tstun.Wrapper]
StateStore SubSystem[ipn.StateStore]
AuditLogStore SubSystem[auditlog.LogStore]
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
DriveForLocal SubSystem[drive.FileSystemForLocal]
DriveForRemote SubSystem[drive.FileSystemForRemote]
@ -108,8 +106,6 @@ func (s *System) Set(v any) {
s.MagicSock.Set(v)
case ipn.StateStore:
s.StateStore.Set(v)
case auditlog.LogStore:
s.AuditLogStore.Set(v)
case NetstackImpl:
s.Netstack.Set(v)
case drive.FileSystemForLocal:

View File

@ -28,6 +28,7 @@ import (
_ "tailscale.com/health"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/auditlog"
_ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/desktop"
_ "tailscale.com/ipn/ipnlocal"