3e89068792
updates tailscale/corp#33891 Addresses several older the TODO's in netmon. This removes the Major flag precomputes the ChangeDelta state, rather than making consumers of ChangeDeltas sort that out themselves. We're also seeing a lot of ChangeDelta's being flagged as "Major" when they are not interesting, triggering rebinds in wgengine that are not needed. This cleans that up and adds a host of additional tests. The dependencies are cleaned, notably removing dependency on netmon itself for calculating what is interesting, and what is not. This includes letting individual platforms set a bespoke global "IsInterestingInterface" function. This is only used on Darwin. RebindRequired now roughly follows how "Major" was historically calculated but includes some additional checks for various uninteresting events such as changes in interface addresses that shouldn't trigger a rebind. This significantly reduces thrashing (by roughly half on Darwin clients which switching between nics). The individual values that we roll into RebindRequired are also exposed so that components consuming netmap.ChangeDelta can ask more targeted questions. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
190 lines
5.3 KiB
Go
190 lines
5.3 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package netmon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
|
"tailscale.com/net/tsaddr"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/eventbus"
|
|
)
|
|
|
|
var (
|
|
errClosed = errors.New("closed")
|
|
)
|
|
|
|
type eventMessage struct {
|
|
eventType string
|
|
}
|
|
|
|
func (eventMessage) ignore() bool { return false }
|
|
|
|
type winMon struct {
|
|
logf logger.Logf
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
isActive func() bool
|
|
messagec chan eventMessage
|
|
addressChangeCallback *winipcfg.UnicastAddressChangeCallback
|
|
routeChangeCallback *winipcfg.RouteChangeCallback
|
|
|
|
mu sync.Mutex
|
|
lastLog time.Time // time we last logged about any windows change event
|
|
|
|
// noDeadlockTicker exists just to have something scheduled as
|
|
// far as the Go runtime is concerned. Otherwise "tailscaled
|
|
// debug --monitor" thinks it's deadlocked with nothing to do,
|
|
// as Go's runtime doesn't know about callbacks registered with
|
|
// Windows.
|
|
noDeadlockTicker *time.Ticker
|
|
}
|
|
|
|
func newOSMon(_ *eventbus.Bus, logf logger.Logf, pm *Monitor) (osMon, error) {
|
|
m := &winMon{
|
|
logf: logf,
|
|
isActive: pm.isActive,
|
|
messagec: make(chan eventMessage, 1),
|
|
noDeadlockTicker: time.NewTicker(5000 * time.Hour), // arbitrary
|
|
}
|
|
m.ctx, m.cancel = context.WithCancel(context.Background())
|
|
|
|
var err error
|
|
m.addressChangeCallback, err = winipcfg.RegisterUnicastAddressChangeCallback(m.unicastAddressChanged)
|
|
if err != nil {
|
|
m.logf("winipcfg.RegisterUnicastAddressChangeCallback error: %v", err)
|
|
m.cancel()
|
|
return nil, err
|
|
}
|
|
|
|
m.routeChangeCallback, err = winipcfg.RegisterRouteChangeCallback(m.routeChanged)
|
|
if err != nil {
|
|
m.addressChangeCallback.Unregister()
|
|
m.logf("winipcfg.RegisterRouteChangeCallback error: %v", err)
|
|
m.cancel()
|
|
return nil, err
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (m *winMon) Close() (ret error) {
|
|
m.cancel()
|
|
m.noDeadlockTicker.Stop()
|
|
|
|
if m.addressChangeCallback != nil {
|
|
if err := m.addressChangeCallback.Unregister(); err != nil {
|
|
m.logf("addressChangeCallback.Unregister error: %v", err)
|
|
ret = err
|
|
} else {
|
|
m.addressChangeCallback = nil
|
|
}
|
|
}
|
|
|
|
if m.routeChangeCallback != nil {
|
|
if err := m.routeChangeCallback.Unregister(); err != nil {
|
|
m.logf("routeChangeCallback.Unregister error: %v", err)
|
|
ret = err
|
|
} else {
|
|
m.routeChangeCallback = nil
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (m *winMon) Receive() (message, error) {
|
|
if m.ctx.Err() != nil {
|
|
m.logf("Receive call on closed monitor")
|
|
return nil, errClosed
|
|
}
|
|
|
|
t0 := time.Now()
|
|
|
|
select {
|
|
case msg := <-m.messagec:
|
|
now := time.Now()
|
|
m.mu.Lock()
|
|
sinceLast := now.Sub(m.lastLog)
|
|
m.lastLog = now
|
|
m.mu.Unlock()
|
|
// If it's either been awhile since we last logged
|
|
// anything, or if this some route/addr that's not
|
|
// about a Tailscale IP ("ts" prefix), then log. This
|
|
// is mainly limited to suppress the flood about our own
|
|
// route updates after connecting to a large tailnet
|
|
// and all the IPv4 /32 routes.
|
|
if sinceLast > 5*time.Second || !strings.HasPrefix(msg.eventType, "ts") {
|
|
m.logf("got windows change event after %v: evt=%s", time.Since(t0).Round(time.Millisecond), msg.eventType)
|
|
}
|
|
return msg, nil
|
|
case <-m.ctx.Done():
|
|
return nil, errClosed
|
|
}
|
|
}
|
|
|
|
// unicastAddressChanged is the callback we register with Windows to call when unicast address changes.
|
|
func (m *winMon) unicastAddressChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibUnicastIPAddressRow) {
|
|
if !m.isActive() {
|
|
// Avoid starting a goroutine that sends events to messagec,
|
|
// or sending messages to messagec directly, if the monitor
|
|
// hasn't started and Receive is not yet reading from messagec.
|
|
//
|
|
// Doing so can lead to goroutine leaks or deadlocks, especially
|
|
// if the monitor is never started.
|
|
return
|
|
}
|
|
|
|
what := "addr"
|
|
if ip := row.Address.Addr(); ip.IsValid() && tsaddr.IsTailscaleIP(ip.Unmap()) {
|
|
what = "tsaddr"
|
|
}
|
|
|
|
// start a goroutine to finish our work, to return to Windows out of this callback
|
|
go m.somethingChanged(what)
|
|
}
|
|
|
|
// routeChanged is the callback we register with Windows to call when route changes.
|
|
func (m *winMon) routeChanged(_ winipcfg.MibNotificationType, row *winipcfg.MibIPforwardRow2) {
|
|
if !m.isActive() {
|
|
// Avoid starting a goroutine that sends events to messagec,
|
|
// or sending messages to messagec directly, if the monitor
|
|
// hasn't started and Receive is not yet reading from messagec.
|
|
//
|
|
// Doing so can lead to goroutine leaks or deadlocks, especially
|
|
// if the monitor is never started.
|
|
return
|
|
}
|
|
|
|
what := "route"
|
|
ip := row.DestinationPrefix.Prefix().Addr().Unmap()
|
|
if ip.IsValid() && tsaddr.IsTailscaleIP(ip) {
|
|
what = "tsroute"
|
|
}
|
|
// start a goroutine to finish our work, to return to Windows out of this callback
|
|
go m.somethingChanged(what)
|
|
}
|
|
|
|
// somethingChanged gets called from OS callbacks whenever address or route changes.
|
|
func (m *winMon) somethingChanged(evt string) {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return
|
|
case m.messagec <- eventMessage{eventType: evt}:
|
|
return
|
|
}
|
|
}
|
|
|
|
// isActive reports whether this monitor has been started and not yet closed.
|
|
func (m *Monitor) isActive() bool {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.started && !m.closed
|
|
}
|