Compare commits

...

1 Commits

Author SHA1 Message Date
Andrew Lytvynov
5b419eccbb health: allow hiding arbitrary warnable codes
If we add a warning that has false positives, or is expected by the
user, the user can now hide it. This can be done via "tailscale set" or
MDM policies.

Updates #3198

Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
2024-11-27 15:33:16 -08:00
10 changed files with 86 additions and 5 deletions

View File

@@ -17,6 +17,7 @@ import (
"tailscale.com/client/web"
"tailscale.com/clientupdate"
"tailscale.com/cmd/tailscale/cli/ffcomplete"
"tailscale.com/health"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/net/tsaddr"
@@ -62,6 +63,7 @@ type setArgsT struct {
snat bool
statefulFiltering bool
netfilterMode string
hideHealthWarnings string
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -82,6 +84,7 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "automatically update to the latest available version")
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, hidden+"allow management plane to gather device posture information")
setf.BoolVar(&setArgs.runWebClient, "webclient", false, "expose the web interface for managing this node over Tailscale at port 5252")
setf.StringVar(&setArgs.hideHealthWarnings, "hide-health-warnings", "", fmt.Sprintf("a comma-separated list of health warnings to hide; known codes: %v", health.RegisteredCodes()))
ffcomplete.Flag(setf, "exit-node", func(args []string) ([]string, ffcomplete.ShellCompDirective, error) {
st, err := localClient.Status(context.Background())
@@ -179,7 +182,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
}
warnOnAdvertiseRouts(ctx, &maskedPrefs.Prefs)
var advertiseExitNodeSet, advertiseRoutesSet bool
var advertiseExitNodeSet, advertiseRoutesSet, hideHealthWarningsSet bool
setFlagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpOrSetFlag(maskedPrefs, f.Name)
switch f.Name {
@@ -187,8 +190,17 @@ func runSet(ctx context.Context, args []string) (retErr error) {
advertiseExitNodeSet = true
case "advertise-routes":
advertiseRoutesSet = true
case "hide-health-warnings":
hideHealthWarningsSet = true
}
})
if hideHealthWarningsSet {
var hide []health.WarnableCode
for _, w := range strings.Split(setArgs.hideHealthWarnings, ",") {
hide = append(hide, health.WarnableCode(strings.TrimSpace(w)))
}
maskedPrefs.Prefs.HideHealthWarnings = hide
}
if maskedPrefs.IsEmpty() {
return flag.ErrHelp
}

View File

