cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
// Package ipnext defines types and interfaces used for extending the core LocalBackend
|
|
|
|
// functionality with additional features and services.
|
|
|
|
package ipnext
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"tailscale.com/control/controlclient"
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
"tailscale.com/ipn/ipnauth"
|
|
|
|
"tailscale.com/tsd"
|
|
|
|
"tailscale.com/types/logger"
|
|
|
|
"tailscale.com/types/views"
|
|
|
|
"tailscale.com/util/mak"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Extension augments LocalBackend with additional functionality.
|
|
|
|
//
|
|
|
|
// An extension uses the provided [Host] to register callbacks
|
|
|
|
// and interact with the backend in a controlled, well-defined
|
|
|
|
// and thread-safe manner.
|
|
|
|
//
|
|
|
|
// Extensions are registered using [RegisterExtension].
|
|
|
|
//
|
|
|
|
// They must be safe for concurrent use.
|
|
|
|
type Extension interface {
|
|
|
|
// Name is a unique name of the extension.
|
|
|
|
// It must be the same as the name used to register the extension.
|
|
|
|
Name() string
|
|
|
|
|
|
|
|
// Init is called to initialize the extension when LocalBackend is initialized.
|
|
|
|
// If the extension cannot be initialized, it must return an error,
|
|
|
|
// and its Shutdown method will not be called on the host's shutdown.
|
|
|
|
// Returned errors are not fatal; they are used for logging.
|
|
|
|
// A [SkipExtension] error indicates an intentional decision rather than a failure.
|
|
|
|
Init(Host) error
|
|
|
|
|
|
|
|
// Shutdown is called when LocalBackend is shutting down,
|
|
|
|
// provided the extension was initialized. For multiple extensions,
|
|
|
|
// Shutdown is called in the reverse order of Init.
|
|
|
|
// Returned errors are not fatal; they are used for logging.
|
|
|
|
Shutdown() error
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewExtensionFn is a function that instantiates an [Extension].
|
|
|
|
// If a registered extension cannot be instantiated, the function must return an error.
|
|
|
|
// If the extension should be skipped at runtime, it must return either [SkipExtension]
|
|
|
|
// or a wrapped [SkipExtension]. Any other error returned is fatal and will prevent
|
|
|
|
// the LocalBackend from starting.
|
|
|
|
type NewExtensionFn func(logger.Logf, *tsd.System) (Extension, error)
|
|
|
|
|
|
|
|
// SkipExtension is an error returned by [NewExtensionFn] to indicate that the extension
|
|
|
|
// should be skipped rather than prevent the LocalBackend from starting.
|
|
|
|
//
|
|
|
|
// Skipping an extension should be reserved for cases where the extension is not supported
|
|
|
|
// on the current platform or configuration, or depends on a feature that is not available,
|
|
|
|
// or otherwise should be disabled permanently rather than temporarily.
|
|
|
|
//
|
|
|
|
// Specifically, it must not be returned if the extension is not required right now
|
|
|
|
// based on user preferences, policy settings, the current tailnet, or other factors
|
|
|
|
// that may change throughout the LocalBackend's lifetime.
|
|
|
|
var SkipExtension = errors.New("skipping extension")
|
|
|
|
|
|
|
|
// Definition describes a registered [Extension].
|
|
|
|
type Definition struct {
|
|
|
|
name string // name under which the extension is registered
|
|
|
|
newFn NewExtensionFn // function that creates a new instance of the extension
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the name of the extension.
|
|
|
|
func (d *Definition) Name() string {
|
|
|
|
return d.name
|
|
|
|
}
|
|
|
|
|
|
|
|
// MakeExtension instantiates the extension.
|
|
|
|
func (d *Definition) MakeExtension(logf logger.Logf, sys *tsd.System) (Extension, error) {
|
|
|
|
ext, err := d.newFn(logf, sys)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if ext.Name() != d.name {
|
|
|
|
return nil, fmt.Errorf("extension name mismatch: registered %q; actual %q", d.name, ext.Name())
|
|
|
|
}
|
|
|
|
return ext, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// extensionsByName is a map of registered extensions,
|
|
|
|
// where the key is the name of the extension.
|
|
|
|
var extensionsByName map[string]*Definition
|
|
|
|
|
|
|
|
// extensionsByOrder is a slice of registered extensions,
|
|
|
|
// in the order they were registered.
|
|
|
|
var extensionsByOrder []*Definition
|
|
|
|
|
|
|
|
// RegisterExtension registers a function that instantiates an [Extension].
|
|
|
|
// The name must be the same as returned by the extension's [Extension.Name].
|
|
|
|
//
|
|
|
|
// It must be called on the main goroutine before LocalBackend is created,
|
|
|
|
// such as from an init function of the package implementing the extension.
|
|
|
|
//
|
|
|
|
// It panics if newExt is nil or if an extension with the same name
|
|
|
|
// has already been registered.
|
|
|
|
func RegisterExtension(name string, newExt NewExtensionFn) {
|
|
|
|
if newExt == nil {
|
|
|
|
panic(fmt.Sprintf("ipnext: newExt is nil: %q", name))
|
|
|
|
}
|
|
|
|
if _, ok := extensionsByName[name]; ok {
|
|
|
|
panic(fmt.Sprintf("ipnext: duplicate extensions: %q", name))
|
|
|
|
}
|
|
|
|
ext := &Definition{name, newExt}
|
|
|
|
mak.Set(&extensionsByName, name, ext)
|
|
|
|
extensionsByOrder = append(extensionsByOrder, ext)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extensions returns a read-only view of the extensions
|
|
|
|
// registered via [RegisterExtension]. It preserves the order
|
|
|
|
// in which the extensions were registered.
|
|
|
|
func Extensions() views.Slice[*Definition] {
|
|
|
|
return views.SliceOf(extensionsByOrder)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefinitionForTest returns a [Definition] for the specified [Extension].
|
|
|
|
// It is primarily used for testing where the test code needs to instantiate
|
|
|
|
// and use an extension without registering it.
|
|
|
|
func DefinitionForTest(ext Extension) *Definition {
|
|
|
|
return &Definition{
|
|
|
|
name: ext.Name(),
|
|
|
|
newFn: func(logger.Logf, *tsd.System) (Extension, error) { return ext, nil },
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DefinitionWithErrForTest returns a [Definition] with the specified extension name
|
|
|
|
// whose [Definition.MakeExtension] method returns the specified error.
|
|
|
|
// It is used for testing.
|
|
|
|
func DefinitionWithErrForTest(name string, err error) *Definition {
|
|
|
|
return &Definition{
|
|
|
|
name: name,
|
|
|
|
newFn: func(logger.Logf, *tsd.System) (Extension, error) { return nil, err },
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Host is the API surface used by [Extension]s to interact with LocalBackend
|
|
|
|
// in a controlled manner.
|
|
|
|
//
|
|
|
|
// Extensions can register callbacks, request information, or perform actions
|
|
|
|
// via the [Host] interface.
|
|
|
|
//
|
|
|
|
// Typically, the host invokes registered callbacks when one of the following occurs:
|
|
|
|
// - LocalBackend notifies it of an event or state change that may be
|
|
|
|
// of interest to extensions, such as when switching [ipn.LoginProfile].
|
|
|
|
// - LocalBackend needs to consult extensions for information, for example,
|
|
|
|
// determining the most appropriate profile for the current state of the system.
|
|
|
|
// - LocalBackend performs an extensible action, such as logging an auditable event,
|
|
|
|
// and delegates its execution to the extension.
|
|
|
|
//
|
|
|
|
// The callbacks are invoked synchronously, and the LocalBackend's state
|
|
|
|
// remains unchanged while callbacks execute.
|
|
|
|
//
|
|
|
|
// In contrast, actions initiated by extensions are generally asynchronous,
|
|
|
|
// as indicated by the "Async" suffix in their names.
|
|
|
|
// Performing actions may result in callbacks being invoked as described above.
|
|
|
|
//
|
|
|
|
// To prevent conflicts between extensions competing for shared state,
|
|
|
|
// such as the current profile or prefs, the host must not expose methods
|
|
|
|
// that directly modify that state. For example, instead of allowing extensions
|
|
|
|
// to switch profiles at-will, the host's [ProfileServices] provides a method
|
|
|
|
// to switch to the "best" profile. The host can then consult extensions
|
|
|
|
// to determine the appropriate profile to use and resolve any conflicts
|
|
|
|
// in a controlled manner.
|
|
|
|
//
|
|
|
|
// A host must be safe for concurrent use.
|
|
|
|
type Host interface {
|
2025-04-11 10:09:03 -05:00
|
|
|
// Extensions returns the host's [ExtensionServices].
|
|
|
|
Extensions() ExtensionServices
|
|
|
|
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// Profiles returns the host's [ProfileServices].
|
|
|
|
Profiles() ProfileServices
|
|
|
|
|
|
|
|
// 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 is a runtime error to register a nil provider.
|
|
|
|
RegisterAuditLogProvider(AuditLogProvider) (unregister func())
|
|
|
|
|
|
|
|
// AuditLogger returns a function that calls all currently registered audit loggers.
|
|
|
|
// The function fails if any logger returns an error, indicating that the action
|
|
|
|
// cannot be logged and must not be performed.
|
|
|
|
//
|
|
|
|
// The returned function captures the current state (e.g., the current profile) at
|
|
|
|
// the time of the call and must not be persisted.
|
|
|
|
AuditLogger() ipnauth.AuditLogFunc
|
|
|
|
|
|
|
|
// RegisterControlClientCallback registers a function to be called every time a new
|
|
|
|
// control client is created. The returned function unregisters the callback.
|
|
|
|
// It is a runtime error to register a nil callback.
|
|
|
|
RegisterControlClientCallback(NewControlClientCallback) (unregister func())
|
|
|
|
}
|
|
|
|
|
2025-04-11 10:09:03 -05:00
|
|
|
// ExtensionServices provides access to the [Host]'s extension management services,
|
|
|
|
// such as fetching active extensions.
|
|
|
|
type ExtensionServices interface {
|
|
|
|
// FindExtensionByName returns an active extension with the given name,
|
|
|
|
// or nil if no such extension exists.
|
|
|
|
FindExtensionByName(name string) any
|
|
|
|
|
|
|
|
// FindMatchingExtension finds the first active extension that matches target,
|
|
|
|
// and if one is found, sets target to that extension and returns true.
|
|
|
|
// Otherwise, it returns false.
|
|
|
|
//
|
|
|
|
// It panics if target is not a non-nil pointer to either a type
|
|
|
|
// that implements [ipnext.Extension], or to any interface type.
|
|
|
|
FindMatchingExtension(target any) bool
|
|
|
|
}
|
|
|
|
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// ProfileServices provides access to the [Host]'s profile management services,
|
|
|
|
// such as switching profiles and registering profile change callbacks.
|
|
|
|
type ProfileServices interface {
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// CurrentProfileState returns read-only views of the current profile
|
|
|
|
// and its preferences. The returned views are always valid,
|
|
|
|
// but the profile's [ipn.LoginProfileView.ID] returns ""
|
|
|
|
// if the profile is new and has not been persisted yet.
|
|
|
|
//
|
|
|
|
// The returned views are immutable snapshots of the current profile
|
|
|
|
// and prefs at the time of the call. The actual state is only guaranteed
|
|
|
|
// to remain unchanged and match these views for the duration
|
|
|
|
// of a callback invoked by the host, if used within that callback.
|
|
|
|
//
|
|
|
|
// Extensions that need the current profile or prefs at other times
|
|
|
|
// should typically subscribe to [ProfileStateChangeCallback]
|
|
|
|
// to be notified if the profile or prefs change after retrieval.
|
|
|
|
// CurrentProfileState returns both the profile and prefs
|
|
|
|
// to guarantee that they are consistent with each other.
|
|
|
|
CurrentProfileState() (ipn.LoginProfileView, ipn.PrefsView)
|
|
|
|
|
|
|
|
// CurrentPrefs is like [CurrentProfileState] but only returns prefs.
|
|
|
|
CurrentPrefs() ipn.PrefsView
|
|
|
|
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// SwitchToBestProfileAsync asynchronously selects the best profile to use
|
|
|
|
// and switches to it, unless it is already the current profile.
|
|
|
|
//
|
|
|
|
// If an extension needs to know when a profile switch occurs,
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// it must use [ProfileServices.RegisterProfileStateChangeCallback]
|
|
|
|
// to register a [ProfileStateChangeCallback].
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
//
|
|
|
|
// The reason indicates why the profile is being switched, such as due
|
|
|
|
// to a client connecting or disconnecting or a change in the desktop
|
|
|
|
// session state. It is used for logging.
|
|
|
|
SwitchToBestProfileAsync(reason string)
|
|
|
|
|
|
|
|
// RegisterBackgroundProfileResolver registers a function to be used when
|
|
|
|
// resolving the background profile. The returned function unregisters the resolver.
|
|
|
|
// It is a runtime error to register a nil resolver.
|
|
|
|
//
|
|
|
|
// TODO(nickkhyl): allow specifying some kind of priority/altitude for the resolver.
|
|
|
|
// TODO(nickkhyl): make it a "profile resolver" instead of a "background profile resolver".
|
|
|
|
// The concepts of the "current user", "foreground profile" and "background profile"
|
|
|
|
// only exist on Windows, and we're moving away from them anyway.
|
|
|
|
RegisterBackgroundProfileResolver(ProfileResolver) (unregister func())
|
|
|
|
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// RegisterProfileStateChangeCallback registers a function to be called when the current
|
|
|
|
// [ipn.LoginProfile] or its [ipn.Prefs] change. The returned function unregisters the callback.
|
|
|
|
//
|
|
|
|
// To get the initial profile or prefs, use [ProfileServices.CurrentProfileState]
|
|
|
|
// or [ProfileServices.CurrentPrefs] from the extension's [Extension.Init].
|
|
|
|
//
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// It is a runtime error to register a nil callback.
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
RegisterProfileStateChangeCallback(ProfileStateChangeCallback) (unregister func())
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// ProfileStore provides read-only access to available login profiles and their preferences.
|
|
|
|
// It is not safe for concurrent use and can only be used from the callback it is passed to.
|
|
|
|
type ProfileStore interface {
|
|
|
|
// CurrentUserID returns the current user ID. It is only non-empty on
|
|
|
|
// Windows where we have a multi-user system.
|
|
|
|
//
|
|
|
|
// Deprecated: this method exists for compatibility with the current (as of 2024-08-27)
|
|
|
|
// permission model and will be removed as we progress on tailscale/corp#18342.
|
|
|
|
CurrentUserID() ipn.WindowsUserID
|
|
|
|
|
|
|
|
// CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
|
|
|
|
// The returned view is always valid, but the profile's [ipn.LoginProfileView.ID]
|
|
|
|
// returns "" if the profile is new and has not been persisted yet.
|
|
|
|
CurrentProfile() ipn.LoginProfileView
|
|
|
|
|
|
|
|
// CurrentPrefs returns a read-only view of the current prefs.
|
|
|
|
// The returned view is always valid.
|
|
|
|
CurrentPrefs() ipn.PrefsView
|
|
|
|
|
|
|
|
// DefaultUserProfile returns a read-only view of the default (last used) profile for the specified user.
|
|
|
|
// It returns a read-only view of a new, non-persisted profile if the specified user does not have a default profile.
|
|
|
|
DefaultUserProfile(uid ipn.WindowsUserID) ipn.LoginProfileView
|
|
|
|
}
|
|
|
|
|
|
|
|
// AuditLogProvider is a function that returns an [ipnauth.AuditLogFunc] for
|
|
|
|
// logging auditable actions.
|
|
|
|
type AuditLogProvider func() ipnauth.AuditLogFunc
|
|
|
|
|
|
|
|
// ProfileResolver is a function that returns a read-only view of a login profile.
|
|
|
|
// An invalid view indicates no profile. A valid profile view with an empty [ipn.ProfileID]
|
|
|
|
// indicates that the profile is new and has not been persisted yet.
|
|
|
|
// The provided [ProfileStore] can only be used for the duration of the callback.
|
|
|
|
type ProfileResolver func(ProfileStore) ipn.LoginProfileView
|
|
|
|
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// ProfileStateChangeCallback is a function to be called when the current login profile
|
|
|
|
// or its preferences change.
|
|
|
|
//
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
// The sameNode parameter indicates whether the profile represents the same node as before,
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// which is true when:
|
|
|
|
// - Only the profile's [ipn.Prefs] or metadata (e.g., [tailcfg.UserProfile]) have changed,
|
|
|
|
// but the node ID and [ipn.ProfileID] remain the same.
|
|
|
|
// - The profile has been persisted and assigned an [ipn.ProfileID] for the first time,
|
|
|
|
// so while its node ID and [ipn.ProfileID] have changed, it is still the same profile.
|
|
|
|
//
|
|
|
|
// It can be used to decide whether to reset state bound to the current profile or node identity.
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
//
|
|
|
|
// The profile and prefs are always valid, but the profile's [ipn.LoginProfileView.ID]
|
|
|
|
// returns "" if the profile is new and has not been persisted yet.
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
type ProfileStateChangeCallback func(_ ipn.LoginProfileView, _ ipn.PrefsView, sameNode bool)
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
|
|
|
|
// NewControlClientCallback is a function to be called when a new [controlclient.Client]
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
// is created and before it is first used. The specified profile represents the node
|
|
|
|
// for which the cc is created and is always valid. Its [ipn.LoginProfileView.ID]
|
|
|
|
// returns "" if it is a new node whose profile has never been persisted.
|
|
|
|
//
|
|
|
|
// If the [controlclient.Client] is created due to a profile switch, any registered
|
|
|
|
// [ProfileStateChangeCallback]s are called first.
|
cmd/tailscaled,ipn/{auditlog,desktop,ipnext,ipnlocal},tsd: extract LocalBackend extension interfaces and implementation
In this PR, we refactor the LocalBackend extension system, moving from direct callbacks to a more organized extension host model.
Specifically, we:
- Extract interface and callback types used by packages extending LocalBackend functionality into a new ipn/ipnext package.
- Define ipnext.Host as a new interface that bridges extensions with LocalBackend.
It enables extensions to register callbacks and interact with LocalBackend in a concurrency-safe, well-defined, and controlled way.
- Move existing callback registration and invocation code from ipnlocal.LocalBackend into a new type called ipnlocal.ExtensionHost,
implementing ipnext.Host.
- Improve docs for existing types and methods while adding docs for the new interfaces.
- Add test coverage for both the extracted and the new code.
- Remove ipn/desktop.SessionManager from tsd.System since ipn/desktop is now self-contained.
- Update existing extensions (e.g., ipn/auditlog and ipn/desktop) to use the new interfaces where appropriate.
We're not introducing new callback and hook types (e.g., for ipn.Prefs changes) just yet, nor are we enhancing current callbacks,
such as by improving conflict resolution when more than one extension tries to influence profile selection via a background profile resolver.
These further improvements will be submitted separately.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#18342
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-10 20:24:58 -05:00
|
|
|
//
|
|
|
|
// It returns a function to be called when the cc is being shut down,
|
|
|
|
// or nil if no cleanup is needed.
|
ipn/{auditlog,ipnext,ipnlocal}: convert the profile-change callback to a profile-state-change callback
In this PR, we enable extensions to track changes in the current prefs. These changes can result from a profile switch
or from the user or system modifying the current profile’s prefs. Since some extensions may want to distinguish between
the two events, while others may treat them similarly, we rename the existing profile-change callback to become
a profile-state-change callback and invoke it whenever the current profile or its preferences change. Extensions can still
use the sameNode parameter to distinguish between situations where the profile information, including its preferences,
has been updated but still represents the same tailnet node, and situations where a switch to a different profile has been made.
Having dedicated prefs-change callbacks is being considered, but currently seems redundant. A single profile-state-change callback
is easier to maintain. We’ll revisit the idea of adding a separate callback as we progress on extracting existing features from LocalBackend,
but the conversion to a profile-state-change callback is intended to be permanent.
Finally, we let extensions retrieve the current prefs or profile state (profile info + prefs) at any time using the new
CurrentProfileState and CurrentPrefs methods. We also simplify the NewControlClientCallback signature to exclude
profile prefs. It’s optional, and extensions can retrieve the current prefs themselves if needed.
Updates #12614
Updates tailscale/corp#27645
Updates tailscale/corp#26435
Updates tailscale/corp#27502
Signed-off-by: Nick Khyl <nickk@tailscale.com>
2025-04-14 10:45:08 -05:00
|
|
|
type NewControlClientCallback func(controlclient.Client, ipn.LoginProfileView) (cleanup func())
|