client/tailscale,ipn/ipn{local,server},util/syspolicy: implement the AlwaysOn.OverrideWithReason policy setting

In this PR, we update client/tailscale.LocalClient to allow sending requests with an optional X-Tailscale-Reason
header. We then update ipn/ipnserver.{actor,Server} to retrieve this reason, if specified, and use it to determine
whether ipnauth.Disconnect is allowed when the AlwaysOn.OverrideWithReason policy setting is enabled.
For now, we log the reason, along with the profile and OS username, to the backend log.

Finally, we update LocalBackend to remember when a disconnect was permitted and do not reconnect automatically
unless the policy changes.

Updates tailscale/corp#26146

Signed-off-by: Nick Khyl <nickk@tailscale.com>
This commit is contained in:
Nick Khyl
2025-01-31 16:14:13 -06:00
committed by Nick Khyl
parent 2c02f712d1
commit d832467461
7 changed files with 125 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ package ipnserver
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -20,6 +21,7 @@ import (
"sync/atomic"
"unicode"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
@@ -198,10 +200,18 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
if actor, ok := ci.(*actor); ok {
lah.PermitRead, lah.PermitWrite = actor.Permissions(lb.OperatorUserID())
lah.PermitCert = actor.CanFetchCerts()
reason, err := base64.StdEncoding.DecodeString(r.Header.Get(apitype.RequestReasonHeader))
if err != nil {
http.Error(w, "invalid reason header", http.StatusBadRequest)
return
}
lah.Actor = actorWithAccessOverride(actor, string(reason))
} else if testenv.InTest() {
lah.PermitRead, lah.PermitWrite = true, true
}
lah.Actor = ci
if lah.Actor == nil {
lah.Actor = ci
}
lah.ServeHTTP(w, r)
return
}