Files
Privatae e4c9b69b46
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s
bastion 0.53.2
2026-10-20 04:42:14 +00:00

298 lines
11 KiB
Go

package main
// Gate: each host's backup folder is its own. Two pairings whose folders are
// the same, or one inside the other, are refused on every backup op, and so
// is a folder over the bastion's own configuration, keys or records.
import (
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func savePairingWithRoot(t *testing.T, host, root string) {
t.Helper()
c := &Config{HostName: host, WGPrivateKey: "k", BastionTunnelIP: "fd00:9::a:1", BackupRoot: root}
if err := c.save(); err != nil {
t.Fatal(err)
}
}
func TestBackupRootsAreEachHostsOwn(t *testing.T) {
uiTestConfigDir(t)
const a, b = "a.host.privatae.ai", "b.host.privatae.ai"
base := t.TempDir()
rootA := filepath.Join(base, "a")
_ = os.MkdirAll(rootA, 0o700)
savePairingWithRoot(t, a, rootA)
savePairingWithRoot(t, b, filepath.Join(base, "b"))
if why := backupRootProblem(a, rootA); why != "" {
t.Fatalf("separate folders were refused: %s", why)
}
cases := map[string]string{
"the same folder": rootA,
"a folder inside it": filepath.Join(rootA, "sub"),
"a folder containing it": base,
}
link := filepath.Join(t.TempDir(), "link-to-a")
if err := os.Symlink(rootA, link); err == nil {
cases["the same folder through a link"] = link
}
for name, other := range cases {
savePairingWithRoot(t, b, other)
why := backupRootProblem(a, rootA)
if why == "" || !strings.Contains(why, b) {
t.Errorf("%s: not refused (%q)", name, why)
}
}
// On the lane: every backup op is refused before its handler runs.
savePairingWithRoot(t, b, rootA)
_, errOut, code := driveBackupOpAs(t, a, "backup_list", "", rootA, map[string]string{"op": "list"})
if code == 0 || !strings.Contains(errOut, "backup_root of its own") {
t.Fatalf("a shared backup folder was listed: %d %s", code, errOut)
}
// And the person cannot share a folder with a host whose own folder is
// not its own.
if err := cmdBackupShare(a, "", t.TempDir()); err == nil || !strings.Contains(err.Error(), "backup_root of its own") {
t.Fatal("a share was given to a host whose backup folder another host also uses")
}
}
func TestBackupRootNeverOverlapsTheBastionsOwnState(t *testing.T) {
cfg := uiTestConfigDir(t)
confDir, _ := configDirPath()
keys, _ := backupKeysDir()
state, _ := backupStateDir()
bad := map[string]string{
"the home folder": cfg,
"the configuration folder": confDir,
"the backup keys": keys,
"inside the backup keys": filepath.Join(keys, "x"),
"the backup records": state,
"inside the backup records": filepath.Join(state, "x"),
"the pairings": filepath.Join(confDir, "pairings"),
"inside the DCN state": filepath.Join(confDir, "dcn", "h.host.privatae.ai"),
"another config subfolder": filepath.Join(confDir, "context"),
"the backups folder itself": filepath.Join(confDir, "backups"),
}
for name, root := range bad {
if why := backupRootProblem("h.host.privatae.ai", root); why == "" {
t.Errorf("%s (%s) was accepted as a backup folder", name, root)
}
}
def := (&Config{HostName: "h.host.privatae.ai"}).backupRootPath()
for _, ok := range []string{def, filepath.Join(confDir, "backups", "any", "depth"), t.TempDir()} {
if why := backupRootProblem("h.host.privatae.ai", ok); why != "" {
t.Fatalf("%s was refused: %s", ok, why)
}
}
if _, err := addBackupShare("h.host.privatae.ai", def, confDir, "conf"); err == nil {
t.Fatal("the configuration folder was shared with a host")
}
}
// Folders shared with a pairing go when it is unpaired.
func TestUnpairTakesBackTheSharesOfThatPairing(t *testing.T) {
uiTestConfigDir(t)
const h, other = "gone.host.privatae.ai", "stays.host.privatae.ai"
savePairingWithRoot(t, h, t.TempDir())
if _, err := addBackupShare(h, t.TempDir(), t.TempDir(), "x"); err != nil {
t.Fatal(err)
}
if _, err := addBackupShare(other, t.TempDir(), t.TempDir(), "y"); err != nil {
t.Fatal(err)
}
prev := unpairNeedsRoot
unpairNeedsRoot = func() bool { return false }
t.Cleanup(func() { unpairNeedsRoot = prev })
if _, err := unpairHost(h); err != nil {
t.Fatal(err)
}
if len(sharesFor(h)) != 0 {
t.Fatal("an unpaired host kept the folders shared with it")
}
if len(sharesFor(other)) != 1 {
t.Fatal("another host's share went with it")
}
}
// A backup folder the person put outside the configuration folder (a NAS
// mount) is off limits to ops like the rest of this box's record: no op may
// name it, anything in it, or a folder holding it, as an effect.
func TestOpsCannotTouchAHostsBackupFolder(t *testing.T) {
uiTestConfigDir(t)
nas := filepath.Join(t.TempDir(), "nas", "backups")
_ = os.MkdirAll(nas, 0o700)
savePairingWithRoot(t, "nas.host.privatae.ai", nas)
offs, err := opOffLimits()
if err != nil {
t.Fatal(err)
}
found := false
for _, o := range offs {
found = found || o == nas
}
if !found {
t.Fatalf("the backup folder %s is not off limits: %v", nas, offs)
}
for _, p := range []string{nas, filepath.Join(nas, fixtureData), filepath.Dir(nas)} {
off, err := underOffLimits(p)
if err != nil || off == "" {
t.Errorf("%s is not refused as an op path: %q %v", p, off, err)
}
if err := effectOK(p + "/"); err == nil {
t.Errorf("%s is accepted as an op effect", p)
}
}
// An action op's root verb cannot be pointed at it either.
if err := opVerbArgOK(&opManifest{}, "verb", "path", filepath.Join(nas, fixtureData)); err == nil ||
!strings.Contains(err.Error(), "own record") {
t.Errorf("an action op's path argument reached the backup folder: %v", err)
}
if off, _ := underOffLimits(filepath.Join(t.TempDir(), "elsewhere")); off != "" {
t.Fatalf("an unrelated folder is refused: %s", off)
}
}
// The bastion's own folders are compared as they resolve: a backup folder
// that is a link to the configuration folder, or a configuration folder
// reached through a link, is refused all the same.
func TestBackupRootOverlapIsJudgedAsItResolves(t *testing.T) {
real := t.TempDir()
link := filepath.Join(t.TempDir(), "cfg-link")
if err := os.Symlink(real, link); err != nil {
t.Skip("no symlinks here")
}
t.Setenv("XDG_CONFIG_HOME", link)
t.Setenv("HOME", link)
t.Setenv("AppData", link)
conf, _ := configDirPath()
_ = os.MkdirAll(conf, 0o700)
realConf := filepath.Join(real, "privatae-bastion")
for name, root := range map[string]string{
"the configuration folder by its real path": realConf,
"inside it by its real path": filepath.Join(realConf, "pairings"),
} {
if why := backupRootProblem("h.host.privatae.ai", root); why == "" {
t.Errorf("%s (%s) was accepted", name, root)
}
}
if why := backupRootProblem("h.host.privatae.ai", filepath.Join(realConf, "backups", "h")); why != "" {
t.Fatalf("the default folder by its real path was refused: %s", why)
}
}
// The daemon never touches the backup folder when it starts: the lane gets
// the folder's path, and each backup op checks and creates it (with a
// bound). Read from the source, because starting the real daemon here would
// need a pairing and a tunnel.
func TestDaemonStartDoesNotTouchTheBackupFolder(t *testing.T) {
src, err := os.ReadFile("run.go")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(src), "backupRoot()") || !strings.Contains(string(src), "cfg.backupRootPath(), mgmtWorkerCount") {
t.Fatal("run.go creates or resolves the backup folder at start instead of passing its path to the lane")
}
}
// pushStatusVia runs backup_push status, a backup op that writes (so it is
// the one that creates the folder), and returns its exit and stderr.
func pushStatusVia(t *testing.T, root string) (int, string) {
t.Helper()
_, errOut, code := driveBackupOp(t, "backup_push", "", root, backupPushReq{Op: "status", JobID: "bp-none"})
return code, errOut
}
// The folder is made by the first backup op that writes, not by a listing:
// backup_list is a read and creates nothing.
func TestTheBackupFolderIsCreatedOnFirstBackupOp(t *testing.T) {
root := filepath.Join(t.TempDir(), "not-yet")
l := listVia(t, root)
if len(l) != 0 {
t.Fatalf("listed %+v", l)
}
if _, err := os.Stat(root); !os.IsNotExist(err) {
t.Fatalf("backup_list, a read, created the backup folder: %v", err)
}
if code, errOut := pushStatusVia(t, root); code != 0 {
t.Fatalf("backup_push status: %d %s", code, errOut)
}
if fi, err := os.Stat(root); err != nil || !fi.IsDir() {
t.Fatal("the first backup op that writes did not create the backup folder")
}
}
// A backup folder that errors when created refuses that op with the reason,
// and the next op tries again: no restart needed.
func TestABackupFolderThatCannotBeCreatedIsRetriedPerOp(t *testing.T) {
root := filepath.Join(t.TempDir(), "nas")
prev := backupMkdirFn
backupMkdirFn = func(string, os.FileMode) error { return errors.New("read-only file system") }
code, errOut := pushStatusVia(t, root)
backupMkdirFn = prev
if code == 0 || !strings.Contains(errOut, "cannot be created") || !strings.Contains(errOut, "read-only") {
t.Fatalf("a folder that cannot be created: %d %s", code, errOut)
}
if l := listVia(t, root); len(l) != 0 {
t.Fatalf("listed %+v", l)
}
}
// A backup folder that does not answer refuses that host's backup ops
// quickly, with a sentence, and the rest of its lane goes on.
func TestABackupFolderThatDoesNotAnswerRefusesOnlyItsBackupOps(t *testing.T) {
opMaskFixture(t)
root := filepath.Join(t.TempDir(), "nas")
_ = os.MkdirAll(root, 0o700)
hangFolder(t, root)
for i := 0; i < 2; i++ {
start := time.Now()
_, errOut, code := driveBackupOp(t, "backup_list", "", root, map[string]string{"op": "list"})
if code == 0 || !strings.Contains(errOut, "does not answer") {
t.Fatalf("a folder that does not answer: %d %s", code, errOut)
}
if el := time.Since(start); el > 2*time.Second {
t.Fatalf("the op waited %s", el)
}
}
// The same lane still answers an op that needs no backup folder.
out, _, code := driveBackupOp(t, "backup_reseal", t.TempDir(), root, resealReq{Op: "status", JobID: "br-none"})
if code != 0 || !strings.Contains(out, "unknown") {
t.Fatalf("the rest of the lane stopped: %d %s", code, out)
}
// A creation that hangs is bounded the same way.
fresh := filepath.Join(t.TempDir(), "fresh")
gate := make(chan struct{})
prev := backupMkdirFn
backupMkdirFn = func(p string, m os.FileMode) error {
if p == fresh {
<-gate
}
return prev(p, m)
}
t.Cleanup(func() {
close(gate)
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
opProbeMu.Lock()
busy := opProbeInFlight["mkdir:"+fresh]
opProbeMu.Unlock()
if !busy {
break
}
time.Sleep(5 * time.Millisecond)
}
backupMkdirFn = prev
})
start := time.Now()
code, errOut := pushStatusVia(t, fresh)
if code == 0 || !strings.Contains(errOut, "does not answer") || time.Since(start) > 2*time.Second {
t.Fatalf("a creation that hangs: %d %s after %s", code, errOut, time.Since(start))
}
}