mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
feat(cli): put the network state of the host in the log journal
The journal records the network state of the host and each change of that state. The support team reads one file to see the interfaces, the routes, the resolvers, and the recoveries around an incident. The same file shows the health of the query path. A change that touches only AirDrop or virtual interfaces runs no DNS work.
This commit is contained in:
1 parent
8934e09428
commit
8ae09ac98e
68 files changed
+10458
-442
No files matched your search
+13
-122
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -13,8 +12,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/Control-D-Inc/ctrld"
|
||||
"tailscale.com/net/netmon"
|
||||
)
|
||||
|
||||
// processStartTime is the time this process started. A reader compares it with
|
||||
@@ -42,21 +41,6 @@ const (
|
||||
// macOS, and a header refresh follows every network change.
|
||||
var headerOSVersion = sync.OnceValue(osVersion)
|
||||
|
||||
// logHeaderInterface is one interface of the header network snapshot.
|
||||
type logHeaderInterface struct {
|
||||
Name string
|
||||
Up bool
|
||||
IPs []string
|
||||
}
|
||||
|
||||
// logHeaderNetwork is the minimal network snapshot of a header line.
|
||||
type logHeaderNetwork struct {
|
||||
DefaultRouteInterface string
|
||||
HaveV4 bool
|
||||
HaveV6 bool
|
||||
Interfaces []logHeaderInterface
|
||||
}
|
||||
|
||||
// logHeaderInput holds every value of a header line. The caller collects the
|
||||
// values, so the renderer stays free of locks and of system calls.
|
||||
type logHeaderInput struct {
|
||||
@@ -72,7 +56,7 @@ type logHeaderInput struct {
|
||||
UpstreamTypes []string
|
||||
ResolverUID string
|
||||
LogFiles []string
|
||||
Network logHeaderNetwork
|
||||
Network networkSnapshot
|
||||
Trigger string
|
||||
}
|
||||
|
||||
@@ -95,7 +79,7 @@ func renderLogHeader(in logHeaderInput) []byte {
|
||||
Strs("upstream_types", in.UpstreamTypes).
|
||||
Str("resolver_uid", in.ResolverUID).
|
||||
Strs("log_files", in.LogFiles).
|
||||
Dict("network", logHeaderNetworkDict(in.Network))
|
||||
Dict("network", snapshotDict(in.Network))
|
||||
if in.Trigger != "" {
|
||||
event = event.Str("trigger", in.Trigger)
|
||||
}
|
||||
@@ -113,86 +97,13 @@ func logHeaderEvent(logger *zerolog.Logger) *zerolog.Event {
|
||||
return logger.Log().Str(zerolog.LevelFieldName, zerolog.LevelInfoValue)
|
||||
}
|
||||
|
||||
func logHeaderNetworkDict(network logHeaderNetwork) *zerolog.Event {
|
||||
interfaces := zerolog.Arr()
|
||||
for _, iface := range network.Interfaces {
|
||||
interfaces = interfaces.Dict(zerolog.Dict().
|
||||
Str("name", iface.Name).
|
||||
Bool("up", iface.Up).
|
||||
Strs("ips", iface.IPs))
|
||||
}
|
||||
return zerolog.Dict().
|
||||
Str("default_route_interface", network.DefaultRouteInterface).
|
||||
Bool("have_v4", network.HaveV4).
|
||||
Bool("have_v6", network.HaveV6).
|
||||
Array("interfaces", interfaces)
|
||||
}
|
||||
|
||||
// logHeaderNetworkFromState makes the header snapshot from a netmon state.
|
||||
func logHeaderNetworkFromState(st *netmon.State) logHeaderNetwork {
|
||||
if st == nil {
|
||||
return logHeaderNetwork{}
|
||||
}
|
||||
network := logHeaderNetwork{
|
||||
DefaultRouteInterface: st.DefaultRouteInterface,
|
||||
HaveV4: st.HaveV4,
|
||||
HaveV6: st.HaveV6,
|
||||
}
|
||||
for name, iface := range st.Interface {
|
||||
network.Interfaces = append(network.Interfaces, logHeaderInterface{
|
||||
Name: name,
|
||||
Up: iface.Interface != nil && iface.IsUp(),
|
||||
IPs: prefixStrings(st.InterfaceIPs[name]),
|
||||
})
|
||||
}
|
||||
slices.SortFunc(network.Interfaces, func(a, b logHeaderInterface) int {
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
return network
|
||||
}
|
||||
|
||||
// logHeaderNetworkFresh reads the network now. A header that no network
|
||||
// callback reached yet, and the header of an upload, need the live picture.
|
||||
func logHeaderNetworkFresh() logHeaderNetwork {
|
||||
state, err := readNetworkSourceStateFn()
|
||||
if err != nil {
|
||||
state = nil
|
||||
}
|
||||
network := logHeaderNetworkFromState(state)
|
||||
if route, err := netmon.DefaultRouteInterface(); err == nil {
|
||||
network.DefaultRouteInterface = route
|
||||
}
|
||||
network.HaveV4, network.HaveV6 = globalAddressFamilies(state)
|
||||
return network
|
||||
}
|
||||
|
||||
// globalAddressFamilies reports which families the host can reach. A loopback
|
||||
// or a link-local address reaches no other host, so it proves nothing.
|
||||
func globalAddressFamilies(st *netmon.State) (haveV4, haveV6 bool) {
|
||||
if st == nil {
|
||||
return false, false
|
||||
}
|
||||
for name, iface := range st.Interface {
|
||||
if iface.Interface == nil || !iface.IsUp() {
|
||||
continue
|
||||
}
|
||||
for _, prefix := range st.InterfaceIPs[name] {
|
||||
addr := prefix.Addr().Unmap()
|
||||
if !addr.IsGlobalUnicast() || addr.IsLoopback() || addr.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
if addr.Is4() {
|
||||
haveV4 = true
|
||||
continue
|
||||
}
|
||||
haveV6 = true
|
||||
}
|
||||
}
|
||||
return haveV4, haveV6
|
||||
}
|
||||
|
||||
// logHeaderInput collects every header value of the running program.
|
||||
func (p *prog) logHeaderInput() logHeaderInput {
|
||||
return p.logHeaderInputWith(p.networkSnapshot())
|
||||
}
|
||||
|
||||
// logHeaderInputWith collects every header value around one network snapshot.
|
||||
func (p *prog) logHeaderInputWith(network networkSnapshot) logHeaderInput {
|
||||
in := logHeaderInput{
|
||||
Version: curVersion(),
|
||||
Commit: commit,
|
||||
@@ -204,7 +115,7 @@ func (p *prog) logHeaderInput() logHeaderInput {
|
||||
// every artifact, so the header keeps its first characters only.
|
||||
ResolverUID: redactToken(cdUID),
|
||||
LogFiles: p.logHeaderFiles(),
|
||||
Network: p.logHeaderNetwork(),
|
||||
Network: network,
|
||||
}
|
||||
p.headerConfigInto(&in)
|
||||
return in
|
||||
@@ -218,35 +129,15 @@ func (p *prog) headerConfigInto(in *logHeaderInput) {
|
||||
logHeaderFromConfig(p.cfg, in)
|
||||
}
|
||||
|
||||
// sendLogHeader renders the header that leads one upload.
|
||||
// sendLogHeader renders the header that leads one upload. The stored state can
|
||||
// be older than the request, and support reads this line to learn the network
|
||||
// of the moment the user asked.
|
||||
func (p *prog) sendLogHeader() []byte {
|
||||
in := p.logHeaderInput()
|
||||
// The stored state can be older than the request, and support reads this
|
||||
// line to learn the network of the moment the user asked.
|
||||
in.Network = logHeaderNetworkFresh()
|
||||
in := p.logHeaderInputWith(buildNetworkSnapshot(p.freshSnapshotInputs()))
|
||||
in.Trigger = logHeaderTriggerSend
|
||||
return renderLogHeader(in)
|
||||
}
|
||||
|
||||
// noteNetworkState keeps the newest network that ctrld saw and puts it in the
|
||||
// header bytes of every open log file. The caller must hold no lock, because
|
||||
// the render reads the config and the network.
|
||||
func (p *prog) noteNetworkState(st *netmon.State) {
|
||||
if st != nil {
|
||||
p.lastNetworkState.Store(st)
|
||||
}
|
||||
p.refreshLogHeader()
|
||||
}
|
||||
|
||||
// logHeaderNetwork takes the network that the last monitor callback reported.
|
||||
func (p *prog) logHeaderNetwork() logHeaderNetwork {
|
||||
state := p.lastNetworkState.Load()
|
||||
if state == nil {
|
||||
return logHeaderNetworkFresh()
|
||||
}
|
||||
return logHeaderNetworkFromState(state)
|
||||
}
|
||||
|
||||
// logHeaderFiles names the files that this run writes, debug first.
|
||||
func (p *prog) logHeaderFiles() []string {
|
||||
files := make([]string, 0, 3)
|
||||
|
||||
Reference in new issue
Block a user