health: add Tracker type, in prep for removing global variables

This moves most of the health package global variables to a new
`health.Tracker` type.

But then rather than plumbing the Tracker in tsd.System everywhere,
this only goes halfway and makes one new global Tracker
(`health.Global`) that all the existing callers now use.

A future change will eliminate that global.

Updates #11874
Updates #4136

Change-Id: I6ee27e0b2e35f68cb38fecdb3b2dc4c3f2e09d68
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2024-04-25 13:24:49 -07:00 committed by Brad Fitzpatrick
parent d5fc52a0f5
commit ebc552d2e0
17 changed files with 268 additions and 256 deletions

View File

@ -195,7 +195,7 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background()) c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf) c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange) c.unregisterHealthWatch = health.Global.RegisterWatcher(direct.ReportHealthChange)
return c, nil return c, nil
} }
@ -316,7 +316,7 @@ func (c *Auto) authRoutine() {
} }
if goal == nil { if goal == nil {
health.SetAuthRoutineInError(nil) health.Global.SetAuthRoutineInError(nil)
// Wait for user to Login or Logout. // Wait for user to Login or Logout.
<-ctx.Done() <-ctx.Done()
c.logf("[v1] authRoutine: context done.") c.logf("[v1] authRoutine: context done.")
@ -343,7 +343,7 @@ func (c *Auto) authRoutine() {
f = "TryLogin" f = "TryLogin"
} }
if err != nil { if err != nil {
health.SetAuthRoutineInError(err) health.Global.SetAuthRoutineInError(err)
report(err, f) report(err, f)
bo.BackOff(ctx, err) bo.BackOff(ctx, err)
continue continue
@ -373,7 +373,7 @@ func (c *Auto) authRoutine() {
} }
// success // success
health.SetAuthRoutineInError(nil) health.Global.SetAuthRoutineInError(nil)
c.mu.Lock() c.mu.Lock()
c.urlToVisit = "" c.urlToVisit = ""
c.loggedIn = true c.loggedIn = true
@ -503,11 +503,11 @@ func (c *Auto) mapRoutine() {
c.logf("[v1] mapRoutine: context done.") c.logf("[v1] mapRoutine: context done.")
continue continue
} }
health.SetOutOfPollNetMap() health.Global.SetOutOfPollNetMap()
err := c.direct.PollNetMap(ctx, mrs) err := c.direct.PollNetMap(ctx, mrs)
health.SetOutOfPollNetMap() health.Global.SetOutOfPollNetMap()
c.mu.Lock() c.mu.Lock()
c.inMapPoll = false c.inMapPoll = false
if c.state == StateSynchronized { if c.state == StateSynchronized {

View File

@ -894,10 +894,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) { ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off") extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
} }
if health.RouterHealth() != nil { if health.Global.RouterHealth() != nil {
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy") extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
} }
extraDebugFlags = health.AppendWarnableDebugFlags(extraDebugFlags) extraDebugFlags = health.Global.AppendWarnableDebugFlags(extraDebugFlags)
if hostinfo.DisabledEtcAptSource() { if hostinfo.DisabledEtcAptSource() {
extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled") extraDebugFlags = append(extraDebugFlags, "warn-etc-apt-source-disabled")
} }
@ -970,7 +970,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
} }
defer res.Body.Close() defer res.Body.Close()
health.NoteMapRequestHeard(request) health.Global.NoteMapRequestHeard(request)
watchdogTimer.Reset(watchdogTimeout) watchdogTimer.Reset(watchdogTimeout)
if nu == nil { if nu == nil {
@ -1041,7 +1041,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
metricMapResponseMessages.Add(1) metricMapResponseMessages.Add(1)
if isStreaming { if isStreaming {
health.GotStreamedMapResponse() health.Global.GotStreamedMapResponse()
} }
if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) { if pr := resp.PingRequest; pr != nil && c.isUniquePingRequest(pr) {

View File

@ -17,40 +17,51 @@
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/opt"
"tailscale.com/util/mak"
"tailscale.com/util/multierr" "tailscale.com/util/multierr"
"tailscale.com/util/set" "tailscale.com/util/set"
) )
var ( var (
mu sync.Mutex
debugHandler map[string]http.Handler
)
// Global is a global health tracker for the process.
//
// TODO(bradfitz): move this to tsd.System so a process can have multiple
// tsnet/etc instances with their own health trackers.
var Global = new(Tracker)
type Tracker struct {
// mu guards everything in this var block. // mu guards everything in this var block.
mu sync.Mutex mu sync.Mutex
sysErr = map[Subsystem]error{} // error key => err (or nil for no error) sysErr map[Subsystem]error // subsystem => err (or nil for no error)
watchers = set.HandleSet[func(Subsystem, error)]{} // opt func to run if error state changes watchers set.HandleSet[func(Subsystem, error)] // opt func to run if error state changes
warnables = set.Set[*Warnable]{} warnables set.Set[*Warnable]
timer *time.Timer timer *time.Timer
debugHandler = map[string]http.Handler{}
inMapPoll bool inMapPoll bool
inMapPollSince time.Time inMapPollSince time.Time
lastMapPollEndedAt time.Time lastMapPollEndedAt time.Time
lastStreamedMapResponse time.Time lastStreamedMapResponse time.Time
derpHomeRegion int derpHomeRegion int
derpHomeless bool derpHomeless bool
derpRegionConnected = map[int]bool{} derpRegionConnected map[int]bool
derpRegionHealthProblem = map[int]string{} derpRegionHealthProblem map[int]string
derpRegionLastFrame = map[int]time.Time{} derpRegionLastFrame map[int]time.Time
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
ipnState string ipnState string
ipnWantRunning bool ipnWantRunning bool
anyInterfaceUp = true // until told otherwise anyInterfaceUp opt.Bool // empty means unknown (assume true)
udp4Unbound bool udp4Unbound bool
controlHealth []string controlHealth []string
lastLoginErr error lastLoginErr error
localLogConfigErr error localLogConfigErr error
tlsConnectionErrors = map[string]error{} // map[ServerName]error tlsConnectionErrors map[string]error // map[ServerName]error
) }
// Subsystem is the name of a subsystem whose health can be monitored. // Subsystem is the name of a subsystem whose health can be monitored.
type Subsystem string type Subsystem string
@ -78,14 +89,17 @@
// NewWarnable returns a new warnable item that the caller can mark // NewWarnable returns a new warnable item that the caller can mark
// as health or in warning state. // as health or in warning state.
func NewWarnable(opts ...WarnableOpt) *Warnable { func (t *Tracker) NewWarnable(opts ...WarnableOpt) *Warnable {
w := new(Warnable) w := new(Warnable)
for _, o := range opts { for _, o := range opts {
o.mod(w) o.mod(w)
} }
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
warnables.Add(w) if t.warnables == nil {
t.warnables = set.Set[*Warnable]{}
}
t.warnables.Add(w)
return w return w
} }
@ -151,12 +165,12 @@ func (w *Warnable) get() error {
// AppendWarnableDebugFlags appends to base any health items that are currently in failed // AppendWarnableDebugFlags appends to base any health items that are currently in failed
// state and were created with MapDebugFlag. // state and were created with MapDebugFlag.
func AppendWarnableDebugFlags(base []string) []string { func (t *Tracker) AppendWarnableDebugFlags(base []string) []string {
ret := base ret := base
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
for w := range warnables { for w := range t.warnables {
if w.debugFlag == "" { if w.debugFlag == "" {
continue continue
} }
@ -172,75 +186,78 @@ func AppendWarnableDebugFlags(base []string) []string {
// error changes state either to unhealthy or from unhealthy. It is // error changes state either to unhealthy or from unhealthy. It is
// not called on transition from unknown to healthy. It must be non-nil // not called on transition from unknown to healthy. It must be non-nil
// and is run in its own goroutine. The returned func unregisters it. // and is run in its own goroutine. The returned func unregisters it.
func RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) { func (t *Tracker) RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
handle := watchers.Add(cb) if t.watchers == nil {
if timer == nil { t.watchers = set.HandleSet[func(Subsystem, error)]{}
timer = time.AfterFunc(time.Minute, timerSelfCheck) }
handle := t.watchers.Add(cb)
if t.timer == nil {
t.timer = time.AfterFunc(time.Minute, t.timerSelfCheck)
} }
return func() { return func() {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
delete(watchers, handle) delete(t.watchers, handle)
if len(watchers) == 0 && timer != nil { if len(t.watchers) == 0 && t.timer != nil {
timer.Stop() t.timer.Stop()
timer = nil t.timer = nil
} }
} }
} }
// SetRouterHealth sets the state of the wgengine/router.Router. // SetRouterHealth sets the state of the wgengine/router.Router.
func SetRouterHealth(err error) { setErr(SysRouter, err) } func (t *Tracker) SetRouterHealth(err error) { t.setErr(SysRouter, err) }
// RouterHealth returns the wgengine/router.Router error state. // RouterHealth returns the wgengine/router.Router error state.
func RouterHealth() error { return get(SysRouter) } func (t *Tracker) RouterHealth() error { return t.get(SysRouter) }
// SetDNSHealth sets the state of the net/dns.Manager // SetDNSHealth sets the state of the net/dns.Manager
func SetDNSHealth(err error) { setErr(SysDNS, err) } func (t *Tracker) SetDNSHealth(err error) { t.setErr(SysDNS, err) }
// DNSHealth returns the net/dns.Manager error state. // DNSHealth returns the net/dns.Manager error state.
func DNSHealth() error { return get(SysDNS) } func (t *Tracker) DNSHealth() error { return t.get(SysDNS) }
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator // SetDNSOSHealth sets the state of the net/dns.OSConfigurator
func SetDNSOSHealth(err error) { setErr(SysDNSOS, err) } func (t *Tracker) SetDNSOSHealth(err error) { t.setErr(SysDNSOS, err) }
// SetDNSManagerHealth sets the state of the Linux net/dns manager's // SetDNSManagerHealth sets the state of the Linux net/dns manager's
// discovery of the /etc/resolv.conf situation. // discovery of the /etc/resolv.conf situation.
func SetDNSManagerHealth(err error) { setErr(SysDNSManager, err) } func (t *Tracker) SetDNSManagerHealth(err error) { t.setErr(SysDNSManager, err) }
// DNSOSHealth returns the net/dns.OSConfigurator error state. // DNSOSHealth returns the net/dns.OSConfigurator error state.
func DNSOSHealth() error { return get(SysDNSOS) } func (t *Tracker) DNSOSHealth() error { return t.get(SysDNSOS) }
// SetTKAHealth sets the health of the tailnet key authority. // SetTKAHealth sets the health of the tailnet key authority.
func SetTKAHealth(err error) { setErr(SysTKA, err) } func (t *Tracker) SetTKAHealth(err error) { t.setErr(SysTKA, err) }
// TKAHealth returns the tailnet key authority error state. // TKAHealth returns the tailnet key authority error state.
func TKAHealth() error { return get(SysTKA) } func (t *Tracker) TKAHealth() error { return t.get(SysTKA) }
// SetLocalLogConfigHealth sets the error state of this client's local log configuration. // SetLocalLogConfigHealth sets the error state of this client's local log configuration.
func SetLocalLogConfigHealth(err error) { func (t *Tracker) SetLocalLogConfigHealth(err error) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
localLogConfigErr = err t.localLogConfigErr = err
} }
// SetTLSConnectionError sets the error state for connections to a specific // SetTLSConnectionError sets the error state for connections to a specific
// host. Setting the error to nil will clear any previously-set error. // host. Setting the error to nil will clear any previously-set error.
func SetTLSConnectionError(host string, err error) { func (t *Tracker) SetTLSConnectionError(host string, err error) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
if err == nil { if err == nil {
delete(tlsConnectionErrors, host) delete(t.tlsConnectionErrors, host)
} else { } else {
tlsConnectionErrors[host] = err mak.Set(&t.tlsConnectionErrors, host, err)
} }
} }
func RegisterDebugHandler(typ string, h http.Handler) { func RegisterDebugHandler(typ string, h http.Handler) {
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
debugHandler[typ] = h mak.Set(&debugHandler, typ, h)
} }
func DebugHandler(typ string) http.Handler { func DebugHandler(typ string) http.Handler {
@ -249,24 +266,27 @@ func DebugHandler(typ string) http.Handler {
return debugHandler[typ] return debugHandler[typ]
} }
func get(key Subsystem) error { func (t *Tracker) get(key Subsystem) error {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
return sysErr[key] return t.sysErr[key]
} }
func setErr(key Subsystem, err error) { func (t *Tracker) setErr(key Subsystem, err error) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
setLocked(key, err) t.setLocked(key, err)
} }
func setLocked(key Subsystem, err error) { func (t *Tracker) setLocked(key Subsystem, err error) {
old, ok := sysErr[key] if t.sysErr == nil {
t.sysErr = map[Subsystem]error{}
}
old, ok := t.sysErr[key]
if !ok && err == nil { if !ok && err == nil {
// Initial happy path. // Initial happy path.
sysErr[key] = nil t.sysErr[key] = nil
selfCheckLocked() t.selfCheckLocked()
return return
} }
if ok && (old == nil) == (err == nil) { if ok && (old == nil) == (err == nil) {
@ -274,22 +294,22 @@ func setLocked(key Subsystem, err error) {
// don't run callbacks, but exact error might've // don't run callbacks, but exact error might've
// changed, so note it. // changed, so note it.
if err != nil { if err != nil {
sysErr[key] = err t.sysErr[key] = err
} }
return return
} }
sysErr[key] = err t.sysErr[key] = err
selfCheckLocked() t.selfCheckLocked()
for _, cb := range watchers { for _, cb := range t.watchers {
go cb(key, err) go cb(key, err)
} }
} }
func SetControlHealth(problems []string) { func (t *Tracker) SetControlHealth(problems []string) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
controlHealth = problems t.controlHealth = problems
selfCheckLocked() t.selfCheckLocked()
} }
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse // GotStreamedMapResponse notes that we got a tailcfg.MapResponse
@ -297,161 +317,161 @@ func SetControlHealth(problems []string) {
// //
// This also notes that a map poll is in progress. To unset that, call // This also notes that a map poll is in progress. To unset that, call
// SetOutOfPollNetMap(). // SetOutOfPollNetMap().
func GotStreamedMapResponse() { func (t *Tracker) GotStreamedMapResponse() {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
lastStreamedMapResponse = time.Now() t.lastStreamedMapResponse = time.Now()
if !inMapPoll { if !t.inMapPoll {
inMapPoll = true t.inMapPoll = true
inMapPollSince = time.Now() t.inMapPollSince = time.Now()
} }
selfCheckLocked() t.selfCheckLocked()
} }
// SetOutOfPollNetMap records that the client is no longer in // SetOutOfPollNetMap records that the client is no longer in
// an HTTP map request long poll to the control plane. // an HTTP map request long poll to the control plane.
func SetOutOfPollNetMap() { func (t *Tracker) SetOutOfPollNetMap() {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
if !inMapPoll { if !t.inMapPoll {
return return
} }
inMapPoll = false t.inMapPoll = false
lastMapPollEndedAt = time.Now() t.lastMapPollEndedAt = time.Now()
selfCheckLocked() t.selfCheckLocked()
} }
// GetInPollNetMap reports whether the client has an open // GetInPollNetMap reports whether the client has an open
// HTTP long poll open to the control plane. // HTTP long poll open to the control plane.
func GetInPollNetMap() bool { func (t *Tracker) GetInPollNetMap() bool {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
return inMapPoll return t.inMapPoll
} }
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is. // SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
// //
// The homeless parameter is whether magicsock is running in DERP-disconnected // The homeless parameter is whether magicsock is running in DERP-disconnected
// mode, without discovering and maintaining a connection to its home DERP. // mode, without discovering and maintaining a connection to its home DERP.
func SetMagicSockDERPHome(region int, homeless bool) { func (t *Tracker) SetMagicSockDERPHome(region int, homeless bool) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
derpHomeRegion = region t.derpHomeRegion = region
derpHomeless = homeless t.derpHomeless = homeless
selfCheckLocked() t.selfCheckLocked()
} }
// NoteMapRequestHeard notes whenever we successfully sent a map request // NoteMapRequestHeard notes whenever we successfully sent a map request
// to control for which we received a 200 response. // to control for which we received a 200 response.
func NoteMapRequestHeard(mr *tailcfg.MapRequest) { func (t *Tracker) NoteMapRequestHeard(mr *tailcfg.MapRequest) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
// TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare // TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare
// against SetMagicSockDERPHome and // against SetMagicSockDERPHome and
// SetDERPRegionConnectedState // SetDERPRegionConnectedState
lastMapRequestHeard = time.Now() t.lastMapRequestHeard = time.Now()
selfCheckLocked() t.selfCheckLocked()
} }
func SetDERPRegionConnectedState(region int, connected bool) { func (t *Tracker) SetDERPRegionConnectedState(region int, connected bool) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
derpRegionConnected[region] = connected mak.Set(&t.derpRegionConnected, region, connected)
selfCheckLocked() t.selfCheckLocked()
} }
// SetDERPRegionHealth sets or clears any problem associated with the // SetDERPRegionHealth sets or clears any problem associated with the
// provided DERP region. // provided DERP region.
func SetDERPRegionHealth(region int, problem string) { func (t *Tracker) SetDERPRegionHealth(region int, problem string) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
if problem == "" { if problem == "" {
delete(derpRegionHealthProblem, region) delete(t.derpRegionHealthProblem, region)
} else { } else {
derpRegionHealthProblem[region] = problem mak.Set(&t.derpRegionHealthProblem, region, problem)
} }
selfCheckLocked() t.selfCheckLocked()
} }
// NoteDERPRegionReceivedFrame is called to note that a frame was received from // NoteDERPRegionReceivedFrame is called to note that a frame was received from
// the given DERP region at the current time. // the given DERP region at the current time.
func NoteDERPRegionReceivedFrame(region int) { func (t *Tracker) NoteDERPRegionReceivedFrame(region int) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
derpRegionLastFrame[region] = time.Now() mak.Set(&t.derpRegionLastFrame, region, time.Now())
selfCheckLocked() t.selfCheckLocked()
} }
// GetDERPRegionReceivedTime returns the last time that a frame was received // GetDERPRegionReceivedTime returns the last time that a frame was received
// from the given DERP region, or the zero time if no communication with that // from the given DERP region, or the zero time if no communication with that
// region has occurred. // region has occurred.
func GetDERPRegionReceivedTime(region int) time.Time { func (t *Tracker) GetDERPRegionReceivedTime(region int) time.Time {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
return derpRegionLastFrame[region] return t.derpRegionLastFrame[region]
} }
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc. // state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
func SetIPNState(state string, wantRunning bool) { func (t *Tracker) SetIPNState(state string, wantRunning bool) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
ipnState = state t.ipnState = state
ipnWantRunning = wantRunning t.ipnWantRunning = wantRunning
selfCheckLocked() t.selfCheckLocked()
} }
// SetAnyInterfaceUp sets whether any network interface is up. // SetAnyInterfaceUp sets whether any network interface is up.
func SetAnyInterfaceUp(up bool) { func (t *Tracker) SetAnyInterfaceUp(up bool) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
anyInterfaceUp = up t.anyInterfaceUp.Set(up)
selfCheckLocked() t.selfCheckLocked()
} }
// SetUDP4Unbound sets whether the udp4 bind failed completely. // SetUDP4Unbound sets whether the udp4 bind failed completely.
func SetUDP4Unbound(unbound bool) { func (t *Tracker) SetUDP4Unbound(unbound bool) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
udp4Unbound = unbound t.udp4Unbound = unbound
selfCheckLocked() t.selfCheckLocked()
} }
// SetAuthRoutineInError records the latest error encountered as a result of a // SetAuthRoutineInError records the latest error encountered as a result of a
// login attempt. Providing a nil error indicates successful login, or that // login attempt. Providing a nil error indicates successful login, or that
// being logged in w/coordination is not currently desired. // being logged in w/coordination is not currently desired.
func SetAuthRoutineInError(err error) { func (t *Tracker) SetAuthRoutineInError(err error) {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
lastLoginErr = err t.lastLoginErr = err
} }
func timerSelfCheck() { func (t *Tracker) timerSelfCheck() {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
checkReceiveFuncs() checkReceiveFuncs()
selfCheckLocked() t.selfCheckLocked()
if timer != nil { if t.timer != nil {
timer.Reset(time.Minute) t.timer.Reset(time.Minute)
} }
} }
func selfCheckLocked() { func (t *Tracker) selfCheckLocked() {
if ipnState == "" { if t.ipnState == "" {
// Don't check yet. // Don't check yet.
return return
} }
setLocked(SysOverall, overallErrorLocked()) t.setLocked(SysOverall, t.overallErrorLocked())
} }
// OverallError returns a summary of the health state. // OverallError returns a summary of the health state.
// //
// If there are multiple problems, the error will be of type // If there are multiple problems, the error will be of type
// multierr.Error. // multierr.Error.
func OverallError() error { func (t *Tracker) OverallError() error {
mu.Lock() t.mu.Lock()
defer mu.Unlock() defer t.mu.Unlock()
return overallErrorLocked() return t.overallErrorLocked()
} }
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR") var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
@ -459,11 +479,11 @@ func OverallError() error {
// networkErrorf creates an error that indicates issues with outgoing network // networkErrorf creates an error that indicates issues with outgoing network
// connectivity. Any active warnings related to network connectivity will // connectivity. Any active warnings related to network connectivity will
// automatically be appended to it. // automatically be appended to it.
func networkErrorf(format string, a ...any) error { func (t *Tracker) networkErrorf(format string, a ...any) error {
errs := []error{ errs := []error{
fmt.Errorf(format, a...), fmt.Errorf(format, a...),
} }
for w := range warnables { for w := range t.warnables {
if !w.hasConnectivityImpact { if !w.hasConnectivityImpact {
continue continue
} }
@ -477,53 +497,53 @@ func networkErrorf(format string, a ...any) error {
return multierr.New(errs...) return multierr.New(errs...)
} }
var errNetworkDown = networkErrorf("network down") var errNetworkDown = errors.New("network down")
var errNotInMapPoll = networkErrorf("not in map poll") var errNotInMapPoll = errors.New("not in map poll")
var errNoDERPHome = errors.New("no DERP home") var errNoDERPHome = errors.New("no DERP home")
var errNoUDP4Bind = networkErrorf("no udp4 bind") var errNoUDP4Bind = errors.New("no udp4 bind")
func overallErrorLocked() error { func (t *Tracker) overallErrorLocked() error {
if !anyInterfaceUp { if v, ok := t.anyInterfaceUp.Get(); ok && !v {
return errNetworkDown return errNetworkDown
} }
if localLogConfigErr != nil { if t.localLogConfigErr != nil {
return localLogConfigErr return t.localLogConfigErr
} }
if !ipnWantRunning { if !t.ipnWantRunning {
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning) return fmt.Errorf("state=%v, wantRunning=%v", t.ipnState, t.ipnWantRunning)
} }
if lastLoginErr != nil { if t.lastLoginErr != nil {
return fmt.Errorf("not logged in, last login error=%v", lastLoginErr) return fmt.Errorf("not logged in, last login error=%v", t.lastLoginErr)
} }
now := time.Now() now := time.Now()
if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) { if !t.inMapPoll && (t.lastMapPollEndedAt.IsZero() || now.Sub(t.lastMapPollEndedAt) > 10*time.Second) {
return errNotInMapPoll return errNotInMapPoll
} }
const tooIdle = 2*time.Minute + 5*time.Second const tooIdle = 2*time.Minute + 5*time.Second
if d := now.Sub(lastStreamedMapResponse).Round(time.Second); d > tooIdle { if d := now.Sub(t.lastStreamedMapResponse).Round(time.Second); d > tooIdle {
return networkErrorf("no map response in %v", d) return t.networkErrorf("no map response in %v", d)
} }
if !derpHomeless { if !t.derpHomeless {
rid := derpHomeRegion rid := t.derpHomeRegion
if rid == 0 { if rid == 0 {
return errNoDERPHome return errNoDERPHome
} }
if !derpRegionConnected[rid] { if !t.derpRegionConnected[rid] {
return networkErrorf("not connected to home DERP region %v", rid) return t.networkErrorf("not connected to home DERP region %v", rid)
} }
if d := now.Sub(derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle { if d := now.Sub(t.derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle {
return networkErrorf("haven't heard from home DERP region %v in %v", rid, d) return t.networkErrorf("haven't heard from home DERP region %v in %v", rid, d)
} }
} }
if udp4Unbound { if t.udp4Unbound {
return errNoUDP4Bind return errNoUDP4Bind
} }
// TODO: use // TODO: use
_ = inMapPollSince _ = t.inMapPollSince
_ = lastMapPollEndedAt _ = t.lastMapPollEndedAt
_ = lastStreamedMapResponse _ = t.lastStreamedMapResponse
_ = lastMapRequestHeard _ = t.lastMapRequestHeard
var errs []error var errs []error
for _, recv := range receiveFuncs { for _, recv := range receiveFuncs {
@ -531,27 +551,27 @@ func overallErrorLocked() error {
errs = append(errs, fmt.Errorf("%s is not running", recv.name)) errs = append(errs, fmt.Errorf("%s is not running", recv.name))
} }
} }
for sys, err := range sysErr { for sys, err := range t.sysErr {
if err == nil || sys == SysOverall { if err == nil || sys == SysOverall {
continue continue
} }
errs = append(errs, fmt.Errorf("%v: %w", sys, err)) errs = append(errs, fmt.Errorf("%v: %w", sys, err))
} }
for w := range warnables { for w := range t.warnables {
if err := w.get(); err != nil { if err := w.get(); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} }
for regionID, problem := range derpRegionHealthProblem { for regionID, problem := range t.derpRegionHealthProblem {
errs = append(errs, fmt.Errorf("derp%d: %v", regionID, problem)) errs = append(errs, fmt.Errorf("derp%d: %v", regionID, problem))
} }
for _, s := range controlHealth { for _, s := range t.controlHealth {
errs = append(errs, errors.New(s)) errs = append(errs, errors.New(s))
} }
if err := envknob.ApplyDiskConfigError(); err != nil { if err := envknob.ApplyDiskConfigError(); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
for serverName, err := range tlsConnectionErrors { for serverName, err := range t.tlsConnectionErrors {
errs = append(errs, fmt.Errorf("TLS connection error for %q: %w", serverName, err)) errs = append(errs, fmt.Errorf("TLS connection error for %q: %w", serverName, err))
} }
if e := fakeErrForTesting(); len(errs) == 0 && e != "" { if e := fakeErrForTesting(); len(errs) == 0 && e != "" {

View File

@ -8,15 +8,13 @@
"fmt" "fmt"
"reflect" "reflect"
"testing" "testing"
"tailscale.com/util/set"
) )
func TestAppendWarnableDebugFlags(t *testing.T) { func TestAppendWarnableDebugFlags(t *testing.T) {
resetWarnables() var tr Tracker
for i := range 10 { for i := range 10 {
w := NewWarnable(WithMapDebugFlag(fmt.Sprint(i))) w := tr.NewWarnable(WithMapDebugFlag(fmt.Sprint(i)))
if i%2 == 0 { if i%2 == 0 {
w.Set(errors.New("boom")) w.Set(errors.New("boom"))
} }
@ -27,15 +25,9 @@ func TestAppendWarnableDebugFlags(t *testing.T) {
var got []string var got []string
for range 20 { for range 20 {
got = append(got[:0], "z", "y") got = append(got[:0], "z", "y")
got = AppendWarnableDebugFlags(got) got = tr.AppendWarnableDebugFlags(got)
if !reflect.DeepEqual(got, want) { if !reflect.DeepEqual(got, want) {
t.Fatalf("AppendWarnableDebugFlags = %q; want %q", got, want) t.Fatalf("AppendWarnableDebugFlags = %q; want %q", got, want)
} }
} }
} }
func resetWarnables() {
mu.Lock()
defer mu.Unlock()
warnables = set.Set[*Warnable]{}
}

View File

@ -426,7 +426,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
b.linkChange(&netmon.ChangeDelta{New: netMon.InterfaceState()}) b.linkChange(&netmon.ChangeDelta{New: netMon.InterfaceState()})
b.unregisterNetMon = netMon.RegisterChangeCallback(b.linkChange) b.unregisterNetMon = netMon.RegisterChangeCallback(b.linkChange)
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange) b.unregisterHealthWatch = health.Global.RegisterWatcher(b.onHealthChange)
if tunWrap, ok := b.sys.Tun.GetOK(); ok { if tunWrap, ok := b.sys.Tun.GetOK(); ok {
tunWrap.PeerAPIPort = b.GetPeerAPIPort tunWrap.PeerAPIPort = b.GetPeerAPIPort
@ -761,7 +761,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
} }
} }
} }
if err := health.OverallError(); err != nil { if err := health.Global.OverallError(); err != nil {
switch e := err.(type) { switch e := err.(type) {
case multierr.Error: case multierr.Error:
for _, err := range e.Errors() { for _, err := range e.Errors() {
@ -820,7 +820,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) { sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
ss.OS = version.OS() ss.OS = version.OS()
ss.Online = health.GetInPollNetMap() ss.Online = health.Global.GetInPollNetMap()
if b.netMap != nil { if b.netMap != nil {
ss.InNetworkMap = true ss.InNetworkMap = true
if hi := b.netMap.SelfNode.Hostinfo(); hi.Valid() { if hi := b.netMap.SelfNode.Hostinfo(); hi.Valid() {
@ -1221,7 +1221,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
if st.NetMap != nil { if st.NetMap != nil {
if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) {
msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line." msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line."
health.SetLocalLogConfigHealth(errors.New(msg)) health.Global.SetLocalLogConfigHealth(errors.New(msg))
// Connecting to this tailnet without logging is forbidden; boot us outta here. // Connecting to this tailnet without logging is forbidden; boot us outta here.
b.mu.Lock() b.mu.Lock()
prefs.WantRunning = false prefs.WantRunning = false
@ -1818,7 +1818,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return nil return nil
} }
var warnInvalidUnsignedNodes = health.NewWarnable() var warnInvalidUnsignedNodes = health.Global.NewWarnable()
// updateFilterLocked updates the packet filter in wgengine based on the // updateFilterLocked updates the packet filter in wgengine based on the
// given netMap and user preferences. // given netMap and user preferences.
@ -3044,7 +3044,7 @@ func (b *LocalBackend) isDefaultServerLocked() bool {
return prefs.ControlURLOrDefault() == ipn.DefaultControlURL return prefs.ControlURLOrDefault() == ipn.DefaultControlURL
} }
var warnExitNodeUsage = health.NewWarnable(health.WithConnectivityImpact()) var warnExitNodeUsage = health.Global.NewWarnable(health.WithConnectivityImpact())
// updateExitNodeUsageWarning updates a warnable meant to notify users of // updateExitNodeUsageWarning updates a warnable meant to notify users of
// configuration issues that could break exit node usage. // configuration issues that could break exit node usage.
@ -4254,7 +4254,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
// prefs may change irrespective of state; WantRunning should be explicitly // prefs may change irrespective of state; WantRunning should be explicitly
// set before potential early return even if the state is unchanged. // set before potential early return even if the state is unchanged.
health.SetIPNState(newState.String(), prefs.Valid() && prefs.WantRunning()) health.Global.SetIPNState(newState.String(), prefs.Valid() && prefs.WantRunning())
if oldState == newState { if oldState == newState {
return return
} }
@ -4692,9 +4692,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.pauseOrResumeControlClientLocked() b.pauseOrResumeControlClientLocked()
if nm != nil { if nm != nil {
health.SetControlHealth(nm.ControlHealth) health.Global.SetControlHealth(nm.ControlHealth)
} else { } else {
health.SetControlHealth(nil) health.Global.SetControlHealth(nil)
} }
// Determine if file sharing is enabled // Determine if file sharing is enabled
@ -5675,7 +5675,7 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
return b.sshServer, nil return b.sshServer, nil
} }
var warnSSHSELinux = health.NewWarnable() var warnSSHSELinux = health.Global.NewWarnable()
func (b *LocalBackend) updateSELinuxHealthWarning() { func (b *LocalBackend) updateSELinuxHealthWarning() {
if hostinfo.IsSELinuxEnforcing() { if hostinfo.IsSELinuxEnforcing() {
@ -5908,7 +5908,7 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err
b.lastServeConfJSON = mem.B(nil) b.lastServeConfJSON = mem.B(nil)
b.serveConfig = ipn.ServeConfigView{} b.serveConfig = ipn.ServeConfigView{}
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
health.SetLocalLogConfigHealth(nil) health.Global.SetLocalLogConfigHealth(nil)
return b.Start(ipn.Options{}) return b.Start(ipn.Options{})
} }

View File

@ -59,11 +59,11 @@ type tkaState struct {
// b.mu must be held. // b.mu must be held.
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) { func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
if b.tka == nil && !b.capTailnetLock { if b.tka == nil && !b.capTailnetLock {
health.SetTKAHealth(nil) health.Global.SetTKAHealth(nil)
return return
} }
if b.tka == nil { if b.tka == nil {
health.SetTKAHealth(nil) health.Global.SetTKAHealth(nil)
return // TKA not enabled. return // TKA not enabled.
} }
@ -117,9 +117,9 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
// Check that we ourselves are not locked out, report a health issue if so. // Check that we ourselves are not locked out, report a health issue if so.
if nm.SelfNode.Valid() && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key(), nm.SelfNode.KeySignature().AsSlice()) != nil { if nm.SelfNode.Valid() && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key(), nm.SelfNode.KeySignature().AsSlice()) != nil {
health.SetTKAHealth(errors.New(healthmsg.LockedOut)) health.Global.SetTKAHealth(errors.New(healthmsg.LockedOut))
} else { } else {
health.SetTKAHealth(nil) health.Global.SetTKAHealth(nil)
} }
} }
@ -188,7 +188,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
b.logf("Disablement failed, leaving TKA enabled. Error: %v", err) b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
} else { } else {
isEnabled = false isEnabled = false
health.SetTKAHealth(nil) health.Global.SetTKAHealth(nil)
} }
} else { } else {
return fmt.Errorf("[bug] unreachable invariant of wantEnabled w/ isEnabled") return fmt.Errorf("[bug] unreachable invariant of wantEnabled w/ isEnabled")

View File

@ -358,7 +358,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
} }
hi, _ := json.Marshal(hostinfo.New()) hi, _ := json.Marshal(hostinfo.New())
h.logf("user bugreport hostinfo: %s", hi) h.logf("user bugreport hostinfo: %s", hi)
if err := health.OverallError(); err != nil { if err := health.Global.OverallError(); err != nil {
h.logf("user bugreport health: %s", err.Error()) h.logf("user bugreport health: %s", err.Error())
} else { } else {
h.logf("user bugreport health: ok") h.logf("user bugreport health: ok")

View File

@ -58,7 +58,7 @@ func (m *directManager) runFileWatcher() {
} }
} }
var warnTrample = health.NewWarnable() var warnTrample = health.Global.NewWarnable()
// checkForFileTrample checks whether /etc/resolv.conf has been trampled // checkForFileTrample checks whether /etc/resolv.conf has been trampled
// by another program on the system. (e.g. a DHCP client) // by another program on the system. (e.g. a DHCP client)

View File

@ -94,10 +94,10 @@ func (m *Manager) Set(cfg Config) error {
return err return err
} }
if err := m.os.SetDNS(ocfg); err != nil { if err := m.os.SetDNS(ocfg); err != nil {
health.SetDNSOSHealth(err) health.Global.SetDNSOSHealth(err)
return err return err
} }
health.SetDNSOSHealth(nil) health.Global.SetDNSOSHealth(nil)
return nil return nil
} }
@ -248,7 +248,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// This is currently (2022-10-13) expected on certain iOS and macOS // This is currently (2022-10-13) expected on certain iOS and macOS
// builds. // builds.
} else { } else {
health.SetDNSOSHealth(err) health.Global.SetDNSOSHealth(err)
return resolver.Config{}, OSConfig{}, err return resolver.Config{}, OSConfig{}, err
} }
} }

