//go:build !windows package main import ( "context" "encoding/json" "os" "path/filepath" "strings" "sync" "testing" "time" ) const ( tLocal = "home.cseries.localhost" tLocal2 = "second.cseries.localhost" tRemote = "remote.host.privatae.ai" ) // localBox is a networked box with machine authority in a temp config dir. func localBox(t *testing.T) { t.Helper() t.Setenv("XDG_CONFIG_HOME", t.TempDir()) withMarkerAt(t, filepath.Join(t.TempDir(), "machine.json")) prev := machineAuthorityHeld machineAuthorityHeld = func() bool { return true } t.Cleanup(func() { machineAuthorityHeld = prev }) withPolicy(t, nil) if err := setBoxMode(modeNetworked); err != nil { t.Fatal(err) } } func withPolicy(t *testing.T, p *anchorPolicy) { t.Helper() prev := readAnchorPolicy readAnchorPolicy = func() *anchorPolicy { if p == nil { return nil } c := *p return &c } t.Cleanup(func() { readAnchorPolicy = prev }) } // writeTCPPairing is a Host reached on a lane port, as pair_tcp.go writes it. func writeTCPPairing(t *testing.T, host, lane, state string) { t.Helper() pd, _ := pairingsDir() // Its control port is one nothing listens on, whatever runs on the machine // running the suite (a bastion of its own answers on 7878). b, _ := json.Marshal(Config{HostName: host, Transport: transportTCP, LaneAddr: lane, WGPrivateKey: "k", State: state, ListenAddr: "127.0.0.1:1"}) if err := os.WriteFile(filepath.Join(pd, host+".json"), b, 0o600); err != nil { t.Fatal(err) } } func pairingNamed(t *testing.T, host string) (*Config, []*Config) { t.Helper() all, err := loadPairings() if err != nil { t.Fatal(err) } c := pairingFor(all, host) if c == nil { t.Fatalf("%s not paired", host) } return c, all } func auditLines(t *testing.T) string { t.Helper() d, _ := configDir() b, _ := os.ReadFile(filepath.Join(d, "pairing_audit.jsonl")) return string(b) } // What "on this machine" is: a unix socket, or a lane port on loopback. A LAN // address, the relay's tunnel, and a Host's own name are not. // // MUTATION: return true for every tcp pairing in sameMachineHost. RESULT: red. func TestSameMachineIsASocketOrALoopbackLane(t *testing.T) { for _, c := range []struct { cfg Config want bool }{ {Config{Transport: transportUnix, LaneSocket: "/run/h/lane.sock"}, true}, {Config{Transport: transportTCP, LaneAddr: "127.0.0.1:8811"}, true}, {Config{Transport: transportTCP, LaneAddr: "127.3.4.5:8811"}, true}, {Config{Transport: transportTCP, LaneAddr: "[::1]:8811"}, true}, {Config{Transport: transportTCP, LaneAddr: "localhost:8811"}, true}, {Config{Transport: transportTCP, LaneAddr: "10.0.2.2:28811"}, false}, {Config{Transport: transportTCP, LaneAddr: "192.168.1.20:8811"}, false}, {Config{Transport: transportTCP, LaneAddr: "home.cseries.localhost:8811"}, false}, {Config{HostName: "x.cseries.localhost", BastionTunnelIP: "fd12:3456::a:1"}, false}, } { if got := sameMachineHost(&c.cfg); got != c.want { t.Errorf("%+v: same machine = %v, want %v", c.cfg, got, c.want) } } } // The S0 order the other way round: the machine was anchored to a remote Host, // then a Host on this machine is paired with machine authority. It takes the // anchor; the remote one becomes an ordinary pairing waiting for it, is told // on its beat, and the audit says so. // // MUTATION: drop the sameMachineHost case from anchorAfterPair. RESULT: red. // Leave the replaced anchor active in takeAnchor. RESULT: red. func TestAHostOnThisMachineTakesTheAnchorFromARemoteOne(t *testing.T) { localBox(t) writePairing(t, tRemote) if err := writeAnchor("the owner", tRemote); err != nil { t.Fatal(err) } writeTCPPairing(t, tLocal, "127.0.0.1:8811", pairingPending) cfg, all := pairingNamed(t, tLocal) took, err := anchorAfterPair(cfg, all) if err != nil || !took.Taken || took.Replaced != tRemote { t.Fatalf("a Host on this machine must take the anchor: %+v %v", took, err) } if a := currentAnchor(); a == nil || a.AnchorHost != tLocal || a.Owner != "the owner" { t.Fatalf("anchor = %+v", a) } if got := loadState(t, tLocal); got != pairingActive { t.Errorf("the new anchor waits on itself: %q", got) } if got := loadState(t, tRemote); got != pairingPending { t.Errorf("the replaced anchor is %q, want pending", got) } a := auditLines(t) if !strings.Contains(a, `"op":"anchor_take"`) || !strings.Contains(a, "it replaced "+tRemote) || !strings.Contains(a, "waits for "+tLocal+" to admit it") { t.Errorf("the audit does not record it:\n%s", a) } note := anchorReplacedNote(tRemote) if note == nil || note["by"] != tLocal || note["why"] != anchorWhyLocal { t.Fatalf("the replaced anchor's beat is not told: %v", note) } if e := pairingEnforcing(tRemote); e["anchor_replaced"] == nil || e["waiting_for"] != tLocal { t.Errorf("the enforcing report of the replaced anchor: %v", e) } if anchorReplacedNote(tLocal) != nil { t.Error("the new anchor is told it was replaced") } var former *pairingEntry for _, e := range pairingsReport() { if e.HostName == tRemote { e := e former = &e } } if former == nil || former.Origin != pairingOriginFormerAnchor || former.WaitingFor != tLocal || former.Transport != "tunnel" || former.SameMachine { t.Errorf("the anchor's report of the former anchor: %+v", former) } } // A networked box with no anchor and a remote Host already serving: the Host on // this machine takes the anchor, and the remote one waits for it. func TestAHostOnThisMachineAnchorsABoxThatAlreadyServesARemoteHost(t *testing.T) { localBox(t) writePairing(t, tRemote) writeTCPPairing(t, tLocal, "127.0.0.1:8811", "") cfg, all := pairingNamed(t, tLocal) took, err := anchorAfterPair(cfg, all) if err != nil || !took.Taken || took.Replaced != "" || strings.Join(took.Waiting, ",") != tRemote { t.Fatalf("%+v %v", took, err) } if got := loadState(t, tRemote); got != pairingPending { t.Errorf("the remote Host holds no grant from the new anchor and must wait: %q", got) } } // Never "whichever paired first": a remote Host never takes the anchor from a // Host on this machine, and a second Host on this machine does not either. // // MUTATION: drop the "a Host on this machine already holds it" return. // RESULT: red. func TestTheAnchorOnThisMachineIsNotReplaced(t *testing.T) { for _, newcomer := range []struct{ host, lane string }{{tRemote, "10.0.2.2:28811"}, {tLocal2, "127.0.0.1:9811"}} { t.Run(newcomer.host, func(t *testing.T) { localBox(t) writeTCPPairing(t, tLocal, "127.0.0.1:8811", "") if err := writeAnchor("the owner", tLocal); err != nil { t.Fatal(err) } writeTCPPairing(t, newcomer.host, newcomer.lane, pairingPending) cfg, all := pairingNamed(t, newcomer.host) if took, err := anchorAfterPair(cfg, all); took.Taken || err != nil { t.Fatalf("%s took the anchor: %+v %v", newcomer.host, took, err) } if a := currentAnchor(); a == nil || a.AnchorHost != tLocal { t.Fatalf("anchor moved: %+v", a) } if got := loadState(t, newcomer.host); got != pairingPending { t.Errorf("state %q", got) } }) } } // Without machine authority, or on a local machine, nothing is anchored. func TestTakingTheAnchorNeedsANetworkedMachineAndItsAuthority(t *testing.T) { localBox(t) writePairing(t, tRemote) if err := writeAnchor("the owner", tRemote); err != nil { t.Fatal(err) } writeTCPPairing(t, tLocal, "127.0.0.1:8811", pairingPending) machineAuthorityHeld = func() bool { return false } cfg, all := pairingNamed(t, tLocal) if took, _ := anchorAfterPair(cfg, all); took.Taken { t.Fatal("anchored without machine authority") } machineAuthorityHeld = func() bool { return true } localBox(t) if err := setBoxMode(modeLocal); err != nil { t.Fatal(err) } writeTCPPairing(t, tLocal, "127.0.0.1:8811", "") cfg, all = pairingNamed(t, tLocal) if took, _ := anchorAfterPair(cfg, all); took.Taken || currentAnchor() != nil { t.Fatal("anchored a local machine") } } // An organisation's policy wins over the Host on this machine: the Host it // names takes the anchor, from a Host on this machine too; any other Host // waits for it; and claim-anchor for any other Host is refused in words that // say an organisation manages the machine. // // MUTATION: check sameMachineHost before the policy in anchorAfterPair. // RESULT: red. Drop the policy refusal in claimAnchorByRule. RESULT: red. func TestAnOrganisationsPolicyNamesTheHostInCharge(t *testing.T) { localBox(t) withPolicy(t, &anchorPolicy{Host: tRemote, Source: "HKLM\\SOFTWARE\\Policies\\Privatae\\Bastion AnchorHost"}) // Unanchored: the Host on this machine waits for the one the policy names. if _, state, err := pairPrecheckState(false, handleOf(tLocal)); err != nil || state != pairingPending { t.Fatalf("a Host the policy does not name must wait: %q %v", state, err) } if _, state, _ := pairPrecheckState(false, handleOf(tRemote)); state != "" { t.Fatalf("the Host the policy names waits for nobody: %q", state) } writeTCPPairing(t, tLocal, "127.0.0.1:8811", pairingPending) cfg, all := pairingNamed(t, tLocal) if took, _ := anchorAfterPair(cfg, all); took.Taken { t.Fatal("the Host on this machine took the anchor against the organisation's policy") } if got := admitter(); got != tRemote { t.Errorf("a pending pairing waits for %q, want the policy's Host", got) } err := cmdClaimAnchor("", false, "", tLocal) if err == nil || !strings.Contains(err.Error(), "an organisation manages this machine") || !strings.Contains(err.Error(), "names "+tRemote) { t.Fatalf("claim-anchor for another Host: %v", err) } if err := cmdClaimAnchor("", false, "", ""); err == nil || !strings.Contains(err.Error(), "not paired with this machine yet") { t.Fatalf("claim-anchor before the policy's Host is paired: %v", err) } // The policy's Host is paired: it takes the anchor, the other one waits. writePairingState(t, tRemote, "") cfg, all = pairingNamed(t, tRemote) took, err := anchorAfterPair(cfg, all) if err != nil || !took.Taken { t.Fatalf("the policy's Host must take the anchor: %+v %v", took, err) } if a := currentAnchor(); a == nil || a.AnchorHost != tRemote { t.Fatalf("anchor = %+v", a) } if !strings.Contains(auditLines(t), "an organisation's policy") { t.Errorf("the audit does not name the policy: %s", auditLines(t)) } if err := cmdClaimAnchor("", true, "", tLocal); err == nil || !strings.Contains(err.Error(), "an organisation manages this machine") { t.Fatalf("claim-anchor --rotate to another Host: %v", err) } } // A policy deployed after a Host on this machine took the anchor: claim-anchor // with no argument hands it to the Host the policy names, without --rotate. func TestClaimAnchorFollowsAPolicyDeployedLater(t *testing.T) { localBox(t) writeTCPPairing(t, tLocal, "127.0.0.1:8811", "") if err := writeAnchor("the owner", tLocal); err != nil { t.Fatal(err) } writePairingState(t, tRemote, pairingPending) withPolicy(t, &anchorPolicy{Host: handleOf(tRemote), Source: "/etc/privatae-bastion/policy.json anchor_host"}) if err := cmdClaimAnchor("", false, "", ""); err != nil { t.Fatal(err) } if a := currentAnchor(); a == nil || a.AnchorHost != tRemote { t.Fatalf("anchor = %+v", a) } if got := loadState(t, tLocal); got != pairingPending { t.Errorf("the Host on this machine is %q, want waiting for the policy's Host", got) } } // claim-anchor with no argument on a box that serves a remote Host and a Host // on this machine picks the one on this machine; naming the remote one is // refused while a Host on this machine is paired. // // MUTATION: return false at the top of claimAnchorByRule's same-machine part. // RESULT: red (the old "say which one" refusal). func TestClaimAnchorPicksTheHostOnThisMachine(t *testing.T) { localBox(t) writePairing(t, tRemote) writeTCPPairing(t, tLocal, "127.0.0.1:8811", "") if err := cmdClaimAnchor("", false, "", tRemote); err == nil || !strings.Contains(err.Error(), "runs on this machine and is the Host in charge") { t.Fatalf("naming the remote Host: %v", err) } if err := cmdClaimAnchor("", false, "", ""); err != nil { t.Fatal(err) } if a := currentAnchor(); a == nil || a.AnchorHost != tLocal { t.Fatalf("anchor = %+v", a) } if got := loadState(t, tRemote); got != pairingPending { t.Errorf("the remote Host is %q", got) } } // The replaced anchor's running beat is beaten once more before its lanes stop. // // MUTATION: drop the tellReplacedAnchor call in superviseOnce, or the // tellReplacedAnchorsBeforeStop body. RESULT: red. func TestTheReplacedAnchorIsToldOnALastBeat(t *testing.T) { localBox(t) writePairing(t, tRemote) if err := writeAnchor("the owner", tRemote); err != nil { t.Fatal(err) } writeTCPPairing(t, tLocal, "127.0.0.1:8811", pairingPending) remote, _ := pairingNamed(t, tRemote) cfg, all := pairingNamed(t, tLocal) if took, err := anchorAfterPair(cfg, all); err != nil || !took.Taken { t.Fatal(took, err) } var told any beat := func() { told = pairingEnforcing(tRemote)["anchor_replaced"] } _, unregister := registerRunBeat(tRemote, beat) defer unregister() done := make(chan struct{}) close(done) supervisor.Lock() supervisor.runs = map[string]*pairingRun{tRemote: {cancel: func() {}, done: done, cfg: remote}} supervisor.Unlock() t.Cleanup(func() { supervisor.Lock(); supervisor.runs = nil; supervisor.Unlock() }) // The daemon stopping first (an installer's Restart-Service right after // `pair`) tells it as well. tellReplacedAnchorsBeforeStop() if told == nil { t.Fatal("the daemon stopped without a last beat to the replaced anchor") } told = nil superviseOnce() if told == nil { t.Fatal("the replaced anchor was stopped without a last beat that says so") } // Admitted again, it is no longer told. clearAnchorReplacedFor(tRemote) if anchorReplacedNote(tRemote) != nil { t.Error("an admitted former anchor still hears it was replaced") } } // One pairing's fatal error stops that pairing alone: nothing reaches the // daemon's fatal channel, the failure is recorded for status, and it is tried // again after a pause. // // MUTATION: send the error to supervisor.errc again. RESULT: red. func TestOnePairingsFatalErrorStopsOnlyThatPairing(t *testing.T) { t.Setenv("XDG_CONFIG_HOME", t.TempDir()) freshLocalEvents(t) // A Host over TCP whose CA does not read: runUnixPairing fails at once. bad := &Config{HostName: "broken.example.test", Transport: transportTCP, LaneAddr: "127.0.0.1:1", Bearer: "b", CACert: "not a certificate", ListenAddr: "127.0.0.1:0"} ctx, cancel := context.WithCancel(context.Background()) defer cancel() errc := make(chan error, 4) supervisor.Lock() prevCtx, prevErrc, prevRuns := supervisor.ctx, supervisor.errc, supervisor.runs supervisor.ctx, supervisor.errc, supervisor.runs = ctx, errc, nil supervisor.Unlock() t.Cleanup(func() { supervisor.Lock() supervisor.ctx, supervisor.errc, supervisor.runs = prevCtx, prevErrc, prevRuns supervisor.Unlock() pairingRecovered(bad.HostName) }) if !startPairing(bad) { t.Fatal("not started") } deadline := time.Now().Add(5 * time.Second) for time.Now().Before(deadline) { if _, ok := loadPairingFailures()[bad.HostName]; ok { break } time.Sleep(20 * time.Millisecond) } select { case err := <-errc: t.Fatalf("one pairing's error went to the whole daemon: %v", err) case <-time.After(100 * time.Millisecond): } f, ok := loadPairingFailures()[bad.HostName] if !ok || f.Error == "" || f.Tries != 1 { t.Fatalf("the failure is not recorded: %+v", f) } if pairingRetryDue(bad.HostName) { t.Error("tried again with no pause") } if retryPause(1) != supervisePeriod || retryPause(3) != 4*supervisePeriod || retryPause(40) != retryMaxPause { t.Errorf("pauses %v %v %v", retryPause(1), retryPause(3), retryPause(40)) } found := false for _, e := range localEventsNewestFirst() { found = found || strings.Contains(e.Text, "The other Hosts go on being served") } if !found { t.Error("the machine's events do not say so") } } // The Windows service manager's stop is logged as its own, not the tray's. // The service handler cannot run here, so its source is read. // // MUTATION: call requestShutdown() in sysservice_windows.go. RESULT: red. func TestTheServiceManagersStopIsNotLoggedAsTheTrays(t *testing.T) { src, err := os.ReadFile("sysservice_windows.go") if err != nil { t.Fatal(err) } if !strings.Contains(string(src), "requestShutdownFor(shutdownFromService)") || strings.Contains(string(src), "requestShutdown()") { t.Error("the service's stop does not name the service manager") } run, _ := os.ReadFile("run.go") if !strings.Contains(string(run), `"\nstopping — " + shutdownWhy`) { t.Error("cmdRun does not log who asked it to stop") } if strings.Contains(shutdownFromService, "tray") { t.Error(shutdownFromService) } } // Over a network logon (ssh) the service cannot be queried for its process id; // the ask pipe is then checked by its owner, SYSTEM, instead of refused. // // MUTATION: drop the ERROR_ACCESS_DENIED branch. RESULT: red. func TestTheAskPipeIsCheckedByItsOwnerWhenTheServiceCannotBeQueried(t *testing.T) { src, err := os.ReadFile("approvalpipe_windows.go") if err != nil { t.Fatal(err) } s := string(src) i := strings.Index(s, "func pipeServerIsService(") if i < 0 { t.Fatal("no pipeServerIsService") } body := s[i : i+strings.Index(s[i:], "\n}\n")] if !strings.Contains(body, "errors.Is(err, windows.ERROR_ACCESS_DENIED)") || !strings.Contains(body, "return pipeOwnedBySystem(h)") { t.Error("a network logon is still refused") } if !strings.Contains(s, "!owner.IsWellKnown(windows.WinLocalSystemSid) && !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid)") { t.Error("the owner check does not require LocalSystem or the Administrators group") } } // The last beat to a replaced anchor (lastBeat, from superviseOnce's // goroutine) never runs beside the run's own beat or path failover: both write // cfg.Endpoints and the WG device, and before lastBeat a run's beats all came // from its one loop. func TestTheLastBeatNeverRunsBesideTheRunsOwnBeat(t *testing.T) { const host = "serial-beat.host.privatae.ai" var mu sync.Mutex in, most := 0, 0 entered := make(chan struct{}, 4) release := make(chan struct{}) beat := func() { mu.Lock() in++ if in > most { most = in } mu.Unlock() entered <- struct{}{} <-release mu.Lock() in-- mu.Unlock() } rb, unregister := registerRunBeat(host, beat) defer unregister() old := lastBeatWait lastBeatWait = 200 * time.Millisecond t.Cleanup(func() { lastBeatWait = old }) loopDone := make(chan struct{}) go func() { defer close(loopDone); rb.run() }() // the run's own beat, in flight <-entered failoverRan := make(chan struct{}) go func() { rb.do(func() {}); close(failoverRan) }() // the tick's failover waits too if !lastBeat(host) { t.Fatal("lastBeat found no beat for a running pairing") } select { case <-entered: t.Fatal("the last beat ran beside the run's own beat") default: } select { case <-failoverRan: t.Fatal("path failover ran beside a beat") default: } close(release) <-loopDone <-failoverRan mu.Lock() defer mu.Unlock() if most != 1 { t.Fatalf("%d beats ran at once, want 1", most) } }