// Copyright (c) 2020 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 monitor import ( "context" "errors" "strings" "sync" "time" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "tailscale.com/net/tsaddr" "tailscale.com/types/logger" ) 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 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(logf logger.Logf, _ *Mon) (osMon, error) { m := &winMon{ logf: logf, messagec: make(chan eventMessage, 1), noDeadlockTicker: time.NewTicker(5000 * time.Hour), // arbitrary } var err error m.addressChangeCallback, err = winipcfg.RegisterUnicastAddressChangeCallback(m.unicastAddressChanged) if err != nil { m.logf("winipcfg.RegisterUnicastAddressChangeCallback error: %v", err) return nil, err } m.routeChangeCallback, err = winipcfg.RegisterRouteChangeCallback(m.routeChanged) if err != nil { m.addressChangeCallback.Unregister() m.logf("winipcfg.RegisterRouteChangeCallback error: %v", err) return nil, err } m.ctx, m.cancel = context.WithCancel(context.Background()) return m, nil } func (m *winMon) IsInterestingInterface(iface string) bool { return true } 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) { 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) { 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 } }