mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
227 lines
7.4 KiB
Go
227 lines
7.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"net"
|
|
"reflect"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
"tailscale.com/net/netmon"
|
|
)
|
|
|
|
// networkSnapshotMessage is the message of the snapshot event. A log tool
|
|
// selects every snapshot of a run by this message.
|
|
const networkSnapshotMessage = "Network snapshot"
|
|
|
|
// snapshotTriggerStart names the snapshot of the start of a run.
|
|
const snapshotTriggerStart = "start"
|
|
|
|
// snapshotMinInterval bounds the snapshots of a flapping network.
|
|
const snapshotMinInterval = 60 * time.Second
|
|
|
|
// snapshotNowFn is the clock of the snapshot limit. A test replaces it.
|
|
var snapshotNowFn = time.Now
|
|
|
|
// defaultRoutesFn reads the default route of each family. A test replaces it,
|
|
// because the route table of the host is not a fixture.
|
|
var defaultRoutesFn = defaultRoutes
|
|
|
|
// platformInterfaceMetaFn reads the platform names of one interface. A test
|
|
// replaces it, because the ports of the host are not a fixture.
|
|
var platformInterfaceMetaFn = platformInterfaceMeta
|
|
|
|
// The delta diff has to know the virtual adapters of the host, and only the
|
|
// platform file reads them.
|
|
func init() {
|
|
virtualInterfaceSetFn = platformVirtualInterfaces
|
|
}
|
|
|
|
// interfaceMetaFor describes one interface for the snapshot and for the delta
|
|
// diff.
|
|
func interfaceMetaFor(name string) interfaceMeta {
|
|
platformClass, hardwarePort, service := platformInterfaceMetaFn(name)
|
|
meta := interfaceMeta{
|
|
Class: interfaceClassFor(name, platformClass),
|
|
HardwarePort: hardwarePort,
|
|
Service: service,
|
|
}
|
|
meta.LinkType = linkTypeFor(hardwarePort, meta.Class)
|
|
return meta
|
|
}
|
|
|
|
// interfaceClassFor picks the class of one interface. A name rule wins for the
|
|
// loopback, the AirDrop, and the tunnel interfaces, because Windows describes
|
|
// a tunnel adapter as an Ethernet adapter.
|
|
func interfaceClassFor(name, platformClass string) string {
|
|
if class := interfaceClass(name, false); class != "hardware" {
|
|
return class
|
|
}
|
|
if platformClass != "" {
|
|
return platformClass
|
|
}
|
|
if _, virtual := virtualInterfaceSetFn()[name]; virtual {
|
|
return "virtual"
|
|
}
|
|
return "hardware"
|
|
}
|
|
|
|
// snapshotInputs reads the sources of one snapshot from the network that the
|
|
// last monitor callback reported. No callback ran before the first header of a
|
|
// run, so that header reads the network itself.
|
|
func (p *prog) snapshotInputs() snapshotInputs {
|
|
state := p.lastNetworkState.Load()
|
|
if state == nil {
|
|
state = freshNetworkState()
|
|
}
|
|
return p.snapshotInputsFor(state)
|
|
}
|
|
|
|
// freshSnapshotInputs reads the sources of one snapshot from the network of
|
|
// this moment. Support reads the header of an upload to learn the network of
|
|
// the moment the user asked, which can differ from the last callback.
|
|
func (p *prog) freshSnapshotInputs() snapshotInputs {
|
|
return p.snapshotInputsFor(freshNetworkState())
|
|
}
|
|
|
|
// networkSnapshot describes the host network of this moment.
|
|
func (p *prog) networkSnapshot() networkSnapshot {
|
|
return buildNetworkSnapshot(p.snapshotInputs())
|
|
}
|
|
|
|
// logNetworkSnapshot puts the host network in the journal. Support reads the
|
|
// snapshot of each trigger to learn the network at the time of an outage.
|
|
func (p *prog) logNetworkSnapshot(trigger string) {
|
|
snapshot := p.networkSnapshot()
|
|
if !p.snapshotWrites.allow(trigger, snapshot, snapshotNowFn()) {
|
|
return
|
|
}
|
|
journal(mainLog.Load().Info()).
|
|
Str("trigger", trigger).
|
|
Dict("network", snapshotDict(snapshot)).
|
|
Msg(networkSnapshotMessage)
|
|
}
|
|
|
|
// snapshotLimiter holds the snapshot that reached the journal last, so a burst
|
|
// of triggers writes one line.
|
|
type snapshotLimiter struct {
|
|
mu sync.Mutex
|
|
last networkSnapshot
|
|
written time.Time
|
|
seen bool
|
|
}
|
|
|
|
// allow reports whether one snapshot reaches the journal, and records the
|
|
// snapshots that do.
|
|
func (s *snapshotLimiter) allow(trigger string, snapshot networkSnapshot, now time.Time) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
if !s.permits(trigger, snapshot, now) {
|
|
return false
|
|
}
|
|
s.last, s.written, s.seen = snapshot, now, true
|
|
return true
|
|
}
|
|
|
|
// permits decides one snapshot. The first snapshot of a run tells a reader
|
|
// where the run started. A snapshot equal to the one before it carries
|
|
// nothing. A new default route and a new resolver set explain an outage, so
|
|
// they pass at once. Every other snapshot waits out the interval, because a
|
|
// flapping interface writes some kilobytes per flap.
|
|
func (s *snapshotLimiter) permits(trigger string, snapshot networkSnapshot, now time.Time) bool {
|
|
if trigger == snapshotTriggerStart || !s.seen {
|
|
return true
|
|
}
|
|
if sameNetworkSnapshot(s.last, snapshot) {
|
|
return false
|
|
}
|
|
if routeOrResolversChanged(s.last, snapshot) {
|
|
return true
|
|
}
|
|
return now.Sub(s.written) >= snapshotMinInterval
|
|
}
|
|
|
|
// sameNetworkSnapshot compares two snapshots field by field. The trigger is no
|
|
// part of a snapshot, so two triggers of one network give one line.
|
|
func sameNetworkSnapshot(before, after networkSnapshot) bool {
|
|
return reflect.DeepEqual(before, after)
|
|
}
|
|
|
|
// routeOrResolversChanged names the changes that a reader of an outage needs
|
|
// at once.
|
|
func routeOrResolversChanged(before, after networkSnapshot) bool {
|
|
return before.DefaultRouteV4 != after.DefaultRouteV4 ||
|
|
before.DefaultRouteV6 != after.DefaultRouteV6 ||
|
|
!slices.Equal(before.Resolvers, after.Resolvers)
|
|
}
|
|
|
|
// snapshotInputsFor reads every source of one snapshot. It takes no writer
|
|
// lock, so any path of the daemon can log a snapshot.
|
|
func (p *prog) snapshotInputsFor(state *netmon.State) snapshotInputs {
|
|
routeV4, routeV6 := defaultRoutesFn()
|
|
inputs := snapshotInputs{
|
|
State: state,
|
|
RouteV4: routeV4,
|
|
RouteV6: routeV6,
|
|
Resolvers: ctrld.OsResolverNameservers(),
|
|
SourceIPv4: addressString(ctrld.GetDefaultLocalIPv4()),
|
|
SourceIPv6: addressString(ctrld.GetDefaultLocalIPv6()),
|
|
InterceptTarget: p.interceptTargetSnapshot(),
|
|
BypassActive: p.recoveryBypass.Load(),
|
|
RecoveryRunning: p.recoveryRunning.Load(),
|
|
PFStabilizing: p.pfStabilizing.Load(),
|
|
Meta: interfaceMetaFor,
|
|
}
|
|
if prefix, ok := p.dns64PrefixSnapshot(); ok {
|
|
inputs.NAT64Prefix = prefix.String()
|
|
}
|
|
if state != nil {
|
|
inputs.CLATPresent = dns64StateHasCLAT(state)
|
|
}
|
|
return inputs
|
|
}
|
|
|
|
// freshNetworkState reads the interfaces of the host now. netmon fills the
|
|
// route and the families of a callback state, so a read of this package has to
|
|
// fill them itself.
|
|
func freshNetworkState() *netmon.State {
|
|
state, err := readNetworkSourceStateFn()
|
|
if err != nil || state == nil {
|
|
return nil
|
|
}
|
|
fresh := *state
|
|
if route, err := netmon.DefaultRouteInterface(); err == nil {
|
|
fresh.DefaultRouteInterface = route
|
|
}
|
|
fresh.HaveV4, fresh.HaveV6 = reachableAddressFamilies(&fresh)
|
|
return &fresh
|
|
}
|
|
|
|
// reachableAddressFamilies reports which families the host can reach. It
|
|
// grades each address with the rule of the network monitor, so the first
|
|
// header of a run reports the same families as the first callback.
|
|
func reachableAddressFamilies(state *netmon.State) (haveV4, haveV6 bool) {
|
|
for name, iface := range state.Interface {
|
|
if iface.Interface == nil || !iface.IsUp() {
|
|
continue
|
|
}
|
|
for _, prefix := range state.InterfaceIPs[name] {
|
|
address := prefix.Addr().Unmap()
|
|
haveV4 = haveV4 || usableV4(address)
|
|
haveV6 = haveV6 || usableV6(address)
|
|
}
|
|
}
|
|
return haveV4, haveV6
|
|
}
|
|
|
|
// addressString names one source address. An address that ctrld does not hold
|
|
// leaves an empty field, because net.IP prints an empty value as the word for
|
|
// a nil address.
|
|
func addressString(ip net.IP) string {
|
|
if len(ip) == 0 {
|
|
return ""
|
|
}
|
|
return ip.String()
|
|
}
|