@@ -777,6 +777,7 @@ func init() {
addPrefFlagMapping("auto-update", "AutoUpdate.Apply")
addPrefFlagMapping("advertise-connector", "AppConnector")
addPrefFlagMapping("posture-checking", "PostureChecking")
addPrefFlagMapping("hide-health-warnings", "HideHealthWarnings")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {

View File

@@ -14,6 +14,7 @@ import (
"net/http"
"os"
"runtime"
"slices"
"sort"
"sync"
"sync/atomic"
@@ -81,6 +82,8 @@ type Tracker struct {
// pendingVisibleTimers contains timers for Warnables that are unhealthy, but are
// not visible to the user yet, because they haven't been unhealthy for TimeToVisible
pendingVisibleTimers map[*Warnable]*time.Timer
// hideWarnables is a list of warnables to skip in Tracker output.
hideWarnables []WarnableCode
// sysErr maps subsystems to their current error (or nil if the subsystem is healthy)
// Deprecated: using Warnables should be preferred
@@ -177,6 +180,11 @@ func Register(w *Warnable) *Warnable {
return w
}
// RegisteredCodes returns the list of all registered Warnable codes.
func RegisteredCodes() []WarnableCode {
return slices.Collect(maps.Keys(registeredWarnables))
}
// unregister removes a Warnable from the health package. It should only be used
// for testing purposes.
func unregister(w *Warnable) {
@@ -377,7 +385,7 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
}
prevWs := t.warnableVal[w]
mak.Set(&t.warnableVal, w, ws)
if !ws.Equal(prevWs) {
if !ws.Equal(prevWs) && !slices.Contains(t.hideWarnables, w.Code) {
for _, cb := range t.watchers {
// If the Warnable has been unhealthy for more than its TimeToVisible, the callback should be
// executed immediately. Otherwise, the callback should be enqueued to run once the Warnable
@@ -883,6 +891,24 @@ func (t *Tracker) SetAutoUpdatePrefs(check bool, apply opt.Bool) {
t.selfCheckLocked()
}
// HideWarnables prevents the provided list of WarnableCodes from notifying
// watchers or returning in any Tracker output if they become unhealthy. The
// provided codes replace the previous codes.
func (t *Tracker) HideWarnables(warnables []WarnableCode) {
if t.nil() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
t.hideWarnables = warnables
for _, w := range t.warnables {
if slices.Contains(t.hideWarnables, w.Code) {
t.setHealthyLocked(w)
}
}
}
func (t *Tracker) timerSelfCheck() {
if t.nil() {
return
@@ -936,7 +962,7 @@ func (t *Tracker) Strings() []string {
func (t *Tracker) stringsLocked() []string {
result := []string{}
for w, ws := range t.warnableVal {
if !w.IsVisible(ws) {
if !w.IsVisible(ws) || slices.Contains(t.hideWarnables, w.Code) {
// Do not append invisible warnings.
continue
}

View File

@@ -4,6 +4,7 @@
package health
import (
"slices"
"time"
)
@@ -86,7 +87,7 @@ func (t *Tracker) CurrentState() *State {
wm := map[WarnableCode]UnhealthyState{}
for w, ws := range t.warnableVal {
if !w.IsVisible(ws) {
if !w.IsVisible(ws) || slices.Contains(t.hideWarnables, w.Code) {
// Skip invisible Warnables.
continue
}

View File

@@ -10,6 +10,7 @@ import (
"net/netip"
"tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
@@ -38,6 +39,7 @@ func (src *Prefs) Clone() *Prefs {
}
}
}
dst.HideHealthWarnings = append(src.HideHealthWarnings[:0:0], src.HideHealthWarnings...)
dst.Persist = src.Persist.Clone()
return dst
}
@@ -73,6 +75,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
PostureChecking bool
NetfilterKind string
DriveShares []*drive.Share
HideHealthWarnings []health.WarnableCode
AllowSingleHosts marshalAsTrueInJSON
Persist *persist.Persist
}{})

View File

@@ -11,6 +11,7 @@ import (
"net/netip"
"tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/types/persist"
@@ -100,6 +101,9 @@ func (v PrefsView) NetfilterKind() string { return v.ж.Netfilte
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
}
func (v PrefsView) HideHealthWarnings() views.Slice[health.WarnableCode] {
return views.SliceOf(v.ж.HideHealthWarnings)
}
func (v PrefsView) AllowSingleHosts() marshalAsTrueInJSON { return v.ж.AllowSingleHosts }
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
@@ -134,6 +138,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
PostureChecking bool
NetfilterKind string
DriveShares []*drive.Share
HideHealthWarnings []health.WarnableCode
AllowSingleHosts marshalAsTrueInJSON
Persist *persist.Persist
}{})

View File

@@ -557,6 +557,9 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
fs.SetShares(shares)
}
}
if hideWarns := pm.prefs.HideHealthWarnings().AsSlice(); len(hideWarns) != 0 {
b.health.HideWarnables(hideWarns)
}
return b, nil
}
@@ -1716,6 +1719,21 @@ func applySysPolicy(prefs *ipn.Prefs, lastSuggestedExitNode tailcfg.StableNodeID
}
}
}
// Gross, but the type mismatch causes these silly conversion copies.
hideHealthWarnings := make([]string, 0, len(prefs.HideHealthWarnings))
for _, w := range prefs.HideHealthWarnings {
hideHealthWarnings = append(hideHealthWarnings, string(w))
}
if po, err := syspolicy.GetStringArray(syspolicy.HideHealthWarnings, hideHealthWarnings); err == nil {
if !slices.Equal(po, hideHealthWarnings) {
newVal := make([]health.WarnableCode, 0, len(po))
for _, w := range po {
newVal = append(newVal, health.WarnableCode(w))
}
prefs.HideHealthWarnings = newVal
anyChange = true
}
}
return anyChange
}
@@ -3999,6 +4017,10 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
b.authReconfig()
}
if !slices.Equal(oldp.HideHealthWarnings().AsSlice(), newp.HideHealthWarnings) {
b.health.HideWarnables(newp.HideHealthWarnings)
}
b.send(ipn.Notify{Prefs: &prefs})
return prefs
}

View File

@@ -19,6 +19,7 @@ import (
"tailscale.com/atomicfile"
"tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/netaddr"
"tailscale.com/net/tsaddr"
@@ -245,6 +246,9 @@ type Prefs struct {
// by name.
DriveShares []*drive.Share
// HideHealthWarnings is a list of warnable codes to hide from the user.
HideHealthWarnings []health.WarnableCode
// AllowSingleHosts was a legacy field that was always true
// for the past 4.5 years. It controlled whether Tailscale
// peers got /32 or /127 routes for each other.
@@ -336,6 +340,7 @@ type MaskedPrefs struct {
PostureCheckingSet bool `json:",omitempty"`
NetfilterKindSet bool `json:",omitempty"`
DriveSharesSet bool `json:",omitempty"`
HideHealthWarningsSet bool `json:",omitempty"`
}
// SetsInternal reports whether mp has any of the Internal*Set field bools set
@@ -615,7 +620,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.AppConnector == p2.AppConnector &&
p.PostureChecking == p2.PostureChecking &&
slices.EqualFunc(p.DriveShares, p2.DriveShares, drive.SharesEqual) &&
p.NetfilterKind == p2.NetfilterKind
p.NetfilterKind == p2.NetfilterKind &&
slices.Equal(p.HideHealthWarnings, p2.HideHealthWarnings)
}
func (au AutoUpdatePrefs) Pretty() string {

View File

@@ -65,6 +65,7 @@ func TestPrefsEqual(t *testing.T) {
"PostureChecking",
"NetfilterKind",
"DriveShares",
"HideHealthWarnings",
"AllowSingleHosts",
"Persist",
}

View File

@@ -126,6 +126,9 @@ const (
// Keys with a string array value.
// AllowedSuggestedExitNodes's string array value is a list of exit node IDs that restricts which exit nodes are considered when generating suggestions for exit nodes.
AllowedSuggestedExitNodes Key = "AllowedSuggestedExitNodes"
// HideHealthWarnings hides the specified health.WarnableCode types from
// the user when they become unhealthy.
HideHealthWarnings Key = "HideHealthWarnings"
)
// implicitDefinitions is a list of [setting.Definition] that will be registered
@@ -170,6 +173,7 @@ var implicitDefinitions = []*setting.Definition{
setting.NewDefinition(TestMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(UpdateMenuVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(OnboardingFlowVisibility, setting.UserSetting, setting.VisibilityValue),
setting.NewDefinition(HideHealthWarnings, setting.UserSetting, setting.StringListValue),
}
func init() {