mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
209 lines
8.1 KiB
Go
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")
|
|
}
|
|
}
|