mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-27 18:57:35 +00:00
ipn/ipnlocal,util/syspolicy,docs/windows/policy: implement the ReconnectAfter policy setting
In this PR, we update the LocalBackend so that when the ReconnectAfter policy setting is configured and a user disconnects Tailscale by setting WantRunning to false in the profile prefs, the LocalBackend will now start a timer to set WantRunning back to true once the ReconnectAfter timer expires. We also update the ADMX/ADML policy definitions to allow configuring this policy setting for Windows via Group Policy and Intune. Updates #14824 Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
parent
d1b0e1af06
commit
8d7033fe7f
@ -109,6 +109,14 @@ If you enable this policy setting, users will not be allowed to disconnect Tails
|
|||||||
If necessary, it can be used along with Unattended Mode to keep Tailscale connected regardless of whether a user is logged in. This can be used to facilitate remote access to a device or ensure connectivity to a Domain Controller before a user logs in.
|
If necessary, it can be used along with Unattended Mode to keep Tailscale connected regardless of whether a user is logged in. This can be used to facilitate remote access to a device or ensure connectivity to a Domain Controller before a user logs in.
|
||||||
|
|
||||||
If you disable or don't configure this policy setting, users will be allowed to disconnect Tailscale at their will.]]></string>
|
If you disable or don't configure this policy setting, users will be allowed to disconnect Tailscale at their will.]]></string>
|
||||||
|
<string id="ReconnectAfter">Configure automatic reconnect delay</string>
|
||||||
|
<string id="ReconnectAfter_Help"><![CDATA[This policy setting controls when Tailscale will attempt to reconnect automatically after a user disconnects it. It helps users remain connected most of the time and retain access to corporate resources without preventing them from temporarily disconnecting Tailscale. To configure whether and when Tailscale can be disconnected, see the "Restrict users from disconnecting Tailscale (always-on mode)" policy setting.
|
||||||
|
|
||||||
|
If you enable this policy setting, you can specify how long Tailscale will wait before attempting to reconnect after a user disconnects. The value should be specified as a Go duration: for example, 30s, 5m, or 1h30m. If the value is left blank, or if the specified duration is zero, Tailscale will not attempt to reconnect automatically.
|
||||||
|
|
||||||
|
If you disable or don't configure this policy setting, Tailscale will only reconnect if a user chooses to or if required by a different policy setting.
|
||||||
|
|
||||||
|
Refer to https://pkg.go.dev/time#ParseDuration for information about the supported duration strings.]]></string>
|
||||||
<string id="ExitNodeAllowLANAccess">Allow Local Network Access when an Exit Node is in use</string>
|
<string id="ExitNodeAllowLANAccess">Allow Local Network Access when an Exit Node is in use</string>
|
||||||
<string id="ExitNodeAllowLANAccess_Help"><![CDATA[This policy can be used to require that the Allow Local Network Access setting is configured a certain way.
|
<string id="ExitNodeAllowLANAccess_Help"><![CDATA[This policy can be used to require that the Allow Local Network Access setting is configured a certain way.
|
||||||
|
|
||||||
@ -280,6 +288,12 @@ See https://tailscale.com/kb/1315/mdm-keys#set-your-organization-name for more d
|
|||||||
<text>The options below allow configuring exceptions where disconnecting Tailscale is permitted.</text>
|
<text>The options below allow configuring exceptions where disconnecting Tailscale is permitted.</text>
|
||||||
<dropdownList refId="AlwaysOn_OverrideWithReason" noSort="true" defaultItem="0">Disconnects with reason:</dropdownList>
|
<dropdownList refId="AlwaysOn_OverrideWithReason" noSort="true" defaultItem="0">Disconnects with reason:</dropdownList>
|
||||||
</presentation>
|
</presentation>
|
||||||
|
<presentation id="ReconnectAfter">
|
||||||
|
<text>The delay must be a valid Go duration string, such as 30s, 5m, or 1h30m, all without spaces or any other symbols.</text>
|
||||||
|
<textBox refId="ReconnectAfterDelay">
|
||||||
|
<label>Reconnect after:</label>
|
||||||
|
</textBox>
|
||||||
|
</presentation>
|
||||||
<presentation id="ExitNodeID">
|
<presentation id="ExitNodeID">
|
||||||
<textBox refId="ExitNodeIDPrompt">
|
<textBox refId="ExitNodeIDPrompt">
|
||||||
<label>Exit Node:</label>
|
<label>Exit Node:</label>
|
||||||
|
@ -156,6 +156,13 @@
|
|||||||
</enum>
|
</enum>
|
||||||
</elements>
|
</elements>
|
||||||
</policy>
|
</policy>
|
||||||
|
<policy name="ReconnectAfter" class="Machine" displayName="$(string.ReconnectAfter)" explainText="$(string.ReconnectAfter_Help)" presentation="$(presentation.ReconnectAfter)" key="Software\Policies\Tailscale">
|
||||||
|
<parentCategory ref="Settings_Category" />
|
||||||
|
<supportedOn ref="SINCE_V1_82" />
|
||||||
|
<elements>
|
||||||
|
<text id="ReconnectAfterDelay" valueName="ReconnectAfter" required="true" />
|
||||||
|
</elements>
|
||||||
|
</policy>
|
||||||
<policy name="ExitNodeAllowLANAccess" class="Machine" displayName="$(string.ExitNodeAllowLANAccess)" explainText="$(string.ExitNodeAllowLANAccess_Help)" key="Software\Policies\Tailscale" valueName="ExitNodeAllowLANAccess">
|
<policy name="ExitNodeAllowLANAccess" class="Machine" displayName="$(string.ExitNodeAllowLANAccess)" explainText="$(string.ExitNodeAllowLANAccess_Help)" key="Software\Policies\Tailscale" valueName="ExitNodeAllowLANAccess">
|
||||||
<parentCategory ref="Settings_Category" />
|
<parentCategory ref="Settings_Category" />
|
||||||
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
|
<supportedOn ref="PARTIAL_FULL_SINCE_V1_56" />
|
||||||
|
@ -442,6 +442,10 @@ type LocalBackend struct {
|
|||||||
// See tailscale/corp#26146.
|
// See tailscale/corp#26146.
|
||||||
overrideAlwaysOn bool
|
overrideAlwaysOn bool
|
||||||
|
|
||||||
|
// reconnectTimer is used to schedule a reconnect by setting [ipn.Prefs.WantRunning]
|
||||||
|
// to true after a delay, or nil if no reconnect is scheduled.
|
||||||
|
reconnectTimer tstime.TimerController
|
||||||
|
|
||||||
// shutdownCbs are the callbacks to be called when the backend is shutting down.
|
// shutdownCbs are the callbacks to be called when the backend is shutting down.
|
||||||
// Each callback is called exactly once in unspecified order and without b.mu held.
|
// Each callback is called exactly once in unspecified order and without b.mu held.
|
||||||
// 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.
|
||||||
@ -1070,6 +1074,8 @@ func (b *LocalBackend) Shutdown() {
|
|||||||
b.captiveCancel()
|
b.captiveCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.stopReconnectTimerLocked()
|
||||||
|
|
||||||
if b.loginFlags&controlclient.LoginEphemeral != 0 {
|
if b.loginFlags&controlclient.LoginEphemeral != 0 {
|
||||||
b.mu.Unlock()
|
b.mu.Unlock()
|
||||||
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(b.ctx, 5*time.Second)
|
||||||
@ -4297,15 +4303,75 @@ func (b *LocalBackend) EditPrefsAs(mp *ipn.MaskedPrefs, actor ipnauth.Actor) (ip
|
|||||||
// mode on them until the policy changes, they switch to a different profile, etc.
|
// mode on them until the policy changes, they switch to a different profile, etc.
|
||||||
b.overrideAlwaysOn = true
|
b.overrideAlwaysOn = true
|
||||||
|
|
||||||
// TODO(nickkhyl): check the ReconnectAfter policy here. If configured,
|
if reconnectAfter, _ := syspolicy.GetDuration(syspolicy.ReconnectAfter, 0); reconnectAfter > 0 {
|
||||||
// start a timer to automatically reconnect after the specified duration.
|
b.startReconnectTimerLocked(reconnectAfter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.editPrefsLockedOnEntry(mp, unlock)
|
return b.editPrefsLockedOnEntry(mp, unlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startReconnectTimerLocked sets a timer to automatically set WantRunning to true
|
||||||
|
// after the specified duration.
|
||||||
|
func (b *LocalBackend) startReconnectTimerLocked(d time.Duration) {
|
||||||
|
if b.reconnectTimer != nil {
|
||||||
|
// Stop may return false if the timer has already fired,
|
||||||
|
// and the function has been called in its own goroutine,
|
||||||
|
// but lost the race to acquire b.mu. In this case, it'll
|
||||||
|
// end up as a no-op due to a reconnectTimer mismatch
|
||||||
|
// once it manages to acquire the lock. This is fine, and we
|
||||||
|
// don't need to check the return value.
|
||||||
|
b.reconnectTimer.Stop()
|
||||||
|
}
|
||||||
|
profileID := b.pm.CurrentProfile().ID()
|
||||||
|
var reconnectTimer tstime.TimerController
|
||||||
|
reconnectTimer = b.clock.AfterFunc(d, func() {
|
||||||
|
unlock := b.lockAndGetUnlock()
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
if b.reconnectTimer != reconnectTimer {
|
||||||
|
// We're either not the most recent timer, or we lost the race when
|
||||||
|
// the timer was stopped. No need to reconnect.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
b.reconnectTimer = nil
|
||||||
|
|
||||||
|
cp := b.pm.CurrentProfile()
|
||||||
|
if cp.ID() != profileID {
|
||||||
|
// The timer fired before the profile changed but we lost the race
|
||||||
|
// and acquired the lock shortly after.
|
||||||
|
// No need to reconnect.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mp := &ipn.MaskedPrefs{WantRunningSet: true, Prefs: ipn.Prefs{WantRunning: true}}
|
||||||
|
if _, err := b.editPrefsLockedOnEntry(mp, unlock); err != nil {
|
||||||
|
b.logf("failed to automatically reconnect as %q after %v: %v", cp.Name(), d, err)
|
||||||
|
} else {
|
||||||
|
b.logf("automatically reconnected as %q after %v", cp.Name(), d)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
b.reconnectTimer = reconnectTimer
|
||||||
|
b.logf("reconnect for %q has been scheduled and will be performed in %v", b.pm.CurrentProfile().Name(), d)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) resetAlwaysOnOverrideLocked() {
|
func (b *LocalBackend) resetAlwaysOnOverrideLocked() {
|
||||||
b.overrideAlwaysOn = false
|
b.overrideAlwaysOn = false
|
||||||
|
b.stopReconnectTimerLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) stopReconnectTimerLocked() {
|
||||||
|
if b.reconnectTimer != nil {
|
||||||
|
// Stop may return false if the timer has already fired,
|
||||||
|
// and the function has been called in its own goroutine,
|
||||||
|
// but lost the race to acquire b.mu.
|
||||||
|
// In this case, it'll end up as a no-op due to a reconnectTimer
|
||||||
|
// mismatch (see [LocalBackend.startReconnectTimerLocked])
|
||||||
|
// once it manages to acquire the lock. This is fine, and we
|
||||||
|
// don't need to check the return value.
|
||||||
|
b.reconnectTimer.Stop()
|
||||||
|
b.reconnectTimer = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warning: b.mu must be held on entry, but it unlocks it on the way out.
|
// Warning: b.mu must be held on entry, but it unlocks it on the way out.
|
||||||
@ -4399,7 +4465,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
|||||||
if oldp.Valid() {
|
if oldp.Valid() {
|
||||||
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
|
newp.Persist = oldp.Persist().AsStruct() // caller isn't allowed to override this
|
||||||
}
|
}
|
||||||
// applySysPolicyToPrefsLocked returns whether it updated newp,
|
// applySysPolicy returns whether it updated newp,
|
||||||
// but everything in this function treats b.prefs as completely new
|
// but everything in this function treats b.prefs as completely new
|
||||||
// anyway, so its return value can be ignored here.
|
// anyway, so its return value can be ignored here.
|
||||||
applySysPolicy(newp, b.lastSuggestedExitNode, b.overrideAlwaysOn)
|
applySysPolicy(newp, b.lastSuggestedExitNode, b.overrideAlwaysOn)
|
||||||
|
@ -42,6 +42,12 @@ const (
|
|||||||
// for auditing purposes. It has no effect when [AlwaysOn] is false.
|
// for auditing purposes. It has no effect when [AlwaysOn] is false.
|
||||||
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
|
AlwaysOnOverrideWithReason Key = "AlwaysOn.OverrideWithReason"
|
||||||
|
|
||||||
|
// ReconnectAfter is a string value formatted for use with time.ParseDuration()
|
||||||
|
// that defines the duration after which the client should automatically reconnect
|
||||||
|
// to the Tailscale network following a user-initiated disconnect.
|
||||||
|
// An empty string or a zero duration disables automatic reconnection.
|
||||||
|
ReconnectAfter Key = "ReconnectAfter"
|
||||||
|
|
||||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
// 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.
|
// Exit node ID takes precedence over exit node IP.
|
||||||
// To find the node ID, go to /api.md#device.
|
// To find the node ID, go to /api.md#device.
|
||||||
@ -176,6 +182,7 @@ var implicitDefinitions = []*setting.Definition{
|
|||||||
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
|
setting.NewDefinition(LogTarget, setting.DeviceSetting, setting.StringValue),
|
||||||
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
setting.NewDefinition(MachineCertificateSubject, setting.DeviceSetting, setting.StringValue),
|
||||||
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
setting.NewDefinition(PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||||
|
setting.NewDefinition(ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||||
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
|
setting.NewDefinition(Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||||
|
|
||||||
// User policy settings (can be configured on a user- or device-basis):
|
// User policy settings (can be configured on a user- or device-basis):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user