tsd, ipnlocal, etc: add tsd.System.HealthTracker, start some plumbing

This adds a health.Tracker to tsd.System, accessible via
a new tsd.System.HealthTracker method.

In the future, that new method will return a tsd.System-specific
HealthTracker, so multiple tsnet.Servers in the same process are
isolated. For now, though, it just always returns the temporary
health.Global value. That permits incremental plumbing over a number
of changes. When the second to last health.Global reference is gone,
then the tsd.System.HealthTracker implementation can return a private
Tracker.

The primary plumbing this does is adding it to LocalBackend and its
dozen and change health calls. A few misc other callers are also
plumbed. Subsequent changes will flesh out other parts of the tree
(magicsock, controlclient, etc).

Updates #11874
Updates #4136

Change-Id: Id51e73cfc8a39110425b6dc19d18b3975eac75ce
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/11881/head
Brad Fitzpatrick 7 months ago committed by Brad Fitzpatrick
parent cb66952a0d
commit 723c775dbb

@ -89,7 +89,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/disco from tailscale.com/derp tailscale.com/disco from tailscale.com/derp
tailscale.com/drive from tailscale.com/client/tailscale+ tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/health from tailscale.com/net/tlsdial tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/hostinfo from tailscale.com/net/interfaces+ tailscale.com/hostinfo from tailscale.com/net/interfaces+
tailscale.com/ipn from tailscale.com/client/tailscale tailscale.com/ipn from tailscale.com/client/tailscale
tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+ tailscale.com/ipn/ipnstate from tailscale.com/client/tailscale+

@ -88,7 +88,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/disco from tailscale.com/derp tailscale.com/disco from tailscale.com/derp
tailscale.com/drive from tailscale.com/client/tailscale+ tailscale.com/drive from tailscale.com/client/tailscale+
tailscale.com/envknob from tailscale.com/client/tailscale+ tailscale.com/envknob from tailscale.com/client/tailscale+
tailscale.com/health from tailscale.com/net/tlsdial tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
tailscale.com/hostinfo from tailscale.com/client/web+ tailscale.com/hostinfo from tailscale.com/client/web+
tailscale.com/ipn from tailscale.com/client/tailscale+ tailscale.com/ipn from tailscale.com/client/tailscale+

