diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 978744947..7c87649d1 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -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+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 0a9c46831..1fbf7caf1 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -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+ diff --git a/cmd/tailscaled/tailscaled_windows.go b/cmd/tailscaled/tailscaled_windows.go index 3574fb5f4..dfe53ef61 100644 --- a/cmd/tailscaled/tailscaled_windows.go +++ b/cmd/tailscaled/tailscaled_windows.go @@ -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" diff --git a/ipn/auditlog/auditlog.go b/ipn/auditlog/auditlog.go index 30f39211f..eba407ea1 100644 --- a/ipn/auditlog/auditlog.go +++ b/ipn/auditlog/auditlog.go @@ -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") } diff --git a/ipn/auditlog/auditlog_test.go b/ipn/auditlog/auditlog_test.go index 3d3bf95cb..041cab354 100644 --- a/ipn/auditlog/auditlog_test.go +++ b/ipn/auditlog/auditlog_test.go @@ -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 diff --git a/ipn/auditlog/extension.go b/ipn/auditlog/extension.go new file mode 100644 index 000000000..9f951ef0f --- /dev/null +++ b/ipn/auditlog/extension.go @@ -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 +} diff --git a/ipn/auditlog/store.go b/ipn/auditlog/store.go new file mode 100644 index 000000000..3b58ffa93 --- /dev/null +++ b/ipn/auditlog/store.go @@ -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 +} diff --git a/ipn/ipnlocal/desktop_sessions.go b/ipn/ipnlocal/desktop_sessions.go index 23307f667..4e9eebf34 100644 --- a/ipn/ipnlocal/desktop_sessions.go +++ b/ipn/ipnlocal/desktop_sessions.go @@ -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") diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 1f9f7e8b2..598fd7720 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -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) diff --git a/tsd/tsd.go b/tsd/tsd.go index 9ab35af55..1d1f35017 100644 --- a/tsd/tsd.go +++ b/tsd/tsd.go @@ -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: diff --git a/tstest/integration/tailscaled_deps_test_windows.go b/tstest/integration/tailscaled_deps_test_windows.go index a6df2f9ff..30ce0892e 100644 --- a/tstest/integration/tailscaled_deps_test_windows.go +++ b/tstest/integration/tailscaled_deps_test_windows.go @@ -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"