mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-26 11:11:01 +00:00
Merge bd0dadd771cb67f9d344e820c7111a51cd951e17 into b3455fa99a5e8d07133d5140017ec7c49f032a07
This commit is contained in:
commit
90f892250a
@ -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
|
||||
@ -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)
|
||||
|
@ -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