@ -21,6 +21,7 @@ import (
"time" "time"
"tailscale.com/derp/derphttp" "tailscale.com/derp/derphttp"
"tailscale.com/health"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
@ -157,6 +158,7 @@ func getURL(ctx context.Context, urlStr string) error {
} }
func checkDerp(ctx context.Context, derpRegion string) (err error) { func checkDerp(ctx context.Context, derpRegion string) (err error) {
ht := new(health.Tracker)
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil) req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
if err != nil { if err != nil {
return fmt.Errorf("create derp map request: %w", err) return fmt.Errorf("create derp map request: %w", err)
@ -195,6 +197,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion) c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion) c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
c1.HealthTracker = ht
c2.HealthTracker = ht
defer func() { defer func() {
if err != nil { if err != nil {
c1.Close() c1.Close()

@ -248,7 +248,7 @@ func NewDirect(opts Options) (*Direct, error) {
tr := http.DefaultTransport.(*http.Transport).Clone() tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr) tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig) tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), health.Global, tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache) tr.DialContext = dnscache.Dialer(opts.Dialer.SystemDial, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig) tr.DialTLSContext = dnscache.TLSDialer(opts.Dialer.SystemDial, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true tr.ForceAttemptHTTP2 = true

@ -38,6 +38,7 @@ import (
"tailscale.com/control/controlbase" "tailscale.com/control/controlbase"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback" "tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
@ -433,7 +434,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
// Disable HTTP2, since h2 can't do protocol switching. // Disable HTTP2, since h2 can't do protocol switching.
tr.TLSClientConfig.NextProtos = []string{} tr.TLSClientConfig.NextProtos = []string{}
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
tr.TLSClientConfig = tlsdial.Config(a.Hostname, tr.TLSClientConfig) tr.TLSClientConfig = tlsdial.Config(a.Hostname, health.Global, tr.TLSClientConfig)
if !tr.TLSClientConfig.InsecureSkipVerify { if !tr.TLSClientConfig.InsecureSkipVerify {
panic("unexpected") // should be set by tlsdial.Config panic("unexpected") // should be set by tlsdial.Config
} }

@ -31,6 +31,7 @@ import (
"go4.org/mem" "go4.org/mem"
"tailscale.com/derp" "tailscale.com/derp"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dnscache" "tailscale.com/net/dnscache"
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
"tailscale.com/net/netns" "tailscale.com/net/netns"
@ -51,10 +52,11 @@ import (
// Send/Recv will completely re-establish the connection (unless Close // Send/Recv will completely re-establish the connection (unless Close
// has been called). // has been called).
type Client struct { type Client struct {
TLSConfig *tls.Config // optional; nil means default TLSConfig *tls.Config // optional; nil means default
DNSCache *dnscache.Resolver // optional; nil means no caching HealthTracker *health.Tracker // optional; used if non-nil only
MeshKey string // optional; for trusted clients DNSCache *dnscache.Resolver // optional; nil means no caching
IsProber bool // optional; for probers to optional declare themselves as such MeshKey string // optional; for trusted clients
IsProber bool // optional; for probers to optional declare themselves as such
// WatchConnectionChanges is whether the client wishes to subscribe to // WatchConnectionChanges is whether the client wishes to subscribe to
// notifications about clients connecting & disconnecting. // notifications about clients connecting & disconnecting.
@ -115,6 +117,7 @@ func (c *Client) String() string {
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily. // NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
// To trigger a connection, use Connect. // To trigger a connection, use Connect.
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups. // The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
// The healthTracker parameter is also optional.
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client { func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
c := &Client{ c := &Client{
@ -612,7 +615,7 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
} }
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn { func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig) tlsConf := tlsdial.Config(c.tlsServerName(node), c.HealthTracker, c.TLSConfig)
if node != nil { if node != nil {
if node.InsecureForTests { if node.InsecureForTests {
tlsConf.InsecureSkipVerify = true tlsConf.InsecureSkipVerify = true

@ -30,8 +30,11 @@ var (
// Global is a global health tracker for the process. // Global is a global health tracker for the process.
// //
// TODO(bradfitz): move this to tsd.System so a process can have multiple // TODO(bradfitz): finish moving all reference to this plumb it (ultimately out
// tsnet/etc instances with their own health trackers. // from tsd.System) so a process can have multiple tsnet/etc instances with
// their own health trackers. But for now (2024-04-25), the tsd.System value
// given out is just this one, until that's the only remaining Global reference
// remaining.
var Global = new(Tracker) var Global = new(Tracker)
type Tracker struct { type Tracker struct {

@ -170,6 +170,7 @@ type LocalBackend struct {
keyLogf logger.Logf // for printing list of peers on change keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change statsLogf logger.Logf // for printing peers stats on change
sys *tsd.System sys *tsd.System
health *health.Tracker // always non-nil
e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys e wgengine.Engine // non-nil; TODO(bradfitz): remove; use sys
store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys store ipn.StateStore // non-nil; TODO(bradfitz): remove; use sys
dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys dialer *tsdial.Dialer // non-nil; TODO(bradfitz): remove; use sys
@ -386,6 +387,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), keyLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now),
sys: sys, sys: sys,
health: sys.HealthTracker(),
conf: sys.InitialConfig, conf: sys.InitialConfig,
e: e, e: e,
dialer: dialer, dialer: dialer,
@ -426,7 +428,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.Global.RegisterWatcher(b.onHealthChange) b.unregisterHealthWatch = b.health.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
@ -625,7 +627,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
// If the local network configuration has changed, our filter may // If the local network configuration has changed, our filter may
// need updating to tweak default routes. // need updating to tweak default routes.
b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs()) b.updateFilterLocked(b.netMap, b.pm.CurrentPrefs())
updateExitNodeUsageWarning(b.pm.CurrentPrefs(), delta.New) updateExitNodeUsageWarning(b.pm.CurrentPrefs(), delta.New, b.health)
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running { if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := b.netMap.GetAddresses().Len() want := b.netMap.GetAddresses().Len()
@ -761,7 +763,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
} }
} }
} }
if err := health.Global.OverallError(); err != nil { if err := b.health.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 +822,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.Global.GetInPollNetMap() ss.Online = b.health.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 +1223,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.Global.SetLocalLogConfigHealth(errors.New(msg)) b.health.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
@ -1851,10 +1853,10 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
if packetFilterPermitsUnlockedNodes(b.peers, packetFilter) { if packetFilterPermitsUnlockedNodes(b.peers, packetFilter) {
err := errors.New("server sent invalid packet filter permitting traffic to unlocked nodes; rejecting all packets for safety") err := errors.New("server sent invalid packet filter permitting traffic to unlocked nodes; rejecting all packets for safety")
health.Global.SetWarnable(warnInvalidUnsignedNodes, err) b.health.SetWarnable(warnInvalidUnsignedNodes, err)
packetFilter = nil packetFilter = nil
} else { } else {
health.Global.SetWarnable(warnInvalidUnsignedNodes, nil) b.health.SetWarnable(warnInvalidUnsignedNodes, nil)
} }
} }
if prefs.Valid() { if prefs.Valid() {
@ -3048,7 +3050,7 @@ var warnExitNodeUsage = health.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.
func updateExitNodeUsageWarning(p ipn.PrefsView, state *interfaces.State) { func updateExitNodeUsageWarning(p ipn.PrefsView, state *interfaces.State, health *health.Tracker) {
var result error var result error
if p.ExitNodeIP().IsValid() || p.ExitNodeID() != "" { if p.ExitNodeIP().IsValid() || p.ExitNodeID() != "" {
warn, _ := netutil.CheckReversePathFiltering(state) warn, _ := netutil.CheckReversePathFiltering(state)
@ -3057,7 +3059,7 @@ func updateExitNodeUsageWarning(p ipn.PrefsView, state *interfaces.State) {
result = fmt.Errorf("%s: %v, %s", healthmsg.WarnExitNodeUsage, warn, comment) result = fmt.Errorf("%s: %v, %s", healthmsg.WarnExitNodeUsage, warn, comment)
} }
} }
health.Global.SetWarnable(warnExitNodeUsage, result) health.SetWarnable(warnExitNodeUsage, result)
} }
func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error { func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error {
@ -4254,7 +4256,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.Global.SetIPNState(newState.String(), prefs.Valid() && prefs.WantRunning()) b.health.SetIPNState(newState.String(), prefs.Valid() && prefs.WantRunning())
if oldState == newState { if oldState == newState {
return return
} }
@ -4692,9 +4694,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.pauseOrResumeControlClientLocked() b.pauseOrResumeControlClientLocked()
if nm != nil { if nm != nil {
health.Global.SetControlHealth(nm.ControlHealth) b.health.SetControlHealth(nm.ControlHealth)
} else { } else {
health.Global.SetControlHealth(nil) b.health.SetControlHealth(nil)
} }
// Determine if file sharing is enabled // Determine if file sharing is enabled
@ -5679,9 +5681,9 @@ var warnSSHSELinux = health.NewWarnable()
func (b *LocalBackend) updateSELinuxHealthWarning() { func (b *LocalBackend) updateSELinuxHealthWarning() {
if hostinfo.IsSELinuxEnforcing() { if hostinfo.IsSELinuxEnforcing() {
health.Global.SetWarnable(warnSSHSELinux, errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux")) b.health.SetWarnable(warnSSHSELinux, errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux"))
} else { } else {
health.Global.SetWarnable(warnSSHSELinux, nil) b.health.SetWarnable(warnSSHSELinux, nil)
} }
} }
@ -5908,7 +5910,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.Global.SetLocalLogConfigHealth(nil) b.health.SetLocalLogConfigHealth(nil)
return b.Start(ipn.Options{}) return b.Start(ipn.Options{})
} }

@ -30,6 +30,7 @@ import (
"golang.org/x/term" "golang.org/x/term"
"tailscale.com/atomicfile" "tailscale.com/atomicfile"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/log/filelogger" "tailscale.com/log/filelogger"
"tailscale.com/logtail" "tailscale.com/logtail"
"tailscale.com/logtail/filch" "tailscale.com/logtail/filch"
@ -782,7 +783,7 @@ func NewLogtailTransport(host string, netMon *netmon.Monitor, logf logger.Logf)
tr.TLSNextProto = map[string]func(authority string, c *tls.Conn) http.RoundTripper{} tr.TLSNextProto = map[string]func(authority string, c *tls.Conn) http.RoundTripper{}
} }
tr.TLSClientConfig = tlsdial.Config(host, tr.TLSClientConfig) tr.TLSClientConfig = tlsdial.Config(host, health.Global, tr.TLSClientConfig)
return tr return tr
} }

@ -28,6 +28,7 @@ import (
"tailscale.com/atomicfile" "tailscale.com/atomicfile"
"tailscale.com/envknob" "tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dns/recursive" "tailscale.com/net/dns/recursive"
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
"tailscale.com/net/netns" "tailscale.com/net/netns"
@ -64,9 +65,10 @@ func MakeLookupFunc(logf logger.Logf, netMon *netmon.Monitor) func(ctx context.C
// fallbackResolver contains the state and configuration for a DNS resolution // fallbackResolver contains the state and configuration for a DNS resolution
// function. // function.
type fallbackResolver struct { type fallbackResolver struct {
logf logger.Logf logf logger.Logf
netMon *netmon.Monitor // or nil netMon *netmon.Monitor // or nil
sf singleflight.Group[string, resolveResult] healthTracker *health.Tracker // or nil
sf singleflight.Group[string, resolveResult]
// for tests // for tests
waitForCompare bool waitForCompare bool
@ -79,7 +81,7 @@ func (fr *fallbackResolver) Lookup(ctx context.Context, host string) ([]netip.Ad
// recursive resolver. (tailscale/corp#15261) In the future, we might // recursive resolver. (tailscale/corp#15261) In the future, we might
// change the default (the opt.Bool being unset) to mean enabled. // change the default (the opt.Bool being unset) to mean enabled.
if disableRecursiveResolver() || !optRecursiveResolver().EqualBool(true) { if disableRecursiveResolver() || !optRecursiveResolver().EqualBool(true) {
return lookup(ctx, host, fr.logf, fr.netMon) return lookup(ctx, host, fr.logf, fr.healthTracker, fr.netMon)
} }
addrsCh := make(chan []netip.Addr, 1) addrsCh := make(chan []netip.Addr, 1)
@ -99,7 +101,7 @@ func (fr *fallbackResolver) Lookup(ctx context.Context, host string) ([]netip.Ad
go fr.compareWithRecursive(ctx, addrsCh, host) go fr.compareWithRecursive(ctx, addrsCh, host)
} }
addrs, err := lookup(ctx, host, fr.logf, fr.netMon) addrs, err := lookup(ctx, host, fr.logf, fr.healthTracker, fr.netMon)
if err != nil { if err != nil {
addrsCh <- nil addrsCh <- nil
return nil, err return nil, err
@ -207,7 +209,7 @@ func (fr *fallbackResolver) compareWithRecursive(
} }
} }
func lookup(ctx context.Context, host string, logf logger.Logf, netMon *netmon.Monitor) ([]netip.Addr, error) { func lookup(ctx context.Context, host string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) ([]netip.Addr, error) {
if ip, err := netip.ParseAddr(host); err == nil && ip.IsValid() { if ip, err := netip.ParseAddr(host); err == nil && ip.IsValid() {
return []netip.Addr{ip}, nil return []netip.Addr{ip}, nil
} }
@ -255,7 +257,7 @@ func lookup(ctx context.Context, host string, logf logger.Logf, netMon *netmon.M
logf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host) logf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host)
ctx, cancel := context.WithTimeout(ctx, 3*time.Second) ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel() defer cancel()
dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host, logf, netMon) dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host, logf, ht, netMon)
if err != nil { if err != nil {
logf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err) logf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err)
continue continue
@ -274,14 +276,16 @@ func lookup(ctx context.Context, host string, logf logger.Logf, netMon *netmon.M
// serverName and serverIP of are, say, "derpN.tailscale.com". // serverName and serverIP of are, say, "derpN.tailscale.com".
// queryName is the name being sought (e.g. "controlplane.tailscale.com"), passed as hint. // queryName is the name being sought (e.g. "controlplane.tailscale.com"), passed as hint.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, netMon *netmon.Monitor) (dnsMap, error) { //
// ht may be nil.
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netip.Addr, queryName string, logf logger.Logf, ht *health.Tracker, netMon *netmon.Monitor) (dnsMap, error) {
dialer := netns.NewDialer(logf, netMon) dialer := netns.NewDialer(logf, netMon)
tr := http.DefaultTransport.(*http.Transport).Clone() tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment tr.Proxy = tshttpproxy.ProxyFromEnvironment
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) { tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443")) return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))
} }
tr.TLSClientConfig = tlsdial.Config(serverName, tr.TLSClientConfig) tr.TLSClientConfig = tlsdial.Config(serverName, ht, tr.TLSClientConfig)
c := &http.Client{Transport: tr} c := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil) req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil)
if err != nil { if err != nil {

@ -46,7 +46,8 @@ var tlsdialWarningPrinted sync.Map // map[string]bool
// Config returns a tls.Config for connecting to a server. // Config returns a tls.Config for connecting to a server.
// If base is non-nil, it's cloned as the base config before // If base is non-nil, it's cloned as the base config before
// being configured and returned. // being configured and returned.
func Config(host string, base *tls.Config) *tls.Config { // If ht is non-nil, it's used to report health errors.
func Config(host string, ht *health.Tracker, base *tls.Config) *tls.Config {
var conf *tls.Config var conf *tls.Config
if base == nil { if base == nil {
conf = new(tls.Config) conf = new(tls.Config)
@ -78,12 +79,14 @@ func Config(host string, base *tls.Config) *tls.Config {
conf.VerifyConnection = func(cs tls.ConnectionState) error { conf.VerifyConnection = func(cs tls.ConnectionState) error {
// Perform some health checks on this certificate before we do // Perform some health checks on this certificate before we do
// any verification. // any verification.
if certIsSelfSigned(cs.PeerCertificates[0]) { if ht != nil {
// Self-signed certs are never valid. if certIsSelfSigned(cs.PeerCertificates[0]) {
health.Global.SetTLSConnectionError(cs.ServerName, fmt.Errorf("certificate is self-signed")) // Self-signed certs are never valid.
} else { ht.SetTLSConnectionError(cs.ServerName, fmt.Errorf("certificate is self-signed"))
// Ensure we clear any error state for this ServerName. } else {
health.Global.SetTLSConnectionError(cs.ServerName, nil) // Ensure we clear any error state for this ServerName.
ht.SetTLSConnectionError(cs.ServerName, nil)
}
} }
// First try doing x509 verification with the system's // First try doing x509 verification with the system's
@ -204,7 +207,7 @@ func NewTransport() *http.Transport {
return nil, err return nil, err
} }
var d tls.Dialer var d tls.Dialer
d.Config = Config(host, nil) d.Config = Config(host, nil, nil)
return d.DialContext(ctx, network, addr) return d.DialContext(ctx, network, addr)
}, },
} }

@ -15,6 +15,8 @@ import (
"runtime" "runtime"
"sync/atomic" "sync/atomic"
"testing" "testing"
"tailscale.com/health"
) )
func resetOnce() { func resetOnce() {
@ -105,7 +107,8 @@ func TestFallbackRootWorks(t *testing.T) {
}, },
DisableKeepAlives: true, // for test cleanup ease DisableKeepAlives: true, // for test cleanup ease
} }
tr.TLSClientConfig = Config("tlsdial.test", tr.TLSClientConfig) ht := new(health.Tracker)
tr.TLSClientConfig = Config("tlsdial.test", ht, tr.TLSClientConfig)
c := &http.Client{Transport: tr} c := &http.Client{Transport: tr}
ctr0 := atomic.LoadInt32(&counterFallbackOK) ctr0 := atomic.LoadInt32(&counterFallbackOK)

@ -23,6 +23,7 @@ import (
"tailscale.com/control/controlknobs" "tailscale.com/control/controlknobs"
"tailscale.com/drive" "tailscale.com/drive"
"tailscale.com/health"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/conffile" "tailscale.com/ipn/conffile"
"tailscale.com/net/dns" "tailscale.com/net/dns"
@ -63,6 +64,8 @@ type System struct {
controlKnobs controlknobs.Knobs controlKnobs controlknobs.Knobs
proxyMap proxymap.Mapper proxyMap proxymap.Mapper
healthTracker health.Tracker
} }
// NetstackImpl is the interface that *netstack.Impl implements. // NetstackImpl is the interface that *netstack.Impl implements.
@ -134,6 +137,19 @@ func (s *System) ProxyMapper() *proxymap.Mapper {
return &s.proxyMap return &s.proxyMap
} }
// HealthTracker returns the system health tracker.
func (s *System) HealthTracker() *health.Tracker {
// TODO(bradfitz): plumb the tsd.System.HealthTracker() value
// everywhere and then then remove this use of the global
// and remove health.Global entirely. But for now we keep
// the two in sync during plumbing.
const stillPlumbing = true
if stillPlumbing {
return health.Global
}
return &s.healthTracker
}
// SubSystem represents some subsystem of the Tailscale node daemon. // SubSystem represents some subsystem of the Tailscale node daemon.
// //
// A subsystem can be set to a value, and then later retrieved. A subsystem // A subsystem can be set to a value, and then later retrieved. A subsystem

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl" _ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/health"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile" _ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal" _ "tailscale.com/ipn/ipnlocal"

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl" _ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/health"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile" _ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal" _ "tailscale.com/ipn/ipnlocal"

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl" _ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/health"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile" _ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal" _ "tailscale.com/ipn/ipnlocal"

@ -17,6 +17,7 @@ import (
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl" _ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/health"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile" _ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal" _ "tailscale.com/ipn/ipnlocal"

@ -24,6 +24,7 @@ import (
_ "tailscale.com/derp/derphttp" _ "tailscale.com/derp/derphttp"
_ "tailscale.com/drive/driveimpl" _ "tailscale.com/drive/driveimpl"
_ "tailscale.com/envknob" _ "tailscale.com/envknob"
_ "tailscale.com/health"
_ "tailscale.com/ipn" _ "tailscale.com/ipn"
_ "tailscale.com/ipn/conffile" _ "tailscale.com/ipn/conffile"
_ "tailscale.com/ipn/ipnlocal" _ "tailscale.com/ipn/ipnlocal"

@ -400,6 +400,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netip.AddrPort, peer key.NodePublic) cha
} }
return derpMap.Regions[regionID] return derpMap.Regions[regionID]
}) })
dc.HealthTracker = health.Global
dc.SetCanAckPings(true) dc.SetCanAckPings(true)
dc.NotePreferred(c.myDerp == regionID) dc.NotePreferred(c.myDerp == regionID)

Loading…
Cancel
Save