Files
ctrld/cmd/cli/log_header_test.go
T
Anthony Wong 8ae09ac98e 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.
2026-09-28 16:49:05 +07:00

654 lines
22 KiB
Go

package cli
import (
"encoding/json"
"io"
"net"
"net/netip"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/rs/zerolog"
"github.com/Control-D-Inc/ctrld"
"tailscale.com/net/netmon"
)
// logHeaderTestSecret is the string that must never reach a header line.
const logHeaderTestSecret = "secret-token"
// logHeaderTestLine is the parsed form of a header line. The json tags name
// the fields that a log tool reads, so a renamed field fails the test.
type logHeaderTestLine struct {
Level string `json:"level"`
Time string `json:"time"`
Journal bool `json:"journal"`
Message string `json:"message"`
Version string `json:"version"`
Commit string `json:"commit"`
OS string `json:"os"`
Arch string `json:"arch"`
PID int `json:"pid"`
StartTime string `json:"start_time"`
InterceptMode string `json:"intercept_mode"`
Listeners []string `json:"listeners"`
UpstreamCount int `json:"upstream_count"`
UpstreamTypes []string `json:"upstream_types"`
ResolverUID string `json:"resolver_uid"`
LogFiles []string `json:"log_files"`
Network struct {
DefaultRouteV4 string `json:"default_route_v4"`
DefaultRouteV6 string `json:"default_route_v6"`
GatewayV4 string `json:"gateway_v4"`
GatewayV6 string `json:"gateway_v6"`
HaveV4 bool `json:"have_v4"`
HaveV6 bool `json:"have_v6"`
Interfaces []struct {
Name string `json:"name"`
Class string `json:"class"`
Up bool `json:"up"`
IPs []string `json:"ips"`
HardwarePort string `json:"hardware_port"`
Service string `json:"service"`
} `json:"interfaces"`
Resolvers []string `json:"resolvers"`
SourceIPv4 string `json:"source_ipv4"`
SourceIPv6 string `json:"source_ipv6"`
InterceptTarget string `json:"intercept_target"`
LinkType string `json:"link_type"`
Tethered bool `json:"tethered"`
} `json:"network"`
Trigger string `json:"trigger"`
}
func logHeaderTestInput() logHeaderInput {
return logHeaderInput{
Version: "1.5.8",
Commit: "abc1234",
OS: "darwin 15.6",
Arch: "arm64",
PID: 4242,
StartTime: time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC),
InterceptMode: "dns",
Listeners: []string{"127.0.0.1:53", "127.0.0.1:5354"},
UpstreamCount: 2,
UpstreamTypes: []string{"doh", "os"},
ResolverUID: redactToken("abcd1234"),
LogFiles: []string{"/tmp/ctrld.log", "/tmp/ctrld-journal.log"},
Network: networkSnapshot{
DefaultRouteV4: "en0",
GatewayV4: "192.0.2.1",
HaveV4: true,
Interfaces: []snapshotInterface{
{Name: "en0", Class: "hardware", Up: true, IPs: []string{"192.0.2.10/24"}, HardwarePort: "Wi-Fi", Service: "Wi-Fi"},
},
Resolvers: []string{"192.0.2.1:53"},
LinkType: "wifi",
},
}
}
// parseLogHeader checks that the render is one line and returns it twice: as
// values, and as raw fields for the tests that ask whether a field is there.
func parseLogHeader(t *testing.T, line []byte) (logHeaderTestLine, map[string]json.RawMessage) {
t.Helper()
if len(line) == 0 {
t.Fatal("renderLogHeader returned no bytes")
}
if line[len(line)-1] != '\n' {
t.Fatalf("header does not end with a newline: %q", line)
}
if count := strings.Count(string(line), "\n"); count != 1 {
t.Fatalf("header holds %d newlines, want 1: %q", count, line)
}
var parsed logHeaderTestLine
if err := json.Unmarshal(line, &parsed); err != nil {
t.Fatalf("parse header %q: %v", line, err)
}
fields := map[string]json.RawMessage{}
if err := json.Unmarshal(line, &fields); err != nil {
t.Fatalf("parse header fields %q: %v", line, err)
}
return parsed, fields
}
func setLogHeaderTestGlobalLevel(t *testing.T, level zerolog.Level) {
t.Helper()
old := zerolog.GlobalLevel()
zerolog.SetGlobalLevel(level)
t.Cleanup(func() { zerolog.SetGlobalLevel(old) })
}
func Test_renderLogHeaderHoldsEveryField(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
in := logHeaderTestInput()
parsed, fields := parseLogHeader(t, renderLogHeader(in))
checks := []struct {
field string
got any
want any
}{
{"level", parsed.Level, "info"},
{"journal", parsed.Journal, true},
{"message", parsed.Message, "Log header"},
{"version", parsed.Version, in.Version},
{"commit", parsed.Commit, in.Commit},
{"os", parsed.OS, in.OS},
{"arch", parsed.Arch, in.Arch},
{"pid", parsed.PID, in.PID},
{"start_time", parsed.StartTime, in.StartTime.Format(time.RFC3339)},
{"intercept_mode", parsed.InterceptMode, in.InterceptMode},
{"listeners", parsed.Listeners, in.Listeners},
{"upstream_count", parsed.UpstreamCount, in.UpstreamCount},
{"upstream_types", parsed.UpstreamTypes, in.UpstreamTypes},
// The resolver UID is a secret, so the header keeps the first four
// characters only.
{"resolver_uid", parsed.ResolverUID, "abcd***"},
{"log_files", parsed.LogFiles, in.LogFiles},
{"network.default_route_v4", parsed.Network.DefaultRouteV4, in.Network.DefaultRouteV4},
{"network.default_route_v6", parsed.Network.DefaultRouteV6, in.Network.DefaultRouteV6},
{"network.gateway_v4", parsed.Network.GatewayV4, in.Network.GatewayV4},
{"network.have_v4", parsed.Network.HaveV4, in.Network.HaveV4},
{"network.have_v6", parsed.Network.HaveV6, in.Network.HaveV6},
{"network.resolvers", parsed.Network.Resolvers, in.Network.Resolvers},
{"network.link_type", parsed.Network.LinkType, in.Network.LinkType},
{"network.tethered", parsed.Network.Tethered, in.Network.Tethered},
}
for _, c := range checks {
if !reflect.DeepEqual(c.got, c.want) {
t.Errorf("%s = %v, want %v", c.field, c.got, c.want)
}
}
if len(parsed.Network.Interfaces) != 1 {
t.Fatalf("network.interfaces holds %d entries, want 1", len(parsed.Network.Interfaces))
}
iface := parsed.Network.Interfaces[0]
if iface.Name != "en0" || iface.Class != "hardware" || !iface.Up || iface.HardwarePort != "Wi-Fi" {
t.Errorf("network.interfaces[0] = %+v, want the hardware port en0 up on Wi-Fi", iface)
}
if !reflect.DeepEqual(iface.IPs, []string{"192.0.2.10/24"}) {
t.Errorf("network.interfaces[0].ips = %v, want 192.0.2.10/24", iface.IPs)
}
if parsed.Time == "" {
t.Error("time is empty")
}
if _, ok := fields["trigger"]; ok {
t.Errorf("trigger is present for an empty Trigger: %s", fields["trigger"])
}
}
func Test_renderLogHeaderAddsTriggerWhenSet(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
in := logHeaderTestInput()
in.Trigger = "send"
parsed, _ := parseLogHeader(t, renderLogHeader(in))
if parsed.Trigger != "send" {
t.Errorf("trigger = %q, want %q", parsed.Trigger, "send")
}
}
// Test_renderLogHeaderIgnoresGlobalLevel guards the promise that every log file
// starts with a header. The global level sits above info on a default start.
func Test_renderLogHeaderIgnoresGlobalLevel(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.WarnLevel)
parsed, _ := parseLogHeader(t, renderLogHeader(logHeaderTestInput()))
if parsed.Level != "info" {
t.Errorf("level = %q, want %q", parsed.Level, "info")
}
if parsed.Message != "Log header" {
t.Errorf("message = %q, want %q", parsed.Message, "Log header")
}
}
func Test_logHeaderFromConfigKeepsUpstreamSecretsOut(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
old := interceptMode
t.Cleanup(func() { interceptMode = old })
interceptMode = ""
cfg := &ctrld.Config{
Listener: map[string]*ctrld.ListenerConfig{
"0": {IP: "127.0.0.1", Port: 53},
logHeaderTestSecret + "-l": {IP: "10.0.0.1", Port: 5354},
},
Upstream: map[string]*ctrld.UpstreamConfig{
"0": {
Name: logHeaderTestSecret + "-name",
Type: "doh",
Endpoint: "https://dns.example/" + logHeaderTestSecret,
BootstrapIP: "192.0.2.53",
},
"1": {Endpoint: "https://dns.example/" + logHeaderTestSecret},
},
}
cfg.Service.InterceptMode = "dns"
in := logHeaderTestInput()
logHeaderFromConfig(cfg, &in)
if in.InterceptMode != "dns" {
t.Errorf("intercept mode = %q, want %q", in.InterceptMode, "dns")
}
if want := []string{"10.0.0.1:5354", "127.0.0.1:53"}; !reflect.DeepEqual(in.Listeners, want) {
t.Errorf("listeners = %v, want %v", in.Listeners, want)
}
if in.UpstreamCount != 2 {
t.Errorf("upstream count = %d, want 2", in.UpstreamCount)
}
if want := []string{"doh", "unspecified"}; !reflect.DeepEqual(in.UpstreamTypes, want) {
t.Errorf("upstream types = %v, want %v", in.UpstreamTypes, want)
}
line := renderLogHeader(in)
if strings.Contains(string(line), logHeaderTestSecret) {
t.Errorf("header holds %q: %s", logHeaderTestSecret, line)
}
}
func Test_logHeaderFromConfigReportsOffWhenUnset(t *testing.T) {
old := interceptMode
t.Cleanup(func() { interceptMode = old })
interceptMode = ""
var in logHeaderInput
logHeaderFromConfig(&ctrld.Config{}, &in)
if in.InterceptMode != "off" {
t.Errorf("intercept mode = %q, want %q", in.InterceptMode, "off")
}
}
// Test_renderLogHeaderShowsTheSnapshotOfAState renders the header of one
// network state, so a reader of the file sees every interface of the host in
// one order.
func Test_renderLogHeaderShowsTheSnapshotOfAState(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
stubHeaderSnapshotSources(t)
state := &netmon.State{
DefaultRouteInterface: "en0",
HaveV4: true,
Interface: map[string]netmon.Interface{
"en0": {Interface: &net.Interface{Name: "en0", Flags: net.FlagUp}},
"awdl0": {Interface: &net.Interface{Name: "awdl0"}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {netip.MustParsePrefix("192.0.2.10/24"), netip.MustParsePrefix("2001:db8::1/64")},
"awdl0": {netip.MustParsePrefix("fe80::2/64")},
},
}
in := logHeaderTestInput()
in.Network = buildNetworkSnapshot(snapshotInputs{
State: state,
RouteV4: defaultRoute{Gateway: "192.0.2.1", Interface: "en0"},
RouteV6: defaultRoute{Gateway: "fe80::1%en0", Interface: "en0"},
Meta: interfaceMetaFor,
})
parsed, _ := parseLogHeader(t, renderLogHeader(in))
if parsed.Network.DefaultRouteV4 != "en0" || parsed.Network.DefaultRouteV6 != "en0" {
t.Errorf("default route = %q, %q, want en0 for both families", parsed.Network.DefaultRouteV4, parsed.Network.DefaultRouteV6)
}
if len(parsed.Network.Interfaces) != 2 {
t.Fatalf("network.interfaces holds %d entries, want 2", len(parsed.Network.Interfaces))
}
if parsed.Network.Interfaces[0].Name != "awdl0" || parsed.Network.Interfaces[0].Class != "airdrop" || parsed.Network.Interfaces[0].Up {
t.Errorf("network.interfaces[0] = %+v, want awdl0 down in the airdrop class", parsed.Network.Interfaces[0])
}
if got := parsed.Network.Interfaces[1].IPs; !reflect.DeepEqual(got, []string{"192.0.2.10/24", "2001:db8::1/64"}) {
t.Errorf("network.interfaces[1].ips = %v", got)
}
}
// setupLogHeaderWiringTest puts ctrld in cd mode with a temporary home
// directory, so initInternalLogging writes its files there. Every global that
// the logging setup touches goes back when the test ends.
func setupLogHeaderWiringTest(t *testing.T) string {
t.Helper()
stubHeaderSnapshotSources(t)
origSilent, origCdUID, origHomedir, origVerbose := silent, cdUID, homedir, verbose
origMainLog, origProxyLogger := mainLog.Load(), ctrld.ProxyLogger.Load()
origLevel := zerolog.GlobalLevel()
origLogPathFile := logPathFile.Load()
t.Cleanup(func() {
silent, cdUID, homedir, verbose = origSilent, origCdUID, origHomedir, origVerbose
mainLog.Store(origMainLog)
ctrld.ProxyLogger.Store(origProxyLogger)
zerolog.SetGlobalLevel(origLevel)
logPathFile.Store(origLogPathFile)
})
homedir = t.TempDir()
cdUID = "header-wiring-uid"
silent, verbose = false, 0
discard := zerolog.New(io.Discard)
mainLog.Store(&discard)
logPathFile.Store(nil)
return homedir
}
// startLogHeaderWiringProg runs the internal logging setup and closes both
// files when the test ends.
func startLogHeaderWiringProg(t *testing.T) *prog {
t.Helper()
p := &prog{cfg: &ctrld.Config{}}
p.initInternalLogging(nil)
t.Cleanup(func() {
p.internalLogWriter.closeLogFile()
p.internalJournalWriter.closeLogFile()
})
return p
}
// logHeaderFileLines parses every line of a log file. It returns line 1 and
// the number of header lines, so a test can tell a missing header from a
// repeated one.
func logHeaderFileLines(t *testing.T, path string) (logHeaderTestLine, int) {
t.Helper()
content := strings.TrimRight(rotatingFileTestContent(t, path), "\n")
if content == "" {
t.Fatalf("%s is empty", path)
}
var firstLine logHeaderTestLine
headers := 0
for i, line := range strings.Split(content, "\n") {
var parsed logHeaderTestLine
if err := json.Unmarshal([]byte(line), &parsed); err != nil {
t.Fatalf("parse line %d of %s (%q): %v", i+1, path, line, err)
}
if i == 0 {
firstLine = parsed
}
if parsed.Message == logHeaderMessage {
headers++
}
}
return firstLine, headers
}
func Test_logHeaderWiring_writesHeaderOnStart(t *testing.T) {
dir := setupLogHeaderWiringTest(t)
startLogHeaderWiringProg(t)
for _, name := range []string{logFileName, journalLogFileName} {
path := filepath.Join(dir, name)
firstLine, headers := logHeaderFileLines(t, path)
if firstLine.Message != logHeaderMessage {
t.Errorf("line 1 of %s = %q, want %q", path, firstLine.Message, logHeaderMessage)
}
if !firstLine.Journal {
t.Errorf("line 1 of %s carries no journal marker", path)
}
if want := redactToken(cdUID); firstLine.ResolverUID != want {
t.Errorf("resolver_uid in %s = %q, want %q", path, firstLine.ResolverUID, want)
}
if headers != 1 {
t.Errorf("%s holds %d header lines, want 1", path, headers)
}
}
}
func Test_logHeaderInput_keepsSecretsOut(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
dir := setupLogHeaderWiringTest(t)
origInterceptMode := interceptMode
t.Cleanup(func() { interceptMode = origInterceptMode })
interceptMode = ""
p := &prog{cfg: &ctrld.Config{
Listener: map[string]*ctrld.ListenerConfig{
logHeaderTestSecret + "-listener": {IP: "127.0.0.1", Port: 53},
},
Upstream: map[string]*ctrld.UpstreamConfig{
"0": {
Name: logHeaderTestSecret + "-name",
Type: "doh",
Endpoint: "https://dns.example/" + logHeaderTestSecret,
BootstrapIP: "192.0.2.53",
Domain: logHeaderTestSecret + ".example",
},
},
}}
p.initInternalLogging(nil)
t.Cleanup(func() {
p.internalLogWriter.closeLogFile()
p.internalJournalWriter.closeLogFile()
})
if line := renderLogHeader(p.logHeaderInput()); strings.Contains(string(line), logHeaderTestSecret) {
t.Errorf("the header holds %q: %s", logHeaderTestSecret, line)
}
for _, name := range []string{logFileName, journalLogFileName} {
path := filepath.Join(dir, name)
if content := rotatingFileTestContent(t, path); strings.Contains(content, logHeaderTestSecret) {
t.Errorf("%s holds %q: %s", path, logHeaderTestSecret, content)
}
}
}
func Test_logHeaderWiring_secondInitAddsNoHeader(t *testing.T) {
dir := setupLogHeaderWiringTest(t)
p := startLogHeaderWiringProg(t)
p.initInternalLogging(nil)
for _, name := range []string{logFileName, journalLogFileName} {
path := filepath.Join(dir, name)
if _, headers := logHeaderFileLines(t, path); headers != 1 {
t.Errorf("%s holds %d header lines after a second start, want 1", path, headers)
}
}
}
func Test_logHeaderWiring_headerLeadsRotatedFile(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
captureMainLog(t)
lw, path := newTestFileLogWriter(t, testLogWriterBudget)
header := string(renderLogHeader(logHeaderInput{Version: "v1"}))
rf := lw.rotating()
rf.setHeader([]byte(header))
if err := rf.writeHeader(); err != nil {
t.Fatalf("writeHeader: %v", err)
}
linesPerFile := int(testLogWriterBudget.maxSize) / rotatingFileTestLineSize
for seq := 1; seq <= linesPerFile+1; seq++ {
if _, err := lw.Write(rotatingFileTestLine(1, seq)); err != nil {
t.Fatalf("write line %d: %v", seq, err)
}
}
for _, name := range []string{path + ".1", path} {
if content := rotatingFileTestContent(t, name); !strings.HasPrefix(content, header) {
t.Errorf("%s starts with %q, want the header", name, strings.SplitN(content, "\n", 2)[0])
}
}
}
func Test_logHeaderWiring_restartAppendsHeader(t *testing.T) {
dir := setupLogHeaderWiringTest(t)
path := filepath.Join(dir, logFileName)
const seed = `{"level":"info","message":"line of the run before"}`
if err := os.WriteFile(path, []byte(seed+"\n"), 0o600); err != nil {
t.Fatalf("seed %s: %v", path, err)
}
startLogHeaderWiringProg(t)
lines := strings.Split(strings.TrimRight(rotatingFileTestContent(t, path), "\n"), "\n")
if len(lines) < 2 {
t.Fatalf("%s holds %d lines, want the seed line and the header", path, len(lines))
}
if lines[0] != seed {
t.Errorf("line 1 of %s = %q, want the seed line", path, lines[0])
}
var header logHeaderTestLine
if err := json.Unmarshal([]byte(lines[1]), &header); err != nil {
t.Fatalf("parse line 2 of %s (%q): %v", path, lines[1], err)
}
if header.Message != logHeaderMessage {
t.Errorf("line 2 of %s = %q, want %q", path, header.Message, logHeaderMessage)
}
if _, headers := logHeaderFileLines(t, path); headers != 1 {
t.Errorf("%s holds %d header lines, want 1", path, headers)
}
}
func Test_logHeaderWiring_usesLastNetworkState(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
stubHeaderSnapshotSources(t)
origRead := readNetworkSourceStateFn
t.Cleanup(func() { readNetworkSourceStateFn = origRead })
p := &prog{cfg: &ctrld.Config{}}
p.lastNetworkState.Store(&netmon.State{
DefaultRouteInterface: "en7",
Interface: map[string]netmon.Interface{"en7": {Interface: &net.Interface{Name: "en7", Flags: net.FlagUp}}},
InterfaceIPs: map[string][]netip.Prefix{"en7": {netip.MustParsePrefix("192.0.2.7/24")}},
})
if got := p.logHeaderInput().Network.DefaultRouteV4; got != "en7" {
t.Errorf("default_route_v4 = %q, want %q", got, "en7")
}
if got := p.logHeaderInput().Network.GatewayV4; got != "192.0.2.1" {
t.Errorf("gateway_v4 = %q, want %q", got, "192.0.2.1")
}
p.lastNetworkState.Store(nil)
readNetworkSourceStateFn = func() (*netmon.State, error) {
return &netmon.State{
Interface: map[string]netmon.Interface{"en9": {Interface: &net.Interface{Name: "en9", Flags: net.FlagUp}}},
InterfaceIPs: map[string][]netip.Prefix{"en9": {netip.MustParsePrefix("192.0.2.9/24")}},
}, nil
}
network := p.logHeaderInput().Network
if len(network.Interfaces) != 1 || network.Interfaces[0].Name != "en9" || !network.Interfaces[0].Up {
t.Fatalf("interfaces = %+v, want one up en9", network.Interfaces)
}
if !network.HaveV4 {
t.Error("have_v4 = false, want true for a global unicast v4 address")
}
if network.HaveV6 {
t.Error("have_v6 = true, want false without a v6 address")
}
}
// stubHeaderSnapshotSources keeps the header snapshot away from the host. One
// render reads the hardware ports, the route table, and the NAT64 class, and no
// test may run the commands behind them.
func stubHeaderSnapshotSources(t *testing.T) {
t.Helper()
stubSnapshotDNS64(t)
stubSnapshotGateways(t, "192.0.2.1", "")
stubSnapshotPlatformMeta(t, nil)
stubSnapshotVirtualSet(t)
}
// stubLogHeaderNetworkRead makes the fresh network read return one up
// interface, so a header test never depends on the host network.
func stubLogHeaderNetworkRead(t *testing.T, name string) {
t.Helper()
stubHeaderSnapshotSources(t)
origRead := readNetworkSourceStateFn
t.Cleanup(func() { readNetworkSourceStateFn = origRead })
readNetworkSourceStateFn = func() (*netmon.State, error) {
return &netmon.State{
Interface: map[string]netmon.Interface{name: {Interface: &net.Interface{Name: name, Flags: net.FlagUp}}},
InterfaceIPs: map[string][]netip.Prefix{name: {netip.MustParsePrefix("192.0.2.9/24")}},
}, nil
}
}
func Test_sendLogHeader_usesFreshNetworkAndTrigger(t *testing.T) {
setLogHeaderTestGlobalLevel(t, zerolog.DebugLevel)
stubHeaderSnapshotSources(t)
stubLogHeaderNetworkRead(t, "en9")
p := &prog{cfg: &ctrld.Config{}}
p.lastNetworkState.Store(&netmon.State{
DefaultRouteInterface: "en7",
Interface: map[string]netmon.Interface{"en7": {Interface: &net.Interface{Name: "en7", Flags: net.FlagUp}}},
InterfaceIPs: map[string][]netip.Prefix{"en7": {netip.MustParsePrefix("192.0.2.7/24")}},
})
parsed, _ := parseLogHeader(t, p.sendLogHeader())
if parsed.Trigger != "send" {
t.Errorf("trigger = %q, want %q", parsed.Trigger, "send")
}
if parsed.Message != logHeaderMessage {
t.Errorf("message = %q, want %q", parsed.Message, logHeaderMessage)
}
names := make([]string, 0, len(parsed.Network.Interfaces))
for _, iface := range parsed.Network.Interfaces {
names = append(names, iface.Name)
}
if want := []string{"en9"}; !reflect.DeepEqual(names, want) {
t.Fatalf("network.interfaces = %v, want %v from the fresh read", names, want)
}
}
// Test_logHeaderInputReadsTheConfigUnderTheLock renders headers beside the
// config swap of a reload. The race detector fails the test when the render
// reads the config outside the lock.
func Test_logHeaderInputReadsTheConfigUnderTheLock(t *testing.T) {
stubLogHeaderNetworkRead(t, "en0")
p := &prog{cfg: &ctrld.Config{
Listener: map[string]*ctrld.ListenerConfig{"0": {IP: "127.0.0.1", Port: 53}},
Upstream: map[string]*ctrld.UpstreamConfig{"0": {Type: "doh"}},
}}
reloaded := &ctrld.Config{
Listener: map[string]*ctrld.ListenerConfig{"0": {IP: "127.0.0.1", Port: 5354}},
Upstream: map[string]*ctrld.UpstreamConfig{"0": {Type: "doq"}, "1": {Type: "legacy"}},
}
stop := make(chan struct{})
done := make(chan struct{})
go func() {
defer close(done)
for {
select {
case <-stop:
return
default:
}
p.mu.Lock()
*p.cfg = *reloaded
p.mu.Unlock()
}
}()
for i := 0; i < 300; i++ {
if in := p.logHeaderInput(); len(in.Listeners) != 1 {
t.Fatalf("listeners = %v, want one", in.Listeners)
}
}
close(stop)
<-done
}
// Test_renderLogHeaderPassesTheSink puts a provisioning secret where the
// header does not expect one. The header is the first line of every file and
// of every upload, so it must pass the same redaction as every other line.
func Test_renderLogHeaderPassesTheSink(t *testing.T) {
origUID := cdUID
cdUID = "abcd1234SECRETUID"
t.Cleanup(func() { cdUID = origUID })
line := string(renderLogHeader(logHeaderInput{Version: "1.0", LogFiles: []string{"/var/log/" + cdUID + "/ctrld.log"}}))
if strings.Contains(line, cdUID) {
t.Fatalf("the header holds the secret: %s", line)
}
}