Files
ctrld/cmd/cli/pf_diagnostic_test.go
T

209 lines
8.1 KiB
Go

package cli
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"time"
)
// Native-format provenance: macOS pfctl's @N / Owner / Evaluations / Inserted
// layout is published at https://apple.stackexchange.com/questions/335019
// (pfctl -vvv -s all); the counter layout is also reported on macOS 26.3 at
// https://apple.stackexchange.com/questions/486150 (pfctl -s rules -v).
// This is an ADAPTED fixture, not a capture from a ctrld host: headers use the
// two expanded forms of generatePFRules' IPv6 DNS block, and numbers are test
// values. Keep the native spacing/metadata; do not use pf.conf input as output.
const pfDiagnosticFixture = `@30 block drop out quick on ! lo0 inet6 proto udp from any to any port = 53
[ Owner : nil Priority : 0 ]
[ Evaluations: 574091 Packets: 812 Bytes: 51968 States: 0 ]
[ Inserted: uid 0 pid 68 ]
@31 block drop out quick on ! lo0 inet6 proto tcp from any to any port = 53
[ Owner : nil Priority : 0 ]
[ Evaluations: 573678 Packets: 210 Bytes: 13152 States: 0 ]
[ Inserted: uid 0 pid 68 ]
`
func TestPFDiagnosticNativeFormat(t *testing.T) {
s := parsePFDiagnostic([]byte(pfDiagnosticFixture))
if s.Code != "ok" || !s.UDP.Installed || !s.TCP.Installed || !s.UDP.Known || !s.TCP.Known || s.UDP.Packets != 812 || s.TCP.Packets != 210 {
t.Fatalf("native-format fixture: %+v", s)
}
// IDs and metadata do not become logged identity, and need not be stable.
changed := strings.NewReplacer("@30", "@1", "@31", "@2", "! lo0", "!lo0", "pid 68", "pid 99").Replace(pfDiagnosticFixture)
if got := parsePFDiagnostic([]byte(changed)); got != s {
t.Fatalf("unstable identity: %+v", got)
}
}
func TestPFDiagnosticUnknownIsNotZero(t *testing.T) {
for _, tc := range []struct {
name, input, code string
installed bool
}{
{"empty", "", "rules_not_observed", false},
{"unrelated", strings.ReplaceAll(pfDiagnosticFixture, "inet6", "inet"), "rules_not_observed", false},
{"other interface", strings.ReplaceAll(pfDiagnosticFixture, "! lo0", "en0"), "rules_not_observed", false},
{"not exact", strings.ReplaceAll(pfDiagnosticFixture, "port = 53", "port = 53 label private"), "rules_not_observed", false},
{"malformed", strings.ReplaceAll(pfDiagnosticFixture, "Packets:", "Packets: invalid"), "partial_or_malformed", true},
{"overflow", strings.ReplaceAll(pfDiagnosticFixture, "Packets:", "Packets: 18446744073709551616"), "partial_or_malformed", true},
{"duplicate", pfDiagnosticFixture + pfDiagnosticFixture, "ambiguous_rules", true},
{"no counters", strings.Split(pfDiagnosticFixture, "\n")[0], "partial_or_malformed", true},
} {
t.Run(tc.name, func(t *testing.T) {
s := parsePFDiagnostic([]byte(tc.input))
if s.Code != tc.code || s.UDP.Installed != tc.installed || s.UDP.Known || s.TCP.Known {
t.Fatalf("unknown misreported: %+v", s)
}
})
}
zero := strings.NewReplacer("812", "0", "210", "0").Replace(pfDiagnosticFixture)
if s := parsePFDiagnostic([]byte(zero)); s.Code != "ok" || !s.UDP.Known || s.UDP.Packets != 0 {
t.Fatalf("real zero: %+v", s)
}
// A missing UDP counter must not consume the following unrelated rule's one.
input := strings.Split(pfDiagnosticFixture, "\n")[0] + "\n@32 pass out all\n [ Evaluations: 1 Packets: 900 Bytes: 100 States: 0 ]\n"
if s := parsePFDiagnostic([]byte(input)); s.UDP.Known {
t.Fatalf("borrowed unrelated counter: %+v", s)
}
}
func TestPFDiagnosticProcess(t *testing.T) {
switch os.Getenv("CTRLD_PF_DIAGNOSTIC_TEST") {
case "":
return
case "ok":
fmt.Print(pfDiagnosticFixture)
case "failure":
fmt.Print(pfDiagnosticFixture)
fmt.Fprint(os.Stderr, "private rules/client-address")
os.Exit(1)
case "limit":
fmt.Print(strings.Repeat("x", pfDiagnosticMaxBytes+1))
case "hang":
time.Sleep(time.Minute)
}
os.Exit(0)
}
func pfDiagnosticTestCommand(ctx context.Context, mode string) *exec.Cmd {
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestPFDiagnosticProcess$")
cmd.Env = append(os.Environ(), "CTRLD_PF_DIAGNOSTIC_TEST="+mode)
return cmd
}
func TestPFDiagnosticCommandBounds(t *testing.T) {
for _, tc := range []struct{ mode, code string }{{"ok", "ok"}, {"failure", "read_failed"}, {"limit", "output_limit"}, {"hang", "timeout"}} {
t.Run(tc.mode, func(t *testing.T) {
d := 5 * time.Second
if tc.mode == "hang" {
d = 100 * time.Millisecond
}
ctx, cancel := context.WithTimeout(context.Background(), d)
defer cancel()
start := time.Now()
s := runPFDiagnosticCommand(ctx, pfDiagnosticTestCommand(ctx, tc.mode))
if s.Code != tc.code {
t.Fatalf("got %+v, want %s", s, tc.code)
}
if tc.mode != "ok" && (s.UDP.Known || s.TCP.Known || s.UDP.Installed) {
t.Fatalf("failed read trusted: %+v", s)
}
if tc.mode == "hang" && time.Since(start) > time.Second {
t.Fatal("deadline not bounded")
}
})
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
if got := runPFDiagnosticCommand(ctx, pfDiagnosticTestCommand(ctx, "ok")); got.Code != "canceled" {
t.Fatalf("cancellation: %+v", got)
}
if got := runPFDiagnosticCommand(context.Background(), exec.Command(t.TempDir()+"/missing")); got.Code != "read_failed" {
t.Fatalf("start failure: %+v", got)
}
var b pfDiagnosticBuffer
_, _ = b.Write(make([]byte, pfDiagnosticMaxBytes))
if _, err := b.Write([]byte{1}); err == nil || b.Len() != pfDiagnosticMaxBytes {
t.Fatal("buffer is unbounded")
}
}
func TestPFDiagnosticCadenceTransitionsAndReset(t *testing.T) {
var s pfDiagnosticState
now := time.Unix(1, 0)
reads := 0
fixture := parsePFDiagnostic([]byte(pfDiagnosticFixture))
capture := func() pfDiagnosticSnapshot { reads++; return fixture }
o := pfProbeObservation{result: pfProbeIntercepted, target: "192.0.2.1:53"}
if _, emit := s.observe(now, o, capture); emit || reads != 0 {
t.Fatal("healthy poll did work")
}
o.result, o.stage, o.code = pfProbeNotIntercepted, "delivery", "timeout"
e, emit := s.observe(now, o, capture)
if !emit || e.Cached || e.Generation != 1 || e.UDPDeltaCode != "unknown" {
t.Fatalf("first failure: %+v %v", e, emit)
}
for i := 0; i < 100; i++ {
o.probeID = fmt.Sprint(i)
o.recoveryGeneration++
if _, emit := s.observe(now.Add(time.Second), o, capture); emit {
t.Fatal("duplicate failure noisy")
}
}
if reads != 1 {
t.Fatalf("unbounded reads: %d", reads)
}
if _, emit := s.observe(now.Add(time.Minute), o, capture); emit || reads != 2 {
t.Fatal("unchanged refresh noisy")
}
fixture.UDP.Packets++
e, emit = s.observe(now.Add(2*time.Minute), o, capture)
if !emit || e.UDPDelta != 1 || e.UDPDeltaCode != "shared_traffic" {
t.Fatalf("delta: %+v", e)
}
fixture.UDP.Packets = 0
e, _ = s.observe(now.Add(3*time.Minute), o, capture)
if e.UDPDeltaCode != "reset" || e.UDPDelta != 0 {
t.Fatalf("reset: %+v", e)
}
o.result = pfProbeIntercepted
e, emit = s.observe(now.Add(3*time.Minute+time.Second), o, capture)
if !emit || !e.Cached || e.Age != time.Second || e.Generation != 4 || e.UDPDeltaCode != "unknown" {
t.Fatalf("restored: %+v", e)
}
if _, emit := s.observe(now.Add(4*time.Minute), o, capture); emit || reads != 4 {
t.Fatal("healthy repeats did work")
}
o.code = "canceled"
o.result = pfProbeIndeterminate
if _, emit := s.observe(now.Add(5*time.Minute), o, capture); emit || reads != 4 {
t.Fatal("shutdown did work")
}
fixture.Code = "read_failed"
fixture.UDP.Known = false
fixture.TCP.Known = false
o.code = "unavailable"
e, emit = s.observe(now.Add(6*time.Minute), o, capture)
if !emit || e.UDPDeltaCode != "unknown" {
t.Fatalf("failed read: %+v", e)
}
}
func TestPFDiagnosticConcurrentCaptureDoesNotBlock(t *testing.T) {
var s pfDiagnosticState
started, release, done := make(chan struct{}), make(chan struct{}), make(chan struct{})
go func() {
defer close(done)
s.observe(time.Now(), pfProbeObservation{}, func() pfDiagnosticSnapshot { close(started); <-release; return pfDiagnosticSnapshot{} })
}()
<-started
defer func() { close(release); <-done }()
if _, emit := s.observe(time.Now(), pfProbeObservation{}, func() pfDiagnosticSnapshot { t.Error("overlapping capture"); return pfDiagnosticSnapshot{} }); emit {
t.Fatal("concurrent capture should stand down")
}
}