View File

@ -271,7 +271,7 @@ func dnsMode(logf logger.Logf, env newOSConfigEnv) (ret string, err error) {
return "direct", nil return "direct", nil
} }
health.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm")) health.Global.SetDNSManagerHealth(errors.New("systemd-resolved and NetworkManager are wired together incorrectly; MagicDNS will probably not work. For more info, see https://tailscale.com/s/resolved-nm"))
dbg("nm-safe", "no") dbg("nm-safe", "no")
return "systemd-resolved", nil return "systemd-resolved", nil
default: default:

View File

@ -163,7 +163,7 @@ func (m *resolvedManager) run(ctx context.Context) {
// Reset backoff and SetNSOSHealth after successful on reconnect. // Reset backoff and SetNSOSHealth after successful on reconnect.
bo.BackOff(ctx, nil) bo.BackOff(ctx, nil)
health.SetDNSOSHealth(nil) health.Global.SetDNSOSHealth(nil)
return nil return nil
} }
@ -241,7 +241,7 @@ func (m *resolvedManager) run(ctx context.Context) {
// Set health while holding the lock, because this will // Set health while holding the lock, because this will
// graciously serialize the resync's health outcome with a // graciously serialize the resync's health outcome with a
// concurrent SetDNS call. // concurrent SetDNS call.
health.SetDNSOSHealth(err) health.Global.SetDNSOSHealth(err)
if err != nil { if err != nil {
m.logf("failed to configure systemd-resolved: %v", err) m.logf("failed to configure systemd-resolved: %v", err)
} }

View File

@ -80,10 +80,10 @@ func Config(host string, base *tls.Config) *tls.Config {
// any verification. // any verification.
if certIsSelfSigned(cs.PeerCertificates[0]) { if certIsSelfSigned(cs.PeerCertificates[0]) {
// Self-signed certs are never valid. // Self-signed certs are never valid.
health.SetTLSConnectionError(cs.ServerName, fmt.Errorf("certificate is self-signed")) health.Global.SetTLSConnectionError(cs.ServerName, fmt.Errorf("certificate is self-signed"))
} else { } else {
// Ensure we clear any error state for this ServerName. // Ensure we clear any error state for this ServerName.
health.SetTLSConnectionError(cs.ServerName, nil) health.Global.SetTLSConnectionError(cs.ServerName, nil)
} }
// First try doing x509 verification with the system's // First try doing x509 verification with the system's

View File

@ -165,7 +165,7 @@ func (c *Conn) maybeSetNearestDERP(report *netcheck.Report) (preferredDERP int)
if testenv.InTest() && !checkControlHealthDuringNearestDERPInTests { if testenv.InTest() && !checkControlHealthDuringNearestDERPInTests {
connectedToControl = true connectedToControl = true
} else { } else {
connectedToControl = health.GetInPollNetMap() connectedToControl = health.Global.GetInPollNetMap()
} }
if !connectedToControl { if !connectedToControl {
c.mu.Lock() c.mu.Lock()
@ -201,12 +201,12 @@ func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) {
defer c.mu.Unlock() defer c.mu.Unlock()
if !c.wantDerpLocked() { if !c.wantDerpLocked() {
c.myDerp = 0 c.myDerp = 0
health.SetMagicSockDERPHome(0, c.homeless) health.Global.SetMagicSockDERPHome(0, c.homeless)
return false return false
} }
if c.homeless { if c.homeless {
c.myDerp = 0 c.myDerp = 0
health.SetMagicSockDERPHome(0, c.homeless) health.Global.SetMagicSockDERPHome(0, c.homeless)
return false return false
} }
if derpNum == c.myDerp { if derpNum == c.myDerp {
@ -217,7 +217,7 @@ func (c *Conn) setNearestDERP(derpNum int) (wantDERP bool) {
metricDERPHomeChange.Add(1) metricDERPHomeChange.Add(1)
} }
c.myDerp = derpNum c.myDerp = derpNum
health.SetMagicSockDERPHome(derpNum, c.homeless) health.Global.SetMagicSockDERPHome(derpNum, c.homeless)
if c.privateKey.IsZero() { if c.privateKey.IsZero() {
// No private key yet, so DERP connections won't come up anyway. // No private key yet, so DERP connections won't come up anyway.
@ -525,8 +525,8 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netip.AddrPort, d
return n return n
} }
defer health.SetDERPRegionConnectedState(regionID, false) defer health.Global.SetDERPRegionConnectedState(regionID, false)
defer health.SetDERPRegionHealth(regionID, "") defer health.Global.SetDERPRegionHealth(regionID, "")
// peerPresent is the set of senders we know are present on this // peerPresent is the set of senders we know are present on this
// connection, based on messages we've received from the server. // connection, based on messages we've received from the server.
@ -538,7 +538,7 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netip.AddrPort, d
for { for {
msg, connGen, err := dc.RecvDetail() msg, connGen, err := dc.RecvDetail()
if err != nil { if err != nil {
health.SetDERPRegionConnectedState(regionID, false) health.Global.SetDERPRegionConnectedState(regionID, false)
// Forget that all these peers have routes. // Forget that all these peers have routes.
for peer := range peerPresent { for peer := range peerPresent {
delete(peerPresent, peer) delete(peerPresent, peer)
@ -576,14 +576,14 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netip.AddrPort, d
now := time.Now() now := time.Now()
if lastPacketTime.IsZero() || now.Sub(lastPacketTime) > frameReceiveRecordRate { if lastPacketTime.IsZero() || now.Sub(lastPacketTime) > frameReceiveRecordRate {
health.NoteDERPRegionReceivedFrame(regionID) health.Global.NoteDERPRegionReceivedFrame(regionID)
lastPacketTime = now lastPacketTime = now
} }
switch m := msg.(type) { switch m := msg.(type) {
case derp.ServerInfoMessage: case derp.ServerInfoMessage:
health.SetDERPRegionConnectedState(regionID, true) health.Global.SetDERPRegionConnectedState(regionID, true)
health.SetDERPRegionHealth(regionID, "") // until declared otherwise health.Global.SetDERPRegionHealth(regionID, "") // until declared otherwise
c.logf("magicsock: derp-%d connected; connGen=%v", regionID, connGen) c.logf("magicsock: derp-%d connected; connGen=%v", regionID, connGen)
continue continue
case derp.ReceivedPacket: case derp.ReceivedPacket:
@ -623,7 +623,7 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netip.AddrPort, d
}() }()
continue continue
case derp.HealthMessage: case derp.HealthMessage:
health.SetDERPRegionHealth(regionID, m.Problem) health.Global.SetDERPRegionHealth(regionID, m.Problem)
continue continue
case derp.PeerGoneMessage: case derp.PeerGoneMessage:
switch m.Reason { switch m.Reason {

View File

@ -666,7 +666,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
// NOTE(andrew-d): I don't love that we're depending on the // NOTE(andrew-d): I don't love that we're depending on the
// health package here, but I'd rather do that and not store // health package here, but I'd rather do that and not store
// the exact same state in two different places. // the exact same state in two different places.
GetLastDERPActivity: health.GetDERPRegionReceivedTime, GetLastDERPActivity: health.Global.GetDERPRegionReceivedTime,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -2471,7 +2471,7 @@ func (c *Conn) bindSocket(ruc *RebindingUDPConn, network string, curPortFate cur
} }
ruc.setConnLocked(pconn, network, c.bind.BatchSize()) ruc.setConnLocked(pconn, network, c.bind.BatchSize())
if network == "udp4" { if network == "udp4" {
health.SetUDP4Unbound(false) health.Global.SetUDP4Unbound(false)
} }
return nil return nil
} }
@ -2482,7 +2482,7 @@ func (c *Conn) bindSocket(ruc *RebindingUDPConn, network string, curPortFate cur
// we get a link change and we can try binding again. // we get a link change and we can try binding again.
ruc.setConnLocked(newBlockForeverConn(), "", c.bind.BatchSize()) ruc.setConnLocked(newBlockForeverConn(), "", c.bind.BatchSize())
if network == "udp4" { if network == "udp4" {
health.SetUDP4Unbound(true) health.Global.SetUDP4Unbound(true)
} }
return fmt.Errorf("failed to bind any ports (tried %v)", ports) return fmt.Errorf("failed to bind any ports (tried %v)", ports)
} }

View File

@ -3120,14 +3120,14 @@ func TestMaybeSetNearestDERP(t *testing.T) {
report := &netcheck.Report{PreferredDERP: tt.reportDERP} report := &netcheck.Report{PreferredDERP: tt.reportDERP}
oldConnected := health.GetInPollNetMap() oldConnected := health.Global.GetInPollNetMap()
if tt.connectedToControl != oldConnected { if tt.connectedToControl != oldConnected {
if tt.connectedToControl { if tt.connectedToControl {
health.GotStreamedMapResponse() health.Global.GotStreamedMapResponse()
t.Cleanup(health.SetOutOfPollNetMap) t.Cleanup(health.Global.SetOutOfPollNetMap)
} else { } else {
health.SetOutOfPollNetMap() health.Global.SetOutOfPollNetMap()
t.Cleanup(health.GotStreamedMapResponse) t.Cleanup(health.Global.GotStreamedMapResponse)
} }
} }

View File

@ -235,7 +235,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid) return nil, fmt.Errorf("interfaceFromLUID: interface with LUID %v not found", luid)
} }
var networkCategoryWarning = health.NewWarnable(health.WithMapDebugFlag("warn-network-category-unhealthy")) var networkCategoryWarning = health.Global.NewWarnable(health.WithMapDebugFlag("warn-network-category-unhealthy"))
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) { func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
var mtu = tstun.DefaultTUNMTU() var mtu = tstun.DefaultTUNMTU()

View File

@ -970,7 +970,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
e.logf("wgengine: Reconfig: configuring router") e.logf("wgengine: Reconfig: configuring router")
e.networkLogger.ReconfigRoutes(routerCfg) e.networkLogger.ReconfigRoutes(routerCfg)
err := e.router.Set(routerCfg) err := e.router.Set(routerCfg)
health.SetRouterHealth(err) health.Global.SetRouterHealth(err)
if err != nil { if err != nil {
return err return err
} }
@ -979,7 +979,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
// assigned address. // assigned address.
e.logf("wgengine: Reconfig: configuring DNS") e.logf("wgengine: Reconfig: configuring DNS")
err = e.dns.Set(*dnsCfg) err = e.dns.Set(*dnsCfg)
health.SetDNSHealth(err) health.Global.SetDNSHealth(err)
if err != nil { if err != nil {
return err return err
} }
@ -1183,7 +1183,7 @@ func (e *userspaceEngine) linkChange(delta *netmon.ChangeDelta) {
e.logf("[v1] LinkChange: minor") e.logf("[v1] LinkChange: minor")
} }
health.SetAnyInterfaceUp(up) health.Global.SetAnyInterfaceUp(up)
e.magicConn.SetNetworkUp(up) e.magicConn.SetNetworkUp(up)
if !up || changed { if !up || changed {
if err := e.dns.FlushCaches(); err != nil { if err := e.dns.FlushCaches(); err != nil {