Files
ctrld/cmd/cli/network_events.go
T

168 lines
5.6 KiB
Go

package cli
import (
"time"
"github.com/Control-D-Inc/ctrld"
"tailscale.com/net/netmon"
)
// networkEventsNowFn is the clock of the network events. Tests replace it.
var networkEventsNowFn = time.Now
const (
hostWokeMessage = "Host woke"
// noiseDeltaMessage names the deltas that no part of the daemon acts on.
noiseDeltaMessage = "Network delta noise"
// interfaceChangedMessage names one interface that a delta changed.
interfaceChangedMessage = "Network interface changed"
// networkTransitionMessage names the outcome of one delta.
networkTransitionMessage = "Network transition"
// transitionOutcomeIgnored names a delta that left every usable interface
// as it was.
transitionOutcomeIgnored = "ignored"
// transitionOutcomeSnapshotSuperseded names a delta whose snapshot netmon
// already replaced. A newer callback reports the same interfaces.
transitionOutcomeSnapshotSuperseded = "snapshot_superseded"
)
// noteHostWoke reports one wake to the wake reporter. The caller passes the
// network of the wake, because the state that ctrld holds still describes the
// network before the sleep. netmon reports a time jump and measures no gap, so
// its report waits for the detector, and the journal gets one event per wake.
func (p *prog) noteHostWoke(source string, gap time.Duration, state *netmon.State) {
now := networkEventsNowFn()
// A resume moves the resolvers of the host, so every wake source starts the
// fast poll.
if p.dnsConfig != nil {
p.dnsConfig.noteActivity(now)
}
p.wake.note(now, wakeReport{source: source, gap: gap, state: state}, logHostWoke)
}
// logHostWoke writes the Host woke journal event of one report.
func logHostWoke(report wakeReport) {
gapKnown := report.gap > 0
event := journal(mainLog.Load().Info()).Str("source", report.source).Bool("gap_known", gapKnown)
if gapKnown {
event.Int64("gap_ms", report.gap.Milliseconds())
}
event.Str("default_route", defaultRouteOf(report.state)).
Int("interfaces_up", upInterfaceCount(report.state)).
Msg(hostWokeMessage)
}
// noteNoiseDelta records one delta of a noise storm. AirDrop and the virtual
// adapters of a container host change every few seconds, so the journal keeps
// one line per window while the debug stream keeps them all.
func (p *prog) noteNoiseDelta(changes []interfaceChange) {
names := changedInterfaceNames(changes)
mainLog.Load().Debug().Strs("interfaces", names).Msg(noiseDeltaMessage)
if summary, emit := p.networkNoise.add(names, networkEventsNowFn()); emit {
logNoiseSummary(summary)
}
}
// flushNoiseSummary closes the open noise window, because the delta that the
// daemon acts on ends the storm.
func (p *prog) flushNoiseSummary() {
if summary, emit := p.networkNoise.flush(networkEventsNowFn()); emit {
logNoiseSummary(summary)
}
}
// logNoiseSummary writes one journal line for a window of noise deltas.
func logNoiseSummary(summary noiseSummary) {
journal(mainLog.Load().Info()).
Int("count", summary.Count).
Strs("interfaces", summary.Interfaces).
Str("first_at", summary.First.Format(time.RFC3339)).
Str("last_at", summary.Last.Format(time.RFC3339)).
Msg(noiseDeltaMessage)
}
// logInterfaceChanges puts the interfaces of one transition in the journal.
// Support reads back the port that lost its address at the time of an outage.
// An AirDrop or a container adapter changes every few seconds, so the
// transition names it and the journal keeps the interfaces that carry the
// traffic of the host.
func logInterfaceChanges(transitionID uint64, changes []interfaceChange) {
for _, change := range changes {
if noiseClass(change.Class) {
continue
}
journal(mainLog.Load().Info()).
Uint64("transition_id", transitionID).
Str("interface", change.Name).
Str("action", change.Action).
Str("class", change.Class).
Str("hardware_port", change.HardwarePort).
Str("service", change.Service).
Strs("ips_before", change.IPsBefore).
Strs("ips_after", change.IPsAfter).
Str("flags", change.Flags).
Int("mtu", change.MTU).
Bool("is_default_route", change.IsDefaultRoute).
Msg(interfaceChangedMessage)
}
}
// networkTransitionEvent picks the level of one transition line. An ignored
// delta and a superseded snapshot arrive many times per minute, so the journal
// keeps the outcomes that can change the resolver state only.
func networkTransitionEvent(outcome string) *ctrld.LogEvent {
if outcome == transitionOutcomeIgnored || outcome == transitionOutcomeSnapshotSuperseded {
return mainLog.Load().Debug()
}
return journal(mainLog.Load().Info())
}
// changedInterfaceNames names the interfaces of one delta, in the order of the
// diff.
func changedInterfaceNames(changes []interfaceChange) []string {
names := make([]string, 0, len(changes))
for _, change := range changes {
names = append(names, change.Name)
}
return names
}
// hasAddOrRemove reports a delta that changed the adapter set of the host. The
// platform names of an adapter come from a cache, and that cache holds nothing
// for an adapter that just appeared.
func hasAddOrRemove(changes []interfaceChange) bool {
for _, change := range changes {
if change.Action == "added" || change.Action == "removed" {
return true
}
}
return false
}
func defaultRouteOf(state *netmon.State) string {
if state == nil {
return ""
}
return state.DefaultRouteInterface
}
// upInterfaceCount counts the interfaces with the up flag. A netmon entry
// without an embedded interface counts as down.
func upInterfaceCount(state *netmon.State) int {
if state == nil {
return 0
}
up := 0
for _, iface := range state.Interface {
if iface.Interface != nil && iface.IsUp() {
up++
}
}
return up
}