mesh/merge-gate head acf075fa63cc merged into trunk 9675babf528b — pass: builds ssh-client → ace, g14, novox, shanks; no bus step; every machine composes w…
mesh/repo-check head acf075fa63cc merged into trunk 9675babf528b — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
ssh-client owns ~/.ssh, but treated every file it did not place as found, so a predecessor's drop-in and old copies stayed there for weeks. Its process now sweeps at start and every ten minutes: keys at any depth (and links to them), authorized_keys, authorized_keys2 and sockets stay, and a folder holding one is gone into; everything else moves to ~/.local/state/ssh-client/retired/<UTC time>/, a folder never shared with another sweep, with a list beside it, which ssh_client_check shows. Nothing is deleted.
272 lines
9.1 KiB
Go
272 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ADR 0415, issue 1017: the sweep keeps ~/.ssh to what this module knows and moves the rest out.
|
|
|
|
func sweepHome(t *testing.T) (*Client, string) {
|
|
t.Helper()
|
|
home := t.TempDir()
|
|
dir := filepath.Join(home, ".ssh")
|
|
put := func(rel, body string, mode os.FileMode) {
|
|
p := filepath.Join(dir, rel)
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, []byte(body), mode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
key := "-----BEGIN OPENSSH PRIVATE KEY-----\nPRIVATE\n"
|
|
for rel := range Declared {
|
|
if rel != "config.d" {
|
|
put(rel, "declared\n", 0o600)
|
|
}
|
|
}
|
|
put("id_ed25519", key, 0o600)
|
|
put("id_ed25519.pub", ed25519Line(1, "me")+"\n", 0o644)
|
|
put("work_key", key, 0o600) // a key with an unusual name
|
|
put("work_key.pub", ed25519Line(2, "work")+"\n", 0o644)
|
|
put("work_key-cert.pub", "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29t work\n", 0o644)
|
|
put("config.bak-20260807-015327", "old\n", 0o600)
|
|
put("known_hosts.old", "old\n", 0o644)
|
|
put("config.d/mesh", "Host ace\n", 0o600)
|
|
put("removed-hal-20261002/config", "old\n", 0o600)
|
|
put("removed-drone-20261004/id_drone", key, 0o600) // a key inside a leftover folder
|
|
put("removed-drone-20261004/config", "old\n", 0o600)
|
|
put("agent_only.pub", ed25519Line(3, "private half in an agent")+"\n", 0o644)
|
|
put("notes.pub", "not a key\n", 0o644)
|
|
put("authorized_keys.revoked-20261001T000000Z", ed25519Line(4, "revoked")+"\n", 0o600)
|
|
put("authorized_keys.bak", ed25519Line(5, "copy")+"\n", 0o600)
|
|
put("authorized_keys2", ed25519Line(6, "second file")+"\n", 0o600)
|
|
put("config", "# BEGIN mesh ssh-client.config\nInclude ~/.ssh/config.d/*\n# END mesh ssh-client.config\n\nHost mine\n User me\nInclude ~/.ssh/config.d/mesh\n", 0o600)
|
|
elsewhere := filepath.Join(home, "token")
|
|
os.WriteFile(elsewhere, []byte(key), 0o600)
|
|
if err := os.Symlink(elsewhere, filepath.Join(dir, "id_token")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(filepath.Join(home, "gone"), filepath.Join(dir, "dangling")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return &Client{Home: home}, dir
|
|
}
|
|
|
|
func exists(p string) bool { _, err := os.Lstat(p); return err == nil }
|
|
|
|
func TestSweepKeepsWhatItKnows(t *testing.T) {
|
|
c, dir := sweepHome(t)
|
|
now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC)
|
|
s, err := c.Sweep(now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, rel := range []string{"config", "known_hosts", "authorized_keys", "config.d/00-mesh", "config.d/10-forge",
|
|
KeyFile, KeyFile + ".pub", "id_ed25519", "id_ed25519.pub", "work_key", "work_key.pub", "work_key-cert.pub", "agent_only.pub", "removed-drone-20261004/id_drone", "authorized_keys2", "id_token"} {
|
|
if !exists(filepath.Join(dir, rel)) {
|
|
t.Errorf("%s was moved; it is known", rel)
|
|
}
|
|
}
|
|
root := filepath.Join(c.Home, RetiredDir, "20261017T120000Z")
|
|
for _, rel := range []string{"config.bak-20260807-015327", "known_hosts.old", "config.d/mesh", "removed-hal-20261002", "notes.pub", "removed-drone-20261004/config",
|
|
"authorized_keys.revoked-20261001T000000Z", "authorized_keys.bak", "dangling"} {
|
|
if exists(filepath.Join(dir, rel)) {
|
|
t.Errorf("%s is still in ~/.ssh", rel)
|
|
}
|
|
if !exists(filepath.Join(root, rel)) {
|
|
t.Errorf("%s is not kept at %s", rel, filepath.Join(root, rel))
|
|
}
|
|
}
|
|
if !exists(filepath.Join(root, "removed-hal-20261002", "config")) {
|
|
t.Error("a folder was not moved whole")
|
|
}
|
|
if len(s.Moved) != 9 {
|
|
t.Errorf("moved %d paths, want 9: %+v", len(s.Moved), s.Moved)
|
|
}
|
|
if raw, _ := os.ReadFile(filepath.Join(dir, "config")); !strings.Contains(string(raw), "Host mine\n User me\nInclude ~/.ssh/config.d/mesh\n") {
|
|
t.Errorf("the operator's lines in config changed: %q", raw)
|
|
}
|
|
if st, _ := os.Stat(filepath.Join(c.Home, RetiredDir)); st == nil || st.Mode().Perm() != 0o700 {
|
|
t.Errorf("the retired folder is not 0700: %v", st)
|
|
}
|
|
|
|
again, err := c.Sweep(now.Add(time.Minute))
|
|
if err != nil || len(again.Moved) != 0 {
|
|
t.Fatalf("a second sweep moved %+v (err %v)", again.Moved, err)
|
|
}
|
|
if exists(filepath.Join(c.Home, RetiredDir, "20261017T120100Z")) {
|
|
t.Error("a sweep that moved nothing made a folder")
|
|
}
|
|
|
|
retired, err := c.Retired()
|
|
if err != nil || len(retired) != 1 || retired[0].At != "20261017T120000Z" || len(retired[0].Moved) != 9 {
|
|
raw, _ := json.Marshal(retired)
|
|
t.Fatalf("Retired = %s (err %v), want one sweep of 9 paths", raw, err)
|
|
}
|
|
for _, m := range retired[0].Moved {
|
|
if !strings.HasPrefix(m.From, dir+"/") || !strings.HasPrefix(m.To, root+"/") || !exists(m.To) {
|
|
t.Errorf("listed %+v, not a path moved from ~/.ssh into %s", m, root)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSweepOnceSaysEachMove(t *testing.T) {
|
|
c, dir := sweepHome(t)
|
|
var said []string
|
|
c.SweepOnce(time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC), func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) })
|
|
want := "moved " + filepath.Join(dir, "known_hosts.old") + ", which this module does not know, to " +
|
|
filepath.Join(c.Home, RetiredDir, "20261017T120000Z", "known_hosts.old")
|
|
if !slices.Contains(said, want) || len(said) != 9 {
|
|
t.Fatalf("said %q, want 9 lines with %q", said, want)
|
|
}
|
|
}
|
|
|
|
func TestSweepLeavesSocketsAndUnreadable(t *testing.T) {
|
|
c, dir := sweepHome(t)
|
|
sock := filepath.Join(dir, "cm-ace")
|
|
if l, err := net.Listen("unix", sock); err == nil {
|
|
defer l.Close()
|
|
} else {
|
|
t.Skipf("no unix socket here: %v", err)
|
|
}
|
|
locked := filepath.Join(dir, "unreadable")
|
|
os.WriteFile(locked, []byte("?"), 0o000)
|
|
if f, err := os.Open(locked); err == nil {
|
|
f.Close()
|
|
t.Skip("running as a user who reads a mode-0 file")
|
|
}
|
|
s, err := c.Sweep(time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !exists(sock) {
|
|
t.Error("a socket was moved")
|
|
}
|
|
if !exists(locked) || len(s.Left) != 1 || s.Left[0] != locked {
|
|
t.Errorf("an unreadable file was not left and named: left %v", s.Left)
|
|
}
|
|
}
|
|
|
|
func TestSweepWithoutSSHDir(t *testing.T) {
|
|
c := &Client{Home: t.TempDir()}
|
|
if s, err := c.Sweep(time.Now()); err != nil || len(s.Moved) != 0 {
|
|
t.Fatalf("Sweep = %+v, %v", s, err)
|
|
}
|
|
}
|
|
|
|
// Every path the manifest declares under ~/.ssh is one the sweep knows.
|
|
func TestDeclaredMatchesManifest(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Resources []struct{ Path string } `json:"resources"`
|
|
Facts map[string]struct{ Path string }
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
under := regexp.MustCompile(`^(\$\{machine:account-home\}/)?\.ssh/(.+)$`)
|
|
paths := []string{}
|
|
for _, r := range m.Resources {
|
|
paths = append(paths, r.Path)
|
|
}
|
|
for _, f := range m.Facts {
|
|
paths = append(paths, f.Path)
|
|
}
|
|
seen := 0
|
|
for _, p := range paths {
|
|
g := under.FindStringSubmatch(strings.TrimSuffix(p, "/"))
|
|
if g == nil {
|
|
continue
|
|
}
|
|
seen++
|
|
if !Declared[g[2]] {
|
|
t.Errorf("module.json declares ~/.ssh/%s, which the sweep does not know: it would be moved out", g[2])
|
|
}
|
|
}
|
|
if seen < 5 {
|
|
t.Errorf("found %d paths under ~/.ssh in module.json; the manifest's shape changed", seen)
|
|
}
|
|
}
|
|
|
|
// Review round 1: a folder holding only a socket is gone into, not moved with the socket.
|
|
func TestSweepKeepsAFolderHoldingASocket(t *testing.T) {
|
|
home := t.TempDir()
|
|
dir := filepath.Join(home, ".ssh", "cm")
|
|
os.MkdirAll(dir, 0o700)
|
|
l, err := net.Listen("unix", filepath.Join(dir, "s"))
|
|
if err != nil {
|
|
t.Skipf("no unix socket here: %v", err)
|
|
}
|
|
defer l.Close()
|
|
c := &Client{Home: home}
|
|
if s, err := c.Sweep(time.Now()); err != nil || len(s.Moved) != 0 || !exists(filepath.Join(dir, "s")) {
|
|
t.Fatalf("moved %+v (err %v); the socket's folder must stay", s.Moved, err)
|
|
}
|
|
}
|
|
|
|
// Review round 1: two sweeps in the same second never share a folder, so neither overwrites the other.
|
|
func TestSweepsInOneSecondKeepBoth(t *testing.T) {
|
|
home := t.TempDir()
|
|
dir := filepath.Join(home, ".ssh")
|
|
os.MkdirAll(dir, 0o700)
|
|
c := &Client{Home: home}
|
|
now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC)
|
|
os.WriteFile(filepath.Join(dir, "a"), []byte("1"), 0o600)
|
|
first, err := c.Sweep(now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.WriteFile(filepath.Join(dir, "a"), []byte("2"), 0o600)
|
|
second, err := c.Sweep(now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.At == second.At {
|
|
t.Fatalf("both sweeps used %s", first.At)
|
|
}
|
|
for at, want := range map[string]string{first.At: "1", second.At: "2"} {
|
|
if raw, _ := os.ReadFile(filepath.Join(home, RetiredDir, at, "a")); string(raw) != want {
|
|
t.Errorf("%s/a = %q, want %q", at, raw, want)
|
|
}
|
|
}
|
|
if r, _ := c.Retired(); len(r) != 2 {
|
|
t.Errorf("Retired lists %d sweeps, want 2", len(r))
|
|
}
|
|
}
|
|
|
|
// Review round 1: a link to a pipe below ~/.ssh never blocks the sweep.
|
|
func TestSweepDoesNotOpenAPipe(t *testing.T) {
|
|
home := t.TempDir()
|
|
sub := filepath.Join(home, ".ssh", "sub")
|
|
os.MkdirAll(sub, 0o700)
|
|
fifo := filepath.Join(home, "fifo")
|
|
if err := syscall.Mkfifo(fifo, 0o600); err != nil {
|
|
t.Skipf("no fifo here: %v", err)
|
|
}
|
|
os.Symlink(fifo, filepath.Join(sub, "p"))
|
|
done := make(chan error, 1)
|
|
go func() { _, err := (&Client{Home: home}).Sweep(time.Now()); done <- err }()
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("the sweep blocked on a pipe")
|
|
}
|
|
}
|