mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-27 19:43:01 +00:00
cmd/tailscaled,ipn/{auditlog,ipnlocal},tsd: omit auditlog unless explicitly imported
In this PR, we update ipnlocal.LocalBackend to allow registering callbacks for control client creation and profile changes. We also allow to register ipnauth.AuditLogFunc to be called when an auditable action is attempted. We then use all this to invert the dependency between the auditlog and ipnlocal packages and make the auditlog functionality optional, where it only registers its callbacks via ipnlocal-provided hooks when the auditlog package is imported. We then underscore-import it when building tailscaled for Windows, and we'll explicitly import it when building xcode/ipn-go-bridge for macOS. Since there's no default log-store location for macOS, we'll also need to call auditlog.SetStoreFilePath to specify where pending audit logs should be persisted. Fixes #15394 Updates tailscale/corp#26435 Updates tailscale/corp#27012 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
6bbf98bef4
commit
bd0dadd771
@ -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+
|
||||
|
@ -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+
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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
187
ipn/auditlog/extension.go
Normal 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
62
ipn/auditlog/store.go
Normal 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
|
||||
}
|
@ -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")
|
||||
|
@ -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
|
||||
@ -4300,16 +4330,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
|
||||
}
|
||||
@ -5918,8 +5974,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.
|
||||
@ -5928,15 +5998,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
|
||||
@ -7519,6 +7580,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.
|
||||
@ -7547,6 +7639,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)
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user