2021-02-18 16:58:13 +00:00
|
|
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
// Package health is a registry for other packages to report & check
|
|
|
|
// overall health status of the node.
|
|
|
|
package health
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sync"
|
2021-02-25 05:29:51 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"tailscale.com/tailcfg"
|
2021-02-18 16:58:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2021-02-25 05:29:51 +00:00
|
|
|
// mu guards everything in this var block.
|
|
|
|
mu sync.Mutex
|
|
|
|
|
2021-02-18 16:58:13 +00:00
|
|
|
m = map[string]error{} // error key => err (or nil for no error)
|
|
|
|
watchers = map[*watchHandle]func(string, error){} // opt func to run if error state changes
|
2021-02-25 05:29:51 +00:00
|
|
|
|
|
|
|
inMapPoll bool
|
|
|
|
inMapPollSince time.Time
|
|
|
|
lastMapPollEndedAt time.Time
|
|
|
|
lastStreamedMapResponse time.Time
|
|
|
|
derpHomeRegion int
|
|
|
|
derpRegionConnected = map[int]bool{}
|
|
|
|
derpRegionLastFrame = map[int]time.Time{}
|
|
|
|
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
|
|
|
|
ipnState string
|
|
|
|
ipnWantRunning bool
|
2021-02-18 16:58:13 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type watchHandle byte
|
|
|
|
|
|
|
|
// RegisterWatcher adds a function that will be called if an
|
|
|
|
// error changes state either to unhealthy or from unhealthy. It is
|
|
|
|
// 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.
|
|
|
|
func RegisterWatcher(cb func(errKey string, err error)) (unregister func()) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
handle := new(watchHandle)
|
|
|
|
watchers[handle] = cb
|
|
|
|
return func() {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
delete(watchers, handle)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetRouter sets the state of the wgengine/router.Router.
|
|
|
|
func SetRouterHealth(err error) { set("router", err) }
|
|
|
|
|
|
|
|
// RouterHealth returns the wgengine/router.Router error state.
|
|
|
|
func RouterHealth() error { return get("router") }
|
|
|
|
|
2021-03-15 22:39:37 +00:00
|
|
|
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
|
|
|
|
// This only applies on Windows.
|
|
|
|
func SetNetworkCategoryHealth(err error) { set("network-category", err) }
|
|
|
|
|
|
|
|
func NetworkCategoryHealth() error { return get("network-category") }
|
|
|
|
|
2021-02-18 16:58:13 +00:00
|
|
|
func get(key string) error {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
return m[key]
|
|
|
|
}
|
|
|
|
|
|
|
|
func set(key string, err error) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
old, ok := m[key]
|
|
|
|
if !ok && err == nil {
|
|
|
|
// Initial happy path.
|
|
|
|
m[key] = nil
|
2021-02-25 05:29:51 +00:00
|
|
|
selfCheckLocked()
|
2021-02-18 16:58:13 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if ok && (old == nil) == (err == nil) {
|
|
|
|
// No change in overall error status (nil-vs-not), so
|
|
|
|
// don't run callbacks, but exact error might've
|
|
|
|
// changed, so note it.
|
|
|
|
if err != nil {
|
|
|
|
m[key] = err
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
m[key] = err
|
2021-02-25 05:29:51 +00:00
|
|
|
selfCheckLocked()
|
2021-02-18 16:58:13 +00:00
|
|
|
for _, cb := range watchers {
|
|
|
|
go cb(key, err)
|
|
|
|
}
|
|
|
|
}
|
2021-02-25 05:29:51 +00:00
|
|
|
|
|
|
|
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse
|
|
|
|
// message in streaming mode, even if it's just a keep-alive message.
|
|
|
|
func GotStreamedMapResponse() {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
lastStreamedMapResponse = time.Now()
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetInPollNetMap records that we're in
|
|
|
|
func SetInPollNetMap(v bool) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
if v == inMapPoll {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
inMapPoll = v
|
|
|
|
if v {
|
|
|
|
inMapPollSince = time.Now()
|
|
|
|
} else {
|
|
|
|
lastMapPollEndedAt = time.Now()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
|
|
|
|
func SetMagicSockDERPHome(region int) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
derpHomeRegion = region
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
// NoteMapRequestHeard notes whenever we successfully sent a map request
|
|
|
|
// to control for which we received a 200 response.
|
|
|
|
func NoteMapRequestHeard(mr *tailcfg.MapRequest) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
// TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare
|
|
|
|
// against SetMagicSockDERPHome and
|
|
|
|
// SetDERPRegionConnectedState
|
|
|
|
|
|
|
|
lastMapRequestHeard = time.Now()
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
func SetDERPRegionConnectedState(region int, connected bool) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
derpRegionConnected[region] = connected
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
func NoteDERPRegionReceivedFrame(region int) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
derpRegionLastFrame[region] = time.Now()
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
|
|
|
|
func SetIPNState(state string, wantRunning bool) {
|
|
|
|
mu.Lock()
|
|
|
|
defer mu.Unlock()
|
|
|
|
ipnState = state
|
|
|
|
ipnWantRunning = wantRunning
|
|
|
|
selfCheckLocked()
|
|
|
|
}
|
|
|
|
|
|
|
|
func selfCheckLocked() {
|
|
|
|
// TODO: check states against each other.
|
|
|
|
// For staticcheck for now:
|
|
|
|
_ = inMapPollSince
|
|
|
|
_ = lastMapPollEndedAt
|
|
|
|
_ = lastStreamedMapResponse
|
|
|
|
_ = derpHomeRegion
|
|
|
|
_ = lastMapRequestHeard
|
|
|
|
_ = ipnState
|
|
|
|
_ = ipnWantRunning
|
|
|
|
}
|