mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-07 08:44:38 +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/client/tailscale from tailscale.com/cmd/k8s-operator
|
||||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||||
tailscale.com/ipn from tailscale.com/client/local+
|
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/conffile from tailscale.com/ipn/ipnlocal+
|
||||||
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/ipn/desktop from tailscale.com/ipn/ipnlocal+
|
||||||
💣 tailscale.com/ipn/ipnauth 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/hostinfo from tailscale.com/client/web+
|
||||||
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
tailscale.com/internal/noiseconn from tailscale.com/control/controlclient
|
||||||
tailscale.com/ipn from tailscale.com/client/local+
|
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/conffile from tailscale.com/cmd/tailscaled+
|
||||||
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
|
💣 tailscale.com/ipn/desktop from tailscale.com/cmd/tailscaled+
|
||||||
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
💣 tailscale.com/ipn/ipnauth from tailscale.com/ipn/ipnlocal+
|
||||||
|
@ -44,6 +44,7 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||||
"tailscale.com/drive/driveimpl"
|
"tailscale.com/drive/driveimpl"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
|
_ "tailscale.com/ipn/auditlog"
|
||||||
"tailscale.com/ipn/desktop"
|
"tailscale.com/ipn/desktop"
|
||||||
"tailscale.com/logpolicy"
|
"tailscale.com/logpolicy"
|
||||||
"tailscale.com/logtail/backoff"
|
"tailscale.com/logtail/backoff"
|
||||||
|
@ -112,7 +112,7 @@ func NewLogger(opts Opts) *Logger {
|
|||||||
|
|
||||||
al := &Logger{
|
al := &Logger{
|
||||||
retryLimit: opts.RetryLimit,
|
retryLimit: opts.RetryLimit,
|
||||||
logf: logger.WithPrefix(opts.Logf, "auditlog: "),
|
logf: opts.Logf,
|
||||||
store: opts.Store,
|
store: opts.Store,
|
||||||
flusher: make(chan struct{}, 1),
|
flusher: make(chan struct{}, 1),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
@ -138,7 +138,9 @@ func (al *Logger) FlushAndStop(ctx context.Context) {
|
|||||||
func (al *Logger) SetProfileID(profileID ipn.ProfileID) error {
|
func (al *Logger) SetProfileID(profileID ipn.ProfileID) error {
|
||||||
al.mu.Lock()
|
al.mu.Lock()
|
||||||
defer al.mu.Unlock()
|
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")
|
return errors.New("profileID already set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,8 +184,11 @@ func TestChangeProfileId(t *testing.T) {
|
|||||||
})
|
})
|
||||||
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
c.Assert(al.SetProfileID("test"), qt.IsNil)
|
||||||
|
|
||||||
// Changing a profile ID must fail
|
// Calling SetProfileID with the same profile ID must not fail.
|
||||||
c.Assert(al.SetProfileID("test"), qt.IsNotNil)
|
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
|
// 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)
|
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// desktopSessionsExt implements [localBackendExtension].
|
// desktopSessionsExt implements [Extension].
|
||||||
var _ localBackendExtension = (*desktopSessionsExt)(nil)
|
var _ Extension = (*desktopSessionsExt)(nil)
|
||||||
|
|
||||||
// desktopSessionsExt extends [LocalBackend] with desktop session management.
|
// desktopSessionsExt extends [LocalBackend] with desktop session management.
|
||||||
// It keeps Tailscale running in the background if Always-On mode is enabled,
|
// 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],
|
// newDesktopSessionsExt returns a new [desktopSessionsExt],
|
||||||
// or an error if [desktop.SessionManager] is not available.
|
// 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()
|
sm, ok := sys.SessionManager.GetOK()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("session manager is not available")
|
return nil, errors.New("session manager is not available")
|
||||||
|
@ -57,12 +57,10 @@ import (
|
|||||||
"tailscale.com/health/healthmsg"
|
"tailscale.com/health/healthmsg"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/auditlog"
|
|
||||||
"tailscale.com/ipn/conffile"
|
"tailscale.com/ipn/conffile"
|
||||||
"tailscale.com/ipn/ipnauth"
|
"tailscale.com/ipn/ipnauth"
|
||||||
"tailscale.com/ipn/ipnstate"
|
"tailscale.com/ipn/ipnstate"
|
||||||
"tailscale.com/ipn/policy"
|
"tailscale.com/ipn/policy"
|
||||||
memstore "tailscale.com/ipn/store/mem"
|
|
||||||
"tailscale.com/log/sockstatlog"
|
"tailscale.com/log/sockstatlog"
|
||||||
"tailscale.com/logpolicy"
|
"tailscale.com/logpolicy"
|
||||||
"tailscale.com/net/captivedetection"
|
"tailscale.com/net/captivedetection"
|
||||||
@ -170,8 +168,8 @@ type watchSession struct {
|
|||||||
cancel context.CancelFunc // to shut down the session
|
cancel context.CancelFunc // to shut down the session
|
||||||
}
|
}
|
||||||
|
|
||||||
// localBackendExtension extends [LocalBackend] with additional functionality.
|
// Extension extends [LocalBackend] with additional functionality.
|
||||||
type localBackendExtension interface {
|
type Extension interface {
|
||||||
// Init is called to initialize the extension when the [LocalBackend] is created
|
// Init is called to initialize the extension when the [LocalBackend] is created
|
||||||
// and before it starts running. If the extension cannot be initialized,
|
// and before it starts running. If the extension cannot be initialized,
|
||||||
// it must return an error, and the Shutdown method will not be called.
|
// it must return an error, and the Shutdown method will not be called.
|
||||||
@ -185,17 +183,17 @@ type localBackendExtension interface {
|
|||||||
Shutdown() error
|
Shutdown() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// newLocalBackendExtension is a function that instantiates a [localBackendExtension].
|
// NewExtensionFn is a function that instantiates an [Extension].
|
||||||
type newLocalBackendExtension func(logger.Logf, *tsd.System) (localBackendExtension, error)
|
type NewExtensionFn func(logger.Logf, *tsd.System) (Extension, error)
|
||||||
|
|
||||||
// registeredExtensions is a map of registered local backend extensions,
|
// registeredExtensions is a map of registered local backend extensions,
|
||||||
// where the key is the name of the extension and the value is the function
|
// where the key is the name of the extension and the value is the function
|
||||||
// that instantiates the extension.
|
// that instantiates the extension.
|
||||||
var registeredExtensions map[string]newLocalBackendExtension
|
var registeredExtensions map[string]NewExtensionFn
|
||||||
|
|
||||||
// RegisterExtension registers a function that creates a [localBackendExtension].
|
// 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.
|
// 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 {
|
if newExt == nil {
|
||||||
panic(fmt.Sprintf("lb: newExt is nil: %q", name))
|
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.
|
// It is called with [LocalBackend.mu] held.
|
||||||
type profileResolver func() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool)
|
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
|
// LocalBackend is the glue between the major pieces of the Tailscale
|
||||||
// network software: the cloud control plane (via controlclient), the
|
// network software: the cloud control plane (via controlclient), the
|
||||||
// network data plane (via wgengine), and the user-facing UIs and CLIs
|
// 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.
|
// Returned errors are logged but otherwise ignored and do not affect the shutdown process.
|
||||||
shutdownCbs set.HandleSet[func() error]
|
shutdownCbs set.HandleSet[func() error]
|
||||||
|
|
||||||
// auditLogger, if non-nil, manages audit logging for the backend.
|
// newControlClientCbs are the functions to be called when a new control client is created.
|
||||||
//
|
newControlClientCbs set.HandleSet[NewControlClientCallback]
|
||||||
// It queues, persists, and sends audit logs
|
|
||||||
// to the control client. auditLogger has the same lifespan as b.cc.
|
// profileChangeCbs are the callbacks to be called when the current login profile changes,
|
||||||
auditLogger *auditlog.Logger
|
// 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.
|
// 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.
|
// Perform all mutations of prefs based on the netmap here.
|
||||||
if prefsChanged {
|
if prefsChanged {
|
||||||
|
profile := b.pm.CurrentProfile()
|
||||||
// Prefs will be written out if stale; this is not safe unless locked or cloned.
|
// Prefs will be written out if stale; this is not safe unless locked or cloned.
|
||||||
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
|
if err := b.pm.SetPrefs(prefs.View(), ipn.NetworkProfile{
|
||||||
MagicDNSName: curNetMap.MagicDNSSuffix(),
|
MagicDNSName: curNetMap.MagicDNSSuffix(),
|
||||||
@ -1686,13 +1723,16 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
b.logf("Failed to save new controlclient state: %v", err)
|
b.logf("Failed to save new controlclient state: %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
|
||||||
// Update the audit logger with the current profile ID.
|
// for the first time, or because of an [ipn.NetworkProfile] or [tailcfg.UserProfile] change.
|
||||||
if b.auditLogger != nil && prefsChanged {
|
// Theoretically, a completed login could also result in a switch to a different existing
|
||||||
pid := b.pm.CurrentProfile().ID()
|
// profile representing a different node (see tailscale/tailscale#8816).
|
||||||
if err := b.auditLogger.SetProfileID(pid); err != nil {
|
// Check if the current profile has changed, and invoke all registered [ProfileChangeCallback]
|
||||||
b.logf("Failed to set profile ID in audit logger: %v", err)
|
// 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...)
|
debugFlags = append([]string{"netstack"}, debugFlags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
var auditLogShutdown func()
|
var ccShutdownCbs []func()
|
||||||
// Audit logging is only available if the client has set up a proper persistent
|
ccShutdown := func() {
|
||||||
// store for the logs in sys.
|
for _, cb := range ccShutdownCbs {
|
||||||
store, ok := b.sys.AuditLogStore.GetOK()
|
cb()
|
||||||
if !ok {
|
|
||||||
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
|
|
||||||
store = auditlog.NewLogStore(&memstore.Store{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// TODO(apenwarr): The only way to change the ServerURL is to
|
||||||
// re-run b.Start, because this is the only place we create a
|
// re-run b.Start, because this is the only place we create a
|
||||||
// new controlclient. EditPrefs allows you to overwrite ServerURL,
|
// 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),
|
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||||
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
||||||
ControlKnobs: b.sys.ControlKnobs(),
|
ControlKnobs: b.sys.ControlKnobs(),
|
||||||
Shutdown: auditLogShutdown,
|
Shutdown: ccShutdown,
|
||||||
|
|
||||||
// Don't warn about broken Linux IP forwarding when
|
// Don't warn about broken Linux IP forwarding when
|
||||||
// netstack is being used.
|
// netstack is being used.
|
||||||
@ -2456,6 +2481,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
for _, cb := range b.newControlClientCbs {
|
||||||
|
if cleanup := cb(cc, b.pm.CurrentProfile(), prefs); cleanup != nil {
|
||||||
|
ccShutdownCbs = append(ccShutdownCbs, cleanup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.setControlClientLocked(cc)
|
b.setControlClientLocked(cc)
|
||||||
endpoints := b.endpoints
|
endpoints := b.endpoints
|
||||||
@ -4302,16 +4332,42 @@ func (b *LocalBackend) MaybeClearAppConnector(mp *ipn.MaskedPrefs) error {
|
|||||||
return err
|
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.
|
||||||
func (b *LocalBackend) getAuditLoggerLocked() ipnauth.AuditLogFunc {
|
// The returned function unregisters the provider.
|
||||||
logger := b.auditLogger
|
// It panics if the provider is nil.
|
||||||
return func(action tailcfg.ClientAuditAction, details string) error {
|
func (b *LocalBackend) RegisterAuditLogProvider(provider AuditLogProvider) (unregister func()) {
|
||||||
if logger == nil {
|
if provider == nil {
|
||||||
return errNoAuditLogger
|
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 {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
if err := logger.Enqueue(action, details); err != nil {
|
|
||||||
return fmt.Errorf("failed to enqueue audit log %v %q: %w", action, details, err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -5920,8 +5976,22 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
|||||||
b.logf("requestEngineStatusAndWait: got status update.")
|
b.logf("requestEngineStatusAndWait: got status update.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// [controlclient.Auto] implements [auditlog.Transport].
|
// RegisterControlClientCallback registers a function to be called every time a new
|
||||||
var _ auditlog.Transport = (*controlclient.Auto)(nil)
|
// 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,
|
// setControlClientLocked sets the control client to cc,
|
||||||
// which may be nil.
|
// which may be nil.
|
||||||
@ -5930,15 +6000,6 @@ var _ auditlog.Transport = (*controlclient.Auto)(nil)
|
|||||||
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
|
func (b *LocalBackend) setControlClientLocked(cc controlclient.Client) {
|
||||||
b.cc = cc
|
b.cc = cc
|
||||||
b.ccAuto, _ = cc.(*controlclient.Auto)
|
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
|
// 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.
|
// resetForProfileChangeLockedOnEntry resets the backend for a profile change.
|
||||||
//
|
//
|
||||||
// b.mu must held on entry. It is released on exit.
|
// 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.lastSuggestedExitNode = ""
|
||||||
b.keyExpired = false
|
b.keyExpired = false
|
||||||
b.resetAlwaysOnOverrideLocked()
|
b.resetAlwaysOnOverrideLocked()
|
||||||
|
b.notifyProfileChangeLocked(b.pm.CurrentProfile(), b.pm.CurrentPrefs(), false)
|
||||||
b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs())
|
b.setAtomicValuesFromPrefsLocked(b.pm.CurrentPrefs())
|
||||||
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
||||||
b.health.SetLocalLogConfigHealth(nil)
|
b.health.SetLocalLogConfigHealth(nil)
|
||||||
|
@ -25,7 +25,6 @@ import (
|
|||||||
"tailscale.com/drive"
|
"tailscale.com/drive"
|
||||||
"tailscale.com/health"
|
"tailscale.com/health"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/auditlog"
|
|
||||||
"tailscale.com/ipn/conffile"
|
"tailscale.com/ipn/conffile"
|
||||||
"tailscale.com/ipn/desktop"
|
"tailscale.com/ipn/desktop"
|
||||||
"tailscale.com/net/dns"
|
"tailscale.com/net/dns"
|
||||||
@ -51,7 +50,6 @@ type System struct {
|
|||||||
Router SubSystem[router.Router]
|
Router SubSystem[router.Router]
|
||||||
Tun SubSystem[*tstun.Wrapper]
|
Tun SubSystem[*tstun.Wrapper]
|
||||||
StateStore SubSystem[ipn.StateStore]
|
StateStore SubSystem[ipn.StateStore]
|
||||||
AuditLogStore SubSystem[auditlog.LogStore]
|
|
||||||
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
|
Netstack SubSystem[NetstackImpl] // actually a *netstack.Impl
|
||||||
DriveForLocal SubSystem[drive.FileSystemForLocal]
|
DriveForLocal SubSystem[drive.FileSystemForLocal]
|
||||||
DriveForRemote SubSystem[drive.FileSystemForRemote]
|
DriveForRemote SubSystem[drive.FileSystemForRemote]
|
||||||
@ -108,8 +106,6 @@ func (s *System) Set(v any) {
|
|||||||
s.MagicSock.Set(v)
|
s.MagicSock.Set(v)
|
||||||
case ipn.StateStore:
|
case ipn.StateStore:
|
||||||
s.StateStore.Set(v)
|
s.StateStore.Set(v)
|
||||||
case auditlog.LogStore:
|
|
||||||
s.AuditLogStore.Set(v)
|
|
||||||
case NetstackImpl:
|
case NetstackImpl:
|
||||||
s.Netstack.Set(v)
|
s.Netstack.Set(v)
|
||||||
case drive.FileSystemForLocal:
|
case drive.FileSystemForLocal:
|
||||||
|
@ -28,6 +28,7 @@ import (
|
|||||||
_ "tailscale.com/health"
|
_ "tailscale.com/health"
|
||||||
_ "tailscale.com/hostinfo"
|
_ "tailscale.com/hostinfo"
|
||||||
_ "tailscale.com/ipn"
|
_ "tailscale.com/ipn"
|
||||||
|
_ "tailscale.com/ipn/auditlog"
|
||||||
_ "tailscale.com/ipn/conffile"
|
_ "tailscale.com/ipn/conffile"
|
||||||
_ "tailscale.com/ipn/desktop"
|
_ "tailscale.com/ipn/desktop"
|
||||||
_ "tailscale.com/ipn/ipnlocal"
|
_ "tailscale.com/ipn/ipnlocal"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user