ipn/ipnlocal,util/syspolicy: add support for ExitNode.AllowOverride policy setting

When the policy setting is enabled, it allows users to override the exit node enforced by the ExitNodeID
or ExitNodeIP policy. It's primarily intended for use when ExitNodeID is set to auto:any, but it can also
be used with specific exit nodes. It does not allow disabling exit node usage entirely.

Once the exit node policy is overridden, it will not be enforced again until the policy changes,
the user connects or disconnects Tailscale, switches profiles, or disables the override.

Updates tailscale/corp#29969

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2025-07-08 16:08:28 -05:00
committed by Nick Khyl
parent 2c630e126b
commit 740b77df59
4 changed files with 434 additions and 12 deletions

View File

@@ -54,6 +54,15 @@ const (
ExitNodeID Key = "ExitNodeID"
ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
// AllowExitNodeOverride is a boolean key that allows the user to override exit node policy settings
// and manually select an exit node. It does not allow disabling exit node usage entirely.
// It is typically used in conjunction with [ExitNodeID] set to "auto:any".
//
// Warning: This policy setting is experimental and may change, be renamed or removed in the future.
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
// See tailscale/corp#29969.
AllowExitNodeOverride Key = "ExitNode.AllowOverride"
// Keys with a string value that specifies an option: "always", "never", "user-decides".
// The default is "user-decides" unless otherwise stated. Enforcement of
// these policies is typically performed in ipnlocal.applySysPolicy(). GUIs
@@ -173,6 +182,7 @@ const (
var implicitDefinitions = []*setting.Definition{
// Device policy settings (can only be configured on a per-device basis):
setting.NewDefinition(AllowedSuggestedExitNodes, setting.DeviceSetting, setting.StringListValue),
setting.NewDefinition(AllowExitNodeOverride, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(AlwaysOnOverrideWithReason, setting.DeviceSetting, setting.BooleanValue),
setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),

View File

@@ -59,6 +59,11 @@ func (c PolicyChange) HasChanged(key setting.Key) bool {
}
}
// HasChangedAnyOf reports whether any of the specified policy settings has changed.
func (c PolicyChange) HasChangedAnyOf(keys ...setting.Key) bool {
return slices.ContainsFunc(keys, c.HasChanged)
}
// policyChangeCallbacks are the callbacks to invoke when the effective policy changes.
// It is safe for concurrent use.
type policyChangeCallbacks struct {