From 039abeff6937b3989defe9ed41dde836e3ca89af Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 7 Oct 2026 18:51:45 +0200 Subject: [PATCH] Drill the network judge in a throwaway machine (hq ADR 0241) The judge's tests run against files and fakes; the drill runs it against a real resolver file and real resolvers, rewritten as the VPN client does, and records the statements the controller's replay raises and clears from. --- internal/network/drill_test.go | 101 +++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 internal/network/drill_test.go diff --git a/internal/network/drill_test.go b/internal/network/drill_test.go new file mode 100644 index 0000000..fa22fba --- /dev/null +++ b/internal/network/drill_test.go @@ -0,0 +1,101 @@ +package network + +import ( + "encoding/json" + "os" + "strings" + "testing" + "time" + + "github.com/novox/mesh-host/internal/link" +) + +// TestDrillAResolverFileRewrittenInAThrowawayMachine is the drill of novox/hq ADR 0241: on a machine of +// its own — a throwaway container, never a machine of the mesh — the resolver file is what was declared, +// then rewritten the way a VPN client rewrites it, then written back; the judge must say it healthy, +// unhealthy on the second look naming the writer and the names it costs, and healthy again on the first +// look after. Each statement is written, as the engine says it, to MESH_NETWORK_DRILL_OUT, for the +// controller's replay of the same drill (mesh-controller testdata/network-drill.json). +// +// Run only where MESH_NETWORK_DRILL names the mesh name to ask, because it rewrites /etc/resolv.conf: +// +// CGO_ENABLED=0 go test -c -o drill ./internal/network +// docker run --rm -v $PWD/drill:/drill:ro -v $PWD/out:/out -e MESH_NETWORK_DRILL= \ +// -e MESH_NETWORK_DRILL_OUT=/out/network-drill.json alpine /drill -test.run Drill -test.v +// +// The clock moves a look on per look; the file, the resolvers and the answers are the machine's own. +func TestDrillAResolverFileRewrittenInAThrowawayMachine(t *testing.T) { + meshName := os.Getenv("MESH_NETWORK_DRILL") + if meshName == "" { + t.Skip("a drill rewrites /etc/resolv.conf: run it in a throwaway container with MESH_NETWORK_DRILL=") + } + declared, err := os.ReadFile(ResolvConf) + if err != nil { + t.Fatal(err) + } + now := time.Now() + j := New(Machine{Now: func() time.Time { return now }, Linked: func() bool { return true }}, meshName) + j.Declare(string(declared), "networkmanager", true) + + var said []link.Health + look := func(want string) Statement { + t.Helper() + now = now.Add(LookEvery) + st, _ := j.Look(t.Context()) + h := link.Health{Contract: link.ReadinessContract, At: st.At.UTC(), Resources: []link.ResourceHealth{}, + Network: &link.NetworkHealth{State: st.State, Since: st.Since.UTC(), Parts: []link.NetworkPart{}}} + for _, p := range st.Parts { + h.Network.Parts = append(h.Network.Parts, link.NetworkPart{Part: p.Part, State: p.State, Reason: p.Reason, + Said: p.Said, Writer: p.Writer, Owner: p.Owner, Toward: p.Toward, Since: p.Since.UTC(), Streak: p.Streak}) + } + said = append(said, h) + raw, _ := json.Marshal(st) + t.Logf("%s", raw) + if st.State != want { + t.Fatalf("want %s, the judge says %s", want, st.State) + } + return st + } + write := func(content string) { + t.Helper() + // In place, as the VPN client's rename leaves a file of the same name: the judge reads by path. + if err := os.WriteFile(ResolvConf, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + defer write(string(declared)) + + look(Healthy) + look(Healthy) + // What the VPN client writes on connect: its header, its own resolvers — addresses that answer + // nothing here — and its search domains. + write("# Dynamic resolv.conf(5) file for glibc resolver(3) generated by forticlient\n" + + "# The original file is backed up and will be restored after the VPN disconnects.\n" + + "nameserver 192.0.2.53\nsearch corp.example\noptions timeout:1\n") + look(Healthy) // one look is not a finding + st := look(Unhealthy) + for _, p := range st.Parts { + switch p.Part { + case PartResolvConf: + if p.State != Unhealthy || p.Writer != "FortiClient" || p.Owner != "networkmanager" { + t.Fatalf("the file is said %+v", p) + } + case PartNames: + if p.State != Unhealthy || !strings.Contains(p.Said, "192.0.2.53") { + t.Fatalf("the names through it are said %+v", p) + } + } + } + write(string(declared)) + look(Healthy) + + if out := os.Getenv("MESH_NETWORK_DRILL_OUT"); out != "" { + raw, err := json.MarshalIndent(said, "", " ") + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(out, raw, 0o644); err != nil { + t.Fatal(err) + } + } +}