mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-19 19:38:40 +00:00
ipn/ipn{auth,server,local}: initial support for the always-on mode
In this PR, we update LocalBackend to set WantRunning=true when applying policy settings to the current profile's prefs, if the "always-on" mode is enabled. We also implement a new (*LocalBackend).EditPrefsAs() method, which is like EditPrefs but accepts an actor (e.g., a LocalAPI client's identity) that initiated the change. If WantRunning is being set to false, the new EditPrefsAs method checks whether the actor has ipnauth.Disconnect access to the profile and propagates an error if they do not. Finally, we update (*ipnserver.actor).CheckProfileAccess to allow a disconnect only if the "always-on" mode is not enabled by the AlwaysOn policy setting. This is not a comprehensive solution to the "always-on" mode across platforms, as instead of disconnecting a user could achieve the same effect by creating a new empty profile, initiating a reauth, or by deleting the profile. These are the things we should address in future PRs. Updates #14823 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
535a3dbebd
commit
02ad21717f
@ -6,3 +6,9 @@ package ipnauth
|
||||
// ProfileAccess is a bitmask representing the requested, required, or granted
|
||||
// access rights to an [ipn.LoginProfile].
|
||||
type ProfileAccess uint32
|
||||
|
||||
// Define access rights that might be granted or denied on a per-profile basis.
|
||||
const (
|
||||
// Disconnect is required to disconnect (or switch from) a Tailscale profile.
|
||||
Disconnect = ProfileAccess(1 << iota)
|
||||
)
|
||||
|
@ -1795,6 +1795,11 @@ func applySysPolicy(prefs *ipn.Prefs, lastSuggestedExitNode tailcfg.StableNodeID
|
||||
}
|
||||
}
|
||||
|
||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); alwaysOn && !prefs.WantRunning {
|
||||
prefs.WantRunning = true
|
||||
anyChange = true
|
||||
}
|
||||
|
||||
for _, opt := range preferencePolicies {
|
||||
if po, err := syspolicy.GetPreferenceOption(opt.key); err == nil {
|
||||
curVal := opt.get(prefs.View())
|
||||
@ -3984,7 +3989,15 @@ func (b *LocalBackend) MaybeClearAppConnector(mp *ipn.MaskedPrefs) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// EditPrefs applies the changes in mp to the current prefs,
|
||||
// acting as the tailscaled itself rather than a specific user.
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
return b.EditPrefsAs(mp, ipnauth.Self)
|
||||
}
|
||||
|
||||
// EditPrefsAs is like EditPrefs, but makes the change as the specified actor.
|
||||
// It returns an error if the actor is not allowed to make the change.
|
||||
func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ipn.PrefsView, error) {
|
||||
if mp.SetsInternal() {
|
||||
return ipn.PrefsView{}, errors.New("can't set Internal fields")
|
||||
}
|
||||
@ -3995,8 +4008,20 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
mp.InternalExitNodePriorSet = true
|
||||
}
|
||||
|
||||
// Acquire the lock before checking the profile access to prevent
|
||||
// TOCTOU issues caused by the current profile changing between the
|
||||
// check and the actual edit.
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
if mp.WantRunningSet && !mp.WantRunning && b.pm.CurrentPrefs().WantRunning() {
|
||||
if err := actor.CheckProfileAccess(b.pm.CurrentProfile(), ipnauth.Disconnect); err != nil {
|
||||
return ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
// TODO(nickkhyl): check the ReconnectAfter policy here. If configured,
|
||||
// start a timer to automatically reconnect after the specified duration.
|
||||
}
|
||||
|
||||
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/ctxkey"
|
||||
"tailscale.com/util/osuser"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@ -63,7 +64,17 @@ func (a *actor) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess
|
||||
if profile.LocalUserID() != a.UserID() {
|
||||
return errors.New("the target profile does not belong to the user")
|
||||
}
|
||||
return errors.New("the requested operation is not allowed")
|
||||
switch requestedAccess {
|
||||
case ipnauth.Disconnect:
|
||||
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); alwaysOn {
|
||||
// TODO(nickkhyl): check if disconnecting with justifications is allowed
|
||||
// and whether a justification is included in the request.
|
||||
return errors.New("profile access denied: always-on mode is enabled")
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return errors.New("the requested operation is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// IsLocalSystem implements [ipnauth.Actor].
|
||||
|
@ -1381,7 +1381,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var err error
|
||||
prefs, err = h.b.EditPrefs(mp)
|
||||
prefs, err = h.b.EditPrefsAs(mp, h.Actor)
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
@ -26,6 +26,15 @@ const (
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
Tailnet Key = "Tailnet" // default ""; if blank, no tailnet name is sent to the server.
|
||||
|
||||
// AlwaysOn is a boolean key that controls whether Tailscale
|
||||
// should always remain in a connected state, and the user should
|
||||
// not be able to disconnect at their discretion.
|
||||
//
|
||||
// Warning: This policy setting is experimental and may change or be removed in the future.
|
||||
// It may also not be fully supported by all Tailscale clients until it is out of experimental status.
|
||||
AlwaysOn Key = "AlwaysOn"
|
||||
|
||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
||||
// Exit node ID takes precedence over exit node IP.
|
||||
// To find the node ID, go to /api.md#device.
|
||||
@ -139,6 +148,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(AlwaysOn, setting.DeviceSetting, setting.BooleanValue),
|
||||
setting.NewDefinition(ApplyUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
setting.NewDefinition(AuthKey, setting.DeviceSetting, setting.StringValue),
|
||||
setting.NewDefinition(CheckUpdates, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||
|
Loading…
x
Reference in New Issue
Block a user