mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 12:58:37 +00:00

Update tailscale/corp#20677 While hunting down DNS issues, it was observed that on apple platforms, w attempt to rebind to interfaces before they're completely configured by the system. The AF_ROUTE messages aren't lying, they're just not taking into account other setup that has to happen, notably the system setting the interface nameservers. When we react based on the route updates, it's often too quickly. This happens frequently on wakes-from-sleep. This replumbs netmon so that (optionally) rather than reacting to every system message by checking for interesting changes, we use that only to update our state, and recheck for interesting network changes when we get a poke from the upper layers indicating that a network change has 'completed'. Notably on Apple, NWPathMonitor seems to call us back at the correct time(s) to check for potentially interesting interface changes that have been observed via AF_ROUTE and to react to them. This seems to reduce thrashing during network transitions, and avoids some of the bad states we were previously ending up in. This is entirely opt in during the construction of the netmon and will have no effect until/unless a specific client chooses to use it. Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
666 lines
18 KiB
Go
666 lines
18 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package monitor provides facilities for monitoring network
|
|
// interface and route changes. It primarily exists to know when
|
|
// portable devices move between different networks.
|
|
package netmon
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/netip"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/clientmetric"
|
|
"tailscale.com/util/set"
|
|
)
|
|
|
|
// pollWallTimeInterval is how often we check the time to check
|
|
// for big jumps in wall (non-monotonic) time as a backup mechanism
|
|
// to get notified of a sleeping device waking back up.
|
|
// Usually there are also minor network change events on wake that let
|
|
// us check the wall time sooner than this.
|
|
const pollWallTimeInterval = 15 * time.Second
|
|
|
|
// message represents a message returned from an osMon.
|
|
type message interface {
|
|
// Ignore is whether we should ignore this message.
|
|
ignore() bool
|
|
}
|
|
|
|
// osMon is the interface that each operating system-specific
|
|
// implementation of the link monitor must implement.
|
|
type osMon interface {
|
|
Close() error
|
|
|
|
// Receive returns a new network interface change message. It
|
|
// should block until there's either something to return, or
|
|
// until the osMon is closed. After a Close, the returned
|
|
// error is ignored.
|
|
Receive() (message, error)
|
|
|
|
// IsInterestingInterface reports whether the provided interface should
|
|
// be considered for network change events.
|
|
IsInterestingInterface(iface string) bool
|
|
}
|
|
|
|
// Monitor represents a monitoring instance.
|
|
type Monitor struct {
|
|
logf logger.Logf
|
|
om osMon // nil means not supported on this platform
|
|
change chan bool // send false to wake poller, true to also force ChangeDeltas be sent
|
|
stop chan struct{} // closed on Stop
|
|
static bool // static Monitor that doesn't actually monitor
|
|
|
|
// Things that must be set early, before use,
|
|
// and not change at runtime.
|
|
tsIfName string // tailscale interface name, if known/set ("tailscale0", "utun3", ...)
|
|
|
|
mu sync.Mutex // guards all following fields
|
|
cbs set.HandleSet[ChangeFunc]
|
|
ruleDelCB set.HandleSet[RuleDeleteCallback]
|
|
ifState *State
|
|
gwValid bool // whether gw and gwSelfIP are valid
|
|
gw netip.Addr // our gateway's IP
|
|
gwSelfIP netip.Addr // our own IP address (that corresponds to gw)
|
|
started bool
|
|
closed bool
|
|
goroutines sync.WaitGroup
|
|
wallTimer *time.Timer // nil until Started; re-armed AfterFunc per tick
|
|
lastWall time.Time
|
|
timeJumped bool // whether we need to send a changed=true after a big time jump
|
|
waitForOSNotify bool // if true we should wait for an os poke before reacting to interface changes
|
|
}
|
|
|
|
// ChangeFunc is a callback function registered with Monitor that's called when the
|
|
// network changed.
|
|
type ChangeFunc func(*ChangeDelta)
|
|
|
|
// ChangeDelta describes the difference between two network states.
|
|
type ChangeDelta struct {
|
|
// Monitor is the network monitor that sent this delta.
|
|
Monitor *Monitor
|
|
|
|
// Old is the old interface state, if known.
|
|
// It's nil if the old state is unknown.
|
|
// Do not mutate it.
|
|
Old *State
|
|
|
|
// New is the new network state.
|
|
// It is always non-nil.
|
|
// Do not mutate it.
|
|
New *State
|
|
|
|
// Major is our legacy boolean of whether the network changed in some major
|
|
// way.
|
|
//
|
|
// Deprecated: do not remove. As of 2023-08-23 we're in a renewed effort to
|
|
// remove it and ask specific qustions of ChangeDelta instead. Look at Old
|
|
// and New (or add methods to ChangeDelta) instead of using Major.
|
|
Major bool
|
|
|
|
// TimeJumped is whether there was a big jump in wall time since the last
|
|
// time we checked. This is a hint that a mobile sleeping device might have
|
|
// come out of sleep.
|
|
TimeJumped bool
|
|
|
|
// TODO(bradfitz): add some lazy cached fields here as needed with methods
|
|
// on *ChangeDelta to let callers ask specific questions
|
|
}
|
|
|
|
// New instantiates and starts a monitoring instance.
|
|
// The returned monitor is inactive until it's started by the Start method.
|
|
// Use RegisterChangeCallback to get notified of network changes.
|
|
func New(logf logger.Logf, waitForOSNotify ...bool) (*Monitor, error) {
|
|
logf = logger.WithPrefix(logf, "monitor: ")
|
|
wait := false
|
|
if len(waitForOSNotify) > 0 {
|
|
wait = waitForOSNotify[0]
|
|
}
|
|
|
|
m := &Monitor{
|
|
logf: logf,
|
|
change: make(chan bool, 1),
|
|
stop: make(chan struct{}),
|
|
lastWall: wallTime(),
|
|
waitForOSNotify: wait,
|
|
}
|
|
st, err := m.interfaceStateUncached()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.ifState = st
|
|
|
|
m.om, err = newOSMon(logf, m)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if m.om == nil {
|
|
return nil, errors.New("newOSMon returned nil, nil")
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
// NewStatic returns a Monitor that's a one-time snapshot of the network state
|
|
// but doesn't actually monitor for changes. It should only be used in tests
|
|
// and situations like cleanups or short-lived CLI programs.
|
|
func NewStatic() *Monitor {
|
|
m := &Monitor{static: true}
|
|
if st, err := m.interfaceStateUncached(); err == nil {
|
|
m.ifState = st
|
|
}
|
|
return m
|
|
}
|
|
|
|
// InterfaceState returns the latest snapshot of the machine's network
|
|
// interfaces.
|
|
//
|
|
// The returned value is owned by Mon; it must not be modified.
|
|
func (m *Monitor) InterfaceState() *State {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.ifState
|
|
}
|
|
|
|
func (m *Monitor) interfaceStateUncached() (*State, error) {
|
|
return GetState()
|
|
}
|
|
|
|
// SetTailscaleInterfaceName sets the name of the Tailscale interface. For
|
|
// example, "tailscale0", "tun0", "utun3", etc.
|
|
//
|
|
// This must be called only early in tailscaled startup before the monitor is
|
|
// used.
|
|
func (m *Monitor) SetTailscaleInterfaceName(ifName string) {
|
|
m.tsIfName = ifName
|
|
}
|
|
|
|
// GatewayAndSelfIP returns the current network's default gateway, and
|
|
// the machine's default IP for that gateway.
|
|
//
|
|
// It's the same as interfaces.LikelyHomeRouterIP, but it caches the
|
|
// result until the monitor detects a network change.
|
|
func (m *Monitor) GatewayAndSelfIP() (gw, myIP netip.Addr, ok bool) {
|
|
if m.static {
|
|
return
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.gwValid {
|
|
return m.gw, m.gwSelfIP, true
|
|
}
|
|
gw, myIP, ok = LikelyHomeRouterIP()
|
|
changed := false
|
|
if ok {
|
|
changed = m.gw != gw || m.gwSelfIP != myIP
|
|
m.gw, m.gwSelfIP = gw, myIP
|
|
m.gwValid = true
|
|
}
|
|
if changed {
|
|
m.logf("gateway and self IP changed: gw=%v self=%v", m.gw, m.gwSelfIP)
|
|
}
|
|
return gw, myIP, ok
|
|
}
|
|
|
|
// RegisterChangeCallback adds callback to the set of parties to be
|
|
// notified (in their own goroutine) when the network state changes.
|
|
// To remove this callback, call unregister (or close the monitor).
|
|
func (m *Monitor) RegisterChangeCallback(callback ChangeFunc) (unregister func()) {
|
|
if m.static {
|
|
return func() {}
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
handle := m.cbs.Add(callback)
|
|
return func() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.cbs, handle)
|
|
}
|
|
}
|
|
|
|
// RuleDeleteCallback is a callback when a Linux IP policy routing
|
|
// rule is deleted. The table is the table number (52, 253, 354) and
|
|
// priority is the priority order number (for Tailscale rules
|
|
// currently: 5210, 5230, 5250, 5270)
|
|
type RuleDeleteCallback func(table uint8, priority uint32)
|
|
|
|
// RegisterRuleDeleteCallback adds callback to the set of parties to be
|
|
// notified (in their own goroutine) when a Linux ip rule is deleted.
|
|
// To remove this callback, call unregister (or close the monitor).
|
|
func (m *Monitor) RegisterRuleDeleteCallback(callback RuleDeleteCallback) (unregister func()) {
|
|
if m.static {
|
|
return func() {}
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
handle := m.ruleDelCB.Add(callback)
|
|
return func() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.ruleDelCB, handle)
|
|
}
|
|
}
|
|
|
|
// Start starts the monitor.
|
|
// A monitor can only be started & closed once.
|
|
func (m *Monitor) Start() {
|
|
if m.static {
|
|
return
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.started || m.closed {
|
|
return
|
|
}
|
|
m.started = true
|
|
|
|
if shouldMonitorTimeJump {
|
|
m.wallTimer = time.AfterFunc(pollWallTimeInterval, m.pollWallTime)
|
|
}
|
|
|
|
if m.om == nil {
|
|
return
|
|
}
|
|
|
|
m.goroutines.Add(2)
|
|
go m.pump()
|
|
|
|
// When UsenativeMonitors is set, the expectation is that we'll be notified
|
|
// of network changes via HandleMajorNetworkChange. We will still collect
|
|
// osMon events and keep our internal state up to date, but we will not
|
|
// act on them until the native monitor gives us a poke.
|
|
if !m.waitForOSNotify {
|
|
go m.debounce()
|
|
}
|
|
}
|
|
|
|
// Close closes the monitor.
|
|
func (m *Monitor) Close() error {
|
|
if m.static {
|
|
return nil
|
|
}
|
|
m.mu.Lock()
|
|
if m.closed {
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
m.closed = true
|
|
close(m.stop)
|
|
|
|
if m.wallTimer != nil {
|
|
m.wallTimer.Stop()
|
|
}
|
|
|
|
var err error
|
|
if m.om != nil {
|
|
err = m.om.Close()
|
|
}
|
|
|
|
started := m.started
|
|
m.mu.Unlock()
|
|
|
|
if started {
|
|
m.goroutines.Wait()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// InjectEvent forces the monitor to pretend there was a network
|
|
// change and re-check the state of the network. Any registered
|
|
// ChangeFunc callbacks will be called within the event coalescing
|
|
// period (under a fraction of a second).
|
|
func (m *Monitor) InjectEvent() {
|
|
if m.static {
|
|
return
|
|
}
|
|
select {
|
|
case m.change <- true:
|
|
default:
|
|
// Another change signal is already
|
|
// buffered. Debounce will wake up soon
|
|
// enough.
|
|
}
|
|
}
|
|
|
|
// Poll forces the monitor to pretend there was a network
|
|
// change and re-check the state of the network.
|
|
//
|
|
// This is like InjectEvent but only fires ChangeFunc callbacks
|
|
// if the network state differed at all.
|
|
func (m *Monitor) Poll() {
|
|
if m.static {
|
|
return
|
|
}
|
|
select {
|
|
case m.change <- false:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (m *Monitor) stopped() bool {
|
|
select {
|
|
case <-m.stop:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// pump continuously retrieves messages from the connection, notifying
|
|
// the change channel of changes, and stopping when a stop is issued.
|
|
func (m *Monitor) pump() {
|
|
defer m.goroutines.Done()
|
|
for !m.stopped() {
|
|
msg, err := m.om.Receive()
|
|
if err != nil {
|
|
if m.stopped() {
|
|
return
|
|
}
|
|
// Keep retrying while we're not closed.
|
|
m.logf("error from link monitor: %v", err)
|
|
time.Sleep(time.Second)
|
|
continue
|
|
}
|
|
if rdm, ok := msg.(ipRuleDeletedMessage); ok {
|
|
m.notifyRuleDeleted(rdm)
|
|
continue
|
|
}
|
|
if msg.ignore() {
|
|
continue
|
|
}
|
|
m.Poll()
|
|
}
|
|
}
|
|
|
|
func (m *Monitor) notifyRuleDeleted(rdm ipRuleDeletedMessage) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, cb := range m.ruleDelCB {
|
|
go cb(rdm.table, rdm.priority)
|
|
}
|
|
}
|
|
|
|
// isInterestingInterface reports whether the provided interface should be
|
|
// considered when checking for network state changes.
|
|
// The ips parameter should be the IPs of the provided interface.
|
|
func (m *Monitor) isInterestingInterface(i Interface, ips []netip.Prefix) bool {
|
|
if !m.om.IsInterestingInterface(i.Name) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// debounce calls the callback function with a delay between events
|
|
// and exits when a stop is issued.
|
|
func (m *Monitor) debounce() {
|
|
defer m.goroutines.Done()
|
|
for {
|
|
var forceCallbacks bool
|
|
select {
|
|
case <-m.stop:
|
|
return
|
|
case forceCallbacks = <-m.change:
|
|
}
|
|
|
|
if newState, err := m.interfaceStateUncached(); err != nil {
|
|
m.logf("interfaces.State: %v", err)
|
|
} else {
|
|
m.handlePotentialChange(newState, forceCallbacks)
|
|
}
|
|
|
|
select {
|
|
case <-m.stop:
|
|
return
|
|
case <-time.After(250 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
metricChangeEq = clientmetric.NewCounter("netmon_link_change_eq")
|
|
metricChange = clientmetric.NewCounter("netmon_link_change")
|
|
metricChangeTimeJump = clientmetric.NewCounter("netmon_link_change_timejump")
|
|
metricChangeMajor = clientmetric.NewCounter("netmon_link_change_major")
|
|
)
|
|
|
|
// handlePotentialChange considers whether newState is different enough to wake
|
|
// up callers and updates the monitor's state if so.
|
|
//
|
|
// If forceCallbacks is true, they're always notified.
|
|
func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
oldState := m.ifState
|
|
timeJumped := shouldMonitorTimeJump && m.checkWallTimeAdvanceLocked()
|
|
if !timeJumped && !forceCallbacks && oldState.Equal(newState) {
|
|
// Exactly equal. Nothing to do.
|
|
metricChangeEq.Add(1)
|
|
return
|
|
}
|
|
|
|
delta := &ChangeDelta{
|
|
Monitor: m,
|
|
Old: oldState,
|
|
New: newState,
|
|
TimeJumped: timeJumped,
|
|
}
|
|
|
|
delta.Major = m.IsMajorChangeFrom(oldState, newState)
|
|
if delta.Major {
|
|
m.gwValid = false
|
|
m.ifState = newState
|
|
|
|
if s1, s2 := oldState.String(), delta.New.String(); s1 == s2 {
|
|
m.logf("[unexpected] network state changed, but stringification didn't: %v", s1)
|
|
m.logf("[unexpected] old: %s", jsonSummary(oldState))
|
|
m.logf("[unexpected] new: %s", jsonSummary(newState))
|
|
}
|
|
}
|
|
// See if we have a queued or new time jump signal.
|
|
if timeJumped {
|
|
m.resetTimeJumpedLocked()
|
|
if !delta.Major {
|
|
// Only log if it wasn't an interesting change.
|
|
m.logf("time jumped (probably wake from sleep); synthesizing major change event")
|
|
delta.Major = true
|
|
}
|
|
}
|
|
metricChange.Add(1)
|
|
if delta.Major {
|
|
metricChangeMajor.Add(1)
|
|
}
|
|
if delta.TimeJumped {
|
|
metricChangeTimeJump.Add(1)
|
|
}
|
|
for _, cb := range m.cbs {
|
|
go cb(delta)
|
|
}
|
|
}
|
|
|
|
// IsMajorChangeFrom reports whether the transition from s1 to s2 is
|
|
// a "major" change, where major roughly means it's worth tearing down
|
|
// a bunch of connections and rebinding.
|
|
//
|
|
// TODO(bradiftz): tigten this definition.
|
|
func (m *Monitor) IsMajorChangeFrom(s1, s2 *State) bool {
|
|
if s1 == nil && s2 == nil {
|
|
return false
|
|
}
|
|
if s1 == nil || s2 == nil {
|
|
return true
|
|
}
|
|
if s1.HaveV6 != s2.HaveV6 ||
|
|
s1.HaveV4 != s2.HaveV4 ||
|
|
s1.IsExpensive != s2.IsExpensive ||
|
|
s1.DefaultRouteInterface != s2.DefaultRouteInterface ||
|
|
s1.HTTPProxy != s2.HTTPProxy ||
|
|
s1.PAC != s2.PAC {
|
|
return true
|
|
}
|
|
for iname, i := range s1.Interface {
|
|
if iname == m.tsIfName {
|
|
// Ignore changes in the Tailscale interface itself.
|
|
continue
|
|
}
|
|
ips := s1.InterfaceIPs[iname]
|
|
if !m.isInterestingInterface(i, ips) {
|
|
continue
|
|
}
|
|
i2, ok := s2.Interface[iname]
|
|
if !ok {
|
|
return true
|
|
}
|
|
ips2, ok := s2.InterfaceIPs[iname]
|
|
if !ok {
|
|
return true
|
|
}
|
|
if !i.Equal(i2) || !prefixesMajorEqual(ips, ips2) {
|
|
return true
|
|
}
|
|
}
|
|
// Iterate over s2 in case there is a field in s2 that doesn't exist in s1
|
|
for iname, i := range s2.Interface {
|
|
if iname == m.tsIfName {
|
|
// Ignore changes in the Tailscale interface itself.
|
|
continue
|
|
}
|
|
ips := s2.InterfaceIPs[iname]
|
|
if !m.isInterestingInterface(i, ips) {
|
|
continue
|
|
}
|
|
i1, ok := s1.Interface[iname]
|
|
if !ok {
|
|
return true
|
|
}
|
|
ips1, ok := s1.InterfaceIPs[iname]
|
|
if !ok {
|
|
return true
|
|
}
|
|
if !i.Equal(i1) || !prefixesMajorEqual(ips, ips1) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// prefixesMajorEqual reports whether a and b are equal after ignoring
|
|
// boring things like link-local, loopback, and multicast addresses.
|
|
func prefixesMajorEqual(a, b []netip.Prefix) bool {
|
|
// trim returns a subslice of p with link local unicast,
|
|
// loopback, and multicast prefixes removed from the front.
|
|
trim := func(p []netip.Prefix) []netip.Prefix {
|
|
for len(p) > 0 {
|
|
a := p[0].Addr()
|
|
if a.IsLinkLocalUnicast() || a.IsLoopback() || a.IsMulticast() {
|
|
p = p[1:]
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
return p
|
|
}
|
|
for {
|
|
a = trim(a)
|
|
b = trim(b)
|
|
if len(a) == 0 || len(b) == 0 {
|
|
return len(a) == 0 && len(b) == 0
|
|
}
|
|
if a[0] != b[0] {
|
|
return false
|
|
}
|
|
a, b = a[1:], b[1:]
|
|
}
|
|
}
|
|
|
|
func jsonSummary(x any) any {
|
|
j, err := json.Marshal(x)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return j
|
|
}
|
|
|
|
func wallTime() time.Time {
|
|
// From time package's docs: "The canonical way to strip a
|
|
// monotonic clock reading is to use t = t.Round(0)."
|
|
return time.Now().Round(0)
|
|
}
|
|
|
|
func (m *Monitor) pollWallTime() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.closed {
|
|
return
|
|
}
|
|
if m.checkWallTimeAdvanceLocked() {
|
|
m.InjectEvent()
|
|
}
|
|
m.wallTimer.Reset(pollWallTimeInterval)
|
|
}
|
|
|
|
// shouldMonitorTimeJump is whether we keep a regular periodic timer running in
|
|
// the background watching for jumps in wall time.
|
|
//
|
|
// We don't do this on mobile platforms for battery reasons, and because these
|
|
// platforms don't really sleep in the same way.
|
|
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios"
|
|
|
|
// checkWallTimeAdvanceLocked reports whether wall time jumped more than 150% of
|
|
// pollWallTimeInterval, indicating we probably just came out of sleep. Once a
|
|
// time jump is detected it must be reset by calling resetTimeJumpedLocked.
|
|
func (m *Monitor) checkWallTimeAdvanceLocked() bool {
|
|
if !shouldMonitorTimeJump {
|
|
panic("unreachable") // if callers are correct
|
|
}
|
|
now := wallTime()
|
|
if now.Sub(m.lastWall) > pollWallTimeInterval*3/2 {
|
|
m.timeJumped = true // it is reset by debounce.
|
|
}
|
|
m.lastWall = now
|
|
return m.timeJumped
|
|
}
|
|
|
|
// resetTimeJumpedLocked consumes the signal set by checkWallTimeAdvanceLocked.
|
|
func (m *Monitor) resetTimeJumpedLocked() {
|
|
m.timeJumped = false
|
|
}
|
|
|
|
type ipRuleDeletedMessage struct {
|
|
table uint8
|
|
priority uint32
|
|
}
|
|
|
|
func (ipRuleDeletedMessage) ignore() bool { return true }
|
|
|
|
// If a platform has an network monitoring service which can/should be used to indicate *when* a
|
|
// new interfaces (or no interface) is ready, call this function at the appropriate time. It is
|
|
// important that this is called whenever and interface is added or removed. The caller should
|
|
// debounce and remove any duplicate calls to this function and this will only be relevant if
|
|
// the netmon was constructed with the waitForOSNotify flag set.
|
|
//
|
|
// For example, Apple devices may choose to use NEPathMonitor to detect network changes and call
|
|
// in here when the list of availableInterfaces changes, and/or when the viability of the path changes.
|
|
//
|
|
// Use this with caution. Usually, netmon is able to detect network changes on it's own.
|
|
func (m *Monitor) NotifyPotentialNetworkChange() {
|
|
if !m.waitForOSNotify {
|
|
m.logf("warning: not using os level monitors, but we're getting a network change event. Ignoring.")
|
|
return
|
|
}
|
|
|
|
if newState, err := m.interfaceStateUncached(); err != nil {
|
|
m.logf("network change ignored. cannot get cached interfaces.State: %v", err)
|
|
} else {
|
|
m.logf("handling potentially interesting network change after os network change")
|
|
m.handlePotentialChange(newState, false)
|
|
}
|
|
}
|