Files
ctrld/cmd/cli/pf_state_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

138 lines
4.2 KiB
Go

package cli
import (
"slices"
"strings"
"testing"
)
// pfStatusEnabled is "pfctl -si" output from a Mac with pf enabled.
const pfStatusEnabled = `Status: Enabled for 0 days 02:11:05 Debug: Urgent
`
// Captured from macOS 26.6 with ctrld installed. "pfctl -sr" prints the scrub and
// filter anchors, "pfctl -sn" prints the translation anchors, and both carry the
// ALTQ warnings because every reader here uses CombinedOutput.
const pfShowRules = altqNoise + `scrub-anchor "com.apple/*" all fragment reassemble
anchor "com.apple/*" all
anchor "com.controld.ctrld" all
pass in quick on lo0 reply-to lo0 inet proto udp from any to 127.0.0.1 port = 5354
`
const pfShowNAT = altqNoise + `nat-anchor "com.apple/*" all
rdr-anchor "com.apple/*" all
rdr-anchor "com.controld.ctrld" all
`
func Test_parsePFAnchorNames(t *testing.T) {
tests := []struct {
name string
rules string
nat string
want []string
}{
{
name: "a real ruleset with ctrld installed",
rules: pfShowRules,
nat: pfShowNAT,
want: []string{"com.apple/*", "com.controld.ctrld"},
},
{
// The names arrive out of order, so the result proves the sort.
name: "a VPN adds its own anchors",
rules: altqNoise + `anchor "windscribe" all
anchor "com.apple/*" all
`,
nat: altqNoise + `dummynet-anchor "com.apple/*" all
rdr-anchor "windscribe" all
`,
want: []string{"com.apple/*", "windscribe"},
},
{
name: "anchors in the translation output only",
rules: altqNoise,
nat: altqNoise + "rdr-anchor \"com.controld.ctrld\" all\n",
want: []string{"com.controld.ctrld"},
},
{
name: "a ruleset with no anchor",
rules: altqNoise + "block drop in all\n",
nat: altqNoise + "(no rules)\n",
want: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := parsePFAnchorNames(strings.NewReader(tc.rules), strings.NewReader(tc.nat))
if !slices.Equal(got, tc.want) {
t.Fatalf("parsePFAnchorNames() = %v, want %v", got, tc.want)
}
})
}
}
func Test_parsePFStatus(t *testing.T) {
tests := []struct {
name string
output string
wantEnabled bool
wantSince string
}{
{
name: "enabled with an uptime",
output: altqNoise + `Status: Enabled for 3 days 02:11:05 Debug: Urgent
Interface Stats for en0 IPv4 IPv6
Bytes In 123456789 0
`,
wantEnabled: true,
wantSince: "3 days 02:11:05",
},
{
name: "disabled",
output: altqNoise + "Status: Disabled Debug: Urgent\n\n",
},
{
name: "enabled without an uptime",
output: "Status: Enabled Debug: Urgent\n",
wantEnabled: true,
},
{
name: "no status line",
output: altqNoise,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
enabled, since := parsePFStatus(strings.NewReader(tc.output))
if enabled != tc.wantEnabled || since != tc.wantSince {
t.Fatalf("parsePFStatus() = %v, %q, want %v, %q", enabled, since, tc.wantEnabled, tc.wantSince)
}
})
}
}
// oversizedLine is longer than the largest line a scanner holds, so the read
// of it stops with an error.
var oversizedLine = strings.Repeat("a", 70<<10) + "\n"
// Test_parsePFAnchorNamesRejectsAnOversizedLine covers a truncated read. The
// names collected before the error read as a ruleset that lost its anchors,
// and that is the state the watchdog acts on.
func Test_parsePFAnchorNamesRejectsAnOversizedLine(t *testing.T) {
if got := parsePFAnchorNames(strings.NewReader(pfShowRules+oversizedLine), strings.NewReader(pfShowNAT)); got != nil {
t.Fatalf("anchor names of a truncated rule list = %v, want none", got)
}
if got := parsePFAnchorNames(strings.NewReader(pfShowRules), strings.NewReader(pfShowNAT+oversizedLine)); got != nil {
t.Fatalf("anchor names of a truncated translation list = %v, want none", got)
}
}
// Test_parsePFStatusRejectsAnOversizedLine covers a truncated status read.
func Test_parsePFStatusRejectsAnOversizedLine(t *testing.T) {
enabled, since := parsePFStatus(strings.NewReader(oversizedLine + pfStatusEnabled))
if enabled || since != "" {
t.Fatalf("pf status of a truncated read = %v, %q, want false and an empty uptime", enabled, since)
}
}