bastion 0.53.2
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user