mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
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.
231 lines
6.9 KiB
Go
231 lines
6.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
// processStartTime is the time this process started. A reader compares it with
|
|
// the time of the header line to tell a restart from a rotation.
|
|
var processStartTime = time.Now()
|
|
|
|
const (
|
|
// logHeaderMessage is the message of the header line. Log tools select the
|
|
// header by this message.
|
|
logHeaderMessage = "Log header"
|
|
|
|
// interceptModeOff is the mode word for a ctrld that does not intercept.
|
|
interceptModeOff = "off"
|
|
|
|
// logHeaderTriggerSend marks the header that a log request renders. It
|
|
// tells a reader that the line came from an upload, not from a file open.
|
|
logHeaderTriggerSend = "send"
|
|
|
|
// upstreamTypeUnspecified names an upstream that has no type in the config.
|
|
// ctrld resolves the type at start, so the header must not claim one.
|
|
upstreamTypeUnspecified = "unspecified"
|
|
)
|
|
|
|
// headerOSVersion reads the OS version once. The read starts a subprocess on
|
|
// macOS, and a header refresh follows every network change.
|
|
var headerOSVersion = sync.OnceValue(osVersion)
|
|
|
|
// 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 {
|
|
Version string
|
|
Commit string
|
|
OS string
|
|
Arch string
|
|
PID int
|
|
StartTime time.Time
|
|
InterceptMode string
|
|
Listeners []string
|
|
UpstreamCount int
|
|
UpstreamTypes []string
|
|
ResolverUID string
|
|
LogFiles []string
|
|
Network networkSnapshot
|
|
Trigger string
|
|
}
|
|
|
|
// renderLogHeader renders the header as one log line. A file writer stores the
|
|
// bytes and writes them itself, so the header reaches a file that the logger
|
|
// never fills.
|
|
func renderLogHeader(in logHeaderInput) []byte {
|
|
var buf bytes.Buffer
|
|
logger := zerolog.New(&buf).With().Timestamp().Logger()
|
|
event := journal(logHeaderEvent(&logger)).
|
|
Str("version", in.Version).
|
|
Str("commit", in.Commit).
|
|
Str("os", in.OS).
|
|
Str("arch", in.Arch).
|
|
Int("pid", in.PID).
|
|
Str("start_time", in.StartTime.Format(time.RFC3339)).
|
|
Str("intercept_mode", in.InterceptMode).
|
|
Strs("listeners", in.Listeners).
|
|
Int("upstream_count", in.UpstreamCount).
|
|
Strs("upstream_types", in.UpstreamTypes).
|
|
Str("resolver_uid", in.ResolverUID).
|
|
Strs("log_files", in.LogFiles).
|
|
Dict("network", snapshotDict(in.Network))
|
|
if in.Trigger != "" {
|
|
event = event.Str("trigger", in.Trigger)
|
|
}
|
|
event.Msg(logHeaderMessage)
|
|
|
|
// The header is the first line of every file and of every upload, and it
|
|
// bypasses the writers, so it passes the journal redaction here.
|
|
return redactRetainedLine(buf.Bytes())
|
|
}
|
|
|
|
// logHeaderEvent starts the header event at info level. The global level sits
|
|
// above info on a default start, and a log file must hold its header at every
|
|
// level, so the event names its level itself.
|
|
func logHeaderEvent(logger *zerolog.Logger) *zerolog.Event {
|
|
return logger.Log().Str(zerolog.LevelFieldName, zerolog.LevelInfoValue)
|
|
}
|
|
|
|
// 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,
|
|
OS: headerOSVersion(),
|
|
Arch: runtime.GOARCH,
|
|
PID: os.Getpid(),
|
|
StartTime: processStartTime,
|
|
// The resolver UID is a secret that the provisioning code strips from
|
|
// every artifact, so the header keeps its first characters only.
|
|
ResolverUID: redactToken(cdUID),
|
|
LogFiles: p.logHeaderFiles(),
|
|
Network: network,
|
|
}
|
|
p.headerConfigInto(&in)
|
|
return in
|
|
}
|
|
|
|
// headerConfigInto reads the config under p.mu, because a reload replaces the
|
|
// config under that lock while log view and log send render a header.
|
|
func (p *prog) headerConfigInto(in *logHeaderInput) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
logHeaderFromConfig(p.cfg, in)
|
|
}
|
|
|
|
// 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.logHeaderInputWith(buildNetworkSnapshot(p.freshSnapshotInputs()))
|
|
in.Trigger = logHeaderTriggerSend
|
|
return renderLogHeader(in)
|
|
}
|
|
|
|
// logHeaderFiles names the files that this run writes, debug first.
|
|
func (p *prog) logHeaderFiles() []string {
|
|
files := make([]string, 0, 3)
|
|
for _, rf := range p.openLogFiles() {
|
|
files = append(files, rf.currentPath())
|
|
}
|
|
return files
|
|
}
|
|
|
|
// refreshLogHeader renders the header and hands the bytes to every open file.
|
|
// The caller must hold no writer lock, because the render reads the config
|
|
// and the network.
|
|
func (p *prog) refreshLogHeader() {
|
|
header := renderLogHeader(p.logHeaderInput())
|
|
for _, rf := range p.openLogFiles() {
|
|
rf.setHeader(header)
|
|
}
|
|
}
|
|
|
|
// writeLogHeaders puts the header at the append point of each open file. A
|
|
// header that cannot land leaves the file usable, so the error only earns a
|
|
// warning.
|
|
func (p *prog) writeLogHeaders() {
|
|
for _, rf := range p.openLogFiles() {
|
|
if err := rf.writeHeader(); err != nil {
|
|
mainLog.Load().Warn().Err(err).Msg("could not write log header")
|
|
}
|
|
}
|
|
}
|
|
|
|
func prefixStrings(prefixes []netip.Prefix) []string {
|
|
ips := make([]string, 0, len(prefixes))
|
|
for _, prefix := range prefixes {
|
|
ips = append(ips, prefix.String())
|
|
}
|
|
return ips
|
|
}
|
|
|
|
// logHeaderFromConfig fills the header fields that the config holds. It keeps
|
|
// every upstream endpoint, name, and bootstrap IP out of them, because a token
|
|
// can hide in each of them.
|
|
func logHeaderFromConfig(cfg *ctrld.Config, in *logHeaderInput) {
|
|
if cfg == nil {
|
|
in.InterceptMode = interceptModeOff
|
|
return
|
|
}
|
|
in.InterceptMode = headerInterceptMode(cfg)
|
|
in.Listeners = listenerAddresses(cfg)
|
|
in.UpstreamCount = len(cfg.Upstream)
|
|
in.UpstreamTypes = upstreamTypeWords(cfg)
|
|
}
|
|
|
|
// headerInterceptMode names the mode for the header. An unset mode means that
|
|
// ctrld does not intercept, which reads as off.
|
|
func headerInterceptMode(cfg *ctrld.Config) string {
|
|
if mode := listenerInterceptMode(cfg); mode != "" {
|
|
return mode
|
|
}
|
|
return interceptModeOff
|
|
}
|
|
|
|
func listenerAddresses(cfg *ctrld.Config) []string {
|
|
addresses := make([]string, 0, len(cfg.Listener))
|
|
for _, listener := range cfg.Listener {
|
|
if listener == nil {
|
|
continue
|
|
}
|
|
addresses = append(addresses, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
|
|
}
|
|
slices.Sort(addresses)
|
|
return addresses
|
|
}
|
|
|
|
func upstreamTypeWords(cfg *ctrld.Config) []string {
|
|
words := make([]string, 0, len(cfg.Upstream))
|
|
for _, upstream := range cfg.Upstream {
|
|
if upstream == nil {
|
|
continue
|
|
}
|
|
word := upstream.Type
|
|
if word == "" {
|
|
word = upstreamTypeUnspecified
|
|
}
|
|
if !slices.Contains(words, word) {
|
|
words = append(words, word)
|
|
}
|
|
}
|
|
slices.Sort(words)
|
|
return words
|
|
}
|