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:
Anthony Wong authored and Cuong Manh Le committed 2026-09-28 16:49:05 +07:00
1 parent 8934e09428
commit 8ae09ac98e
68 files changed
+10458 -442

No files matched your search

+13 -122
View File
@@ -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)