util/syspolicy: finish plumbing policyclient, add feature/syspolicy, move global impl

This is step 4 of making syspolicy a build-time feature.

This adds a policyclient.Get() accessor to return the correct
implementation to use: either the real one, or the no-op one. (A third
type, a static one for testing, also exists, so in general a
policyclient.Client should be plumbed around and not always fetched
via policyclient.Get whenever possible, especially if tests need to use
alternate syspolicy)

Updates #16998
Updates #12614

Change-Id: Iaf19670744a596d5918acfa744f5db4564272978
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2025-09-02 12:49:37 -07:00
committed by Brad Fitzpatrick
parent 9e9bf13063
commit 2b3e533048
44 changed files with 242 additions and 207 deletions
+2 -2
View File
@@ -18,8 +18,8 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnext"
"tailscale.com/types/logger"
"tailscale.com/util/syspolicy"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
)
// featureName is the name of the feature implemented by this package.
@@ -136,7 +136,7 @@ func (e *desktopSessionsExt) getBackgroundProfile(profiles ipnext.ProfileStore)
e.mu.Lock()
defer e.mu.Unlock()
if alwaysOn, _ := syspolicy.GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
if alwaysOn, _ := policyclient.Get().GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
// If the Always-On mode is disabled, there's no background profile
// as far as the desktop session extension is concerned.
return ipn.LoginProfileView{}
+3 -3
View File
@@ -10,8 +10,8 @@ import (
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/util/syspolicy"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
)
type actorWithPolicyChecks struct{ Actor }
@@ -51,10 +51,10 @@ func (a actorWithPolicyChecks) CheckProfileAccess(profile ipn.LoginProfileView,
// TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
// and corp to this package.
func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditFn AuditLogFunc) error {
if alwaysOn, _ := syspolicy.GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
if alwaysOn, _ := policyclient.Get().GetBoolean(pkey.AlwaysOn, false); !alwaysOn {
return nil
}
if allowWithReason, _ := syspolicy.GetBoolean(pkey.AlwaysOnOverrideWithReason, false); !allowWithReason {
if allowWithReason, _ := policyclient.Get().GetBoolean(pkey.AlwaysOnOverrideWithReason, false); !allowWithReason {
return errors.New("disconnect not allowed: always-on mode is enabled")
}
if reason == "" {
+2 -1
View File
@@ -30,6 +30,7 @@ import (
"tailscale.com/util/goroutines"
"tailscale.com/util/set"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -342,7 +343,7 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
// this will first check syspolicy, MDM settings like Registry
// on Windows or defaults on macOS. If they are not set, it falls
// back to the cli-flag, `--posture-checking`.
choice, err := b.polc.GetPreferenceOption(pkey.PostureChecking)
choice, err := b.polc.GetPreferenceOption(pkey.PostureChecking, ptype.ShowChoiceByPolicy)
if err != nil {
b.logf(
"c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s",
+11 -10
View File
@@ -109,6 +109,7 @@ import (
"tailscale.com/util/slicesx"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/util/syspolicy/ptype"
"tailscale.com/util/systemd"
"tailscale.com/util/testenv"
"tailscale.com/util/usermetric"
@@ -1610,7 +1611,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
// future "tailscale up" to start checking for
// implicit setting reverts, which it doesn't do when
// ControlURL is blank.
prefs.ControlURL = prefs.ControlURLOrDefault()
prefs.ControlURL = prefs.ControlURLOrDefault(b.polc)
prefsChanged = true
}
if st.Persist.Valid() {
@@ -1870,7 +1871,7 @@ func (b *LocalBackend) applySysPolicyLocked(prefs *ipn.Prefs) (anyChange bool) {
}
for _, opt := range preferencePolicies {
if po, err := b.polc.GetPreferenceOption(opt.key); err == nil {
if po, err := b.polc.GetPreferenceOption(opt.key, ptype.ShowChoiceByPolicy); err == nil {
curVal := opt.get(prefs.View())
newVal := po.ShouldEnable(curVal)
if curVal != newVal {
@@ -2425,7 +2426,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
loggedOut := prefs.LoggedOut()
serverURL := prefs.ControlURLOrDefault()
serverURL := prefs.ControlURLOrDefault(b.polc)
if inServerMode := prefs.ForceDaemon(); inServerMode || runtime.GOOS == "windows" {
b.logf("Start: serverMode=%v", inServerMode)
}
@@ -3498,7 +3499,7 @@ func (b *LocalBackend) validPopBrowserURLLocked(urlStr string) bool {
if err != nil {
return false
}
serverURL := b.sanitizedPrefsLocked().ControlURLOrDefault()
serverURL := b.sanitizedPrefsLocked().ControlURLOrDefault(b.polc)
if ipn.IsLoginServerSynonym(serverURL) {
// When connected to the official Tailscale control plane, only allow
// URLs from tailscale.com or its subdomains.
@@ -4049,7 +4050,7 @@ func (b *LocalBackend) SwitchToBestProfile(reason string) {
// but b.mu must held on entry. It is released on exit.
func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock unlockOnce) {
defer unlock()
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(b.polc)
profile, background := b.resolveBestProfileLocked()
cp, switched, err := b.pm.SwitchToProfile(profile)
switch {
@@ -4076,7 +4077,7 @@ func (b *LocalBackend) switchToBestProfileLockedOnEntry(reason string, unlock un
return
}
// As an optimization, only reset the dialPlan if the control URL changed.
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(); oldControlURL != newControlURL {
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(b.polc); oldControlURL != newControlURL {
b.resetDialPlan()
}
if err := b.resetForProfileChangeLockedOnEntry(unlock); err != nil {
@@ -4250,7 +4251,7 @@ func (b *LocalBackend) isDefaultServerLocked() bool {
if !prefs.Valid() {
return true // assume true until set otherwise
}
return prefs.ControlURLOrDefault() == ipn.DefaultControlURL
return prefs.ControlURLOrDefault(b.polc) == ipn.DefaultControlURL
}
var exitNodeMisconfigurationWarnable = health.Register(&health.Warnable{
@@ -5687,7 +5688,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
// Some temporary (2024-05-05) debugging code to help us catch
// https://github.com/tailscale/tailscale/issues/11962 in the act.
if prefs.WantRunning() &&
prefs.ControlURLOrDefault() == ipn.DefaultControlURL &&
prefs.ControlURLOrDefault(b.polc) == ipn.DefaultControlURL &&
envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") {
panic("[unexpected] use of main control server in integration test")
}
@@ -7288,13 +7289,13 @@ func (b *LocalBackend) SwitchProfile(profile ipn.ProfileID) error {
unlock := b.lockAndGetUnlock()
defer unlock()
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault()
oldControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(b.polc)
if _, changed, err := b.pm.SwitchToProfileByID(profile); !changed || err != nil {
return err // nil if we're already on the target profile
}
// As an optimization, only reset the dialPlan if the control URL changed.
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(); oldControlURL != newControlURL {
if newControlURL := b.pm.CurrentPrefs().ControlURLOrDefault(b.polc); oldControlURL != newControlURL {
b.resetDialPlan()
}
+8 -8
View File
@@ -28,8 +28,8 @@ import (
"tailscale.com/types/preftype"
"tailscale.com/types/views"
"tailscale.com/util/dnsname"
"tailscale.com/util/syspolicy"
"tailscale.com/util/syspolicy/pkey"
"tailscale.com/util/syspolicy/policyclient"
"tailscale.com/version"
)
@@ -718,16 +718,16 @@ func NewPrefs() *Prefs {
//
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p PrefsView) ControlURLOrDefault() string {
return p.ж.ControlURLOrDefault()
func (p PrefsView) ControlURLOrDefault(polc policyclient.Client) string {
return p.ж.ControlURLOrDefault(polc)
}
// ControlURLOrDefault returns the coordination server's URL base.
//
// If not configured, or if the configured value is a legacy name equivalent to
// the default, then DefaultControlURL is returned instead.
func (p *Prefs) ControlURLOrDefault() string {
controlURL, err := syspolicy.GetString(pkey.ControlURL, p.ControlURL)
func (p *Prefs) ControlURLOrDefault(polc policyclient.Client) string {
controlURL, err := polc.GetString(pkey.ControlURL, p.ControlURL)
if err != nil {
controlURL = p.ControlURL
}
@@ -756,11 +756,11 @@ func (p *Prefs) DefaultRouteAll(goos string) bool {
}
// AdminPageURL returns the admin web site URL for the current ControlURL.
func (p PrefsView) AdminPageURL() string { return p.ж.AdminPageURL() }
func (p PrefsView) AdminPageURL(polc policyclient.Client) string { return p.ж.AdminPageURL(polc) }
// AdminPageURL returns the admin web site URL for the current ControlURL.
func (p *Prefs) AdminPageURL() string {
url := p.ControlURLOrDefault()
func (p *Prefs) AdminPageURL(polc policyclient.Client) string {
url := p.ControlURLOrDefault(polc)
if IsLoginServerSynonym(url) {
// TODO(crawshaw): In future release, make this https://console.tailscale.com
url = "https://login.tailscale.com"
+5 -3
View File
@@ -23,6 +23,7 @@ import (
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/util/syspolicy/policyclient"
)
func fieldsOf(t reflect.Type) (fields []string) {
@@ -1032,15 +1033,16 @@ func TestExitNodeIPOfArg(t *testing.T) {
func TestControlURLOrDefault(t *testing.T) {
var p Prefs
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
polc := policyclient.NoPolicyClient{}
if got, want := p.ControlURLOrDefault(polc), DefaultControlURL; got != want {
t.Errorf("got %q; want %q", got, want)
}
p.ControlURL = "http://foo.bar"
if got, want := p.ControlURLOrDefault(), "http://foo.bar"; got != want {
if got, want := p.ControlURLOrDefault(polc), "http://foo.bar"; got != want {
t.Errorf("got %q; want %q", got, want)
}
p.ControlURL = "https://login.tailscale.com"
if got, want := p.ControlURLOrDefault(), DefaultControlURL; got != want {
if got, want := p.ControlURLOrDefault(polc), DefaultControlURL; got != want {
t.Errorf("got %q; want %q", got, want)
}
}