package host import ( "bufio" "bytes" "crypto/ed25519" "crypto/rand" "encoding/base64" "fmt" "os" "path/filepath" "slices" "strings" "testing" "github.com/carlosframework/platform/internal/dbreads" ) func TestDBReadsSettingsAreSpec71(t *testing.T) { t.Parallel() pub, _, _ := ed25519.GenerateKey(rand.Reader) good := map[string]string{ "CARLOS_HIBERNATE_BUCKET": "data-uk", "CARLOS_REGION": "uk", "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(pub), } env := func(over map[string]string) func(string) string { return func(k string) string { if v, ok := over[k]; ok { return v } return good[k] } } got, why := dbReadsSettings(env(nil)) if why != "" || got.region != "uk" || got.maxRestored != dbreads.DefaultMaxRestored { t.Fatalf("a capable box: %+v %q", got, why) } if got, why := dbReadsSettings(env(map[string]string{"CARLOS_DB_READ_MAX_BYTES": "1073741824"})); why != "" || got.maxRestored != 1<<30 { t.Fatalf("the size cap: %+v %q", got, why) } for name, over := range map[string]map[string]string{ "no hibernation": {"CARLOS_HIBERNATE_BUCKET": ""}, "no region": {"CARLOS_REGION": ""}, "a fleet placement": {"CARLOS_REGION": "fleet/acme"}, "no release key": {"CARLOS_RELEASE_PUBKEY": ""}, "switched off": {"CARLOS_DB_READS": "0"}, "host claims": {"CARLOS_HOST_CLAIM": "1"}, "a cap that is not a number": {"CARLOS_DB_READ_MAX_BYTES": "2GB"}, } { if _, why := dbReadsSettings(env(over)); why == "" { t.Errorf("%s: armed", name) } } } func TestCountDBReadUnits(t *testing.T) { t.Parallel() out := strings.Join([]string{ "carlos-dbread@0-1234-999.service loaded active running carlos db-read worker", "carlos-dbread@1-1235-999.service loaded activating start carlos db-read worker", "carlos-dbread@2-1236-999.service loaded deactivating stop-sigterm carlos db-read worker", "carlos-dbread@3-1237-999.service loaded failed failed carlos db-read worker", "carlos-dbread@4-1238-999.service loaded inactive dead carlos db-read worker", "", }, "\n") if n := countDBReadUnits(out); n != 3 { t.Fatalf("counted %d units, want active, activating and deactivating: 3", n) } } func TestWithDBReadCapability(t *testing.T) { t.Parallel() base := []string{"region-ha-1"} if got := withDBReadCapability(base, false); !slices.Equal(got, base) { t.Fatalf("incapable: %v", got) } if got := withDBReadCapability(base, true); !slices.Equal(got, []string{"region-ha-1", dbreads.Capability}) || len(base) != 1 { t.Fatalf("capable: %v (base %v)", got, base) } } func unitLines(t *testing.T, name string) []string { t.Helper() b, err := unitsFS.ReadFile("units/" + name) if err != nil { t.Fatal(err) } var lines []string sc := bufio.NewScanner(bytes.NewReader(b)) for sc.Scan() { if l := strings.TrimSpace(sc.Text()); l != "" && !strings.HasPrefix(l, "#") { lines = append(lines, l) } } return lines } // The sandbox is the unit file (spec §8.3): every line the spec names is // there, and nothing loosens it. func TestDBReadUnitsCarryTheSandbox(t *testing.T) { t.Parallel() sock := unitLines(t, "carlos-dbread.socket") // The socket lives under /run/carlos, which every tenant unit replaces // with an empty TemporaryFileSystem: tenants run as carlos, the uid the // worker serves, so a path they could see would let them start workers // directly (Astra round 1, finding 1). if !strings.HasPrefix(dbReadSocketPath, "/run/carlos/") { t.Errorf("the worker socket %s is outside the tree tenant units hide", dbReadSocketPath) } for _, tenant := range []string{"carlos-instance@.service", "carlos-instance-cmd@.service"} { if !slices.Contains(unitLines(t, tenant), "TemporaryFileSystem=/run/carlos:ro") { t.Errorf("%s no longer hides /run/carlos", tenant) } } for _, want := range []string{"ListenStream=" + dbReadSocketPath, "SocketUser=carlos", "SocketMode=0600", "Accept=yes", "MaxConnections=3"} { if !slices.Contains(sock, want) { t.Errorf("the socket lacks %q", want) } } svc := unitLines(t, "carlos-dbread@.service") for _, want := range []string{ "ExecStart=/opt/carlos/carlos system db-worker", "StandardInput=socket", "StandardOutput=socket", "StandardError=journal", "DynamicUser=yes", "PrivateNetwork=yes", "RestrictAddressFamilies=AF_UNIX", "TemporaryFileSystem=/data:ro /var/lib:ro /opt/carlos:ro /run/carlos:ro", "BindReadOnlyPaths=/opt/carlos/carlos", "InaccessiblePaths=/etc/carlos -/run/carlos-control", "PrivateTmp=yes", "MemoryMax=512M", "CPUQuota=100%", "TasksMax=16", "LimitFSIZE=2147483648", "RuntimeMaxSec=20min", "SystemCallFilter=@system-service", "NoNewPrivileges=yes", "CapabilityBoundingSet=", "ProtectProc=invisible", "ProtectSystem=strict", } { if !slices.Contains(svc, want) { t.Errorf("the service lacks %q", want) } } for _, l := range svc { for _, bad := range []string{"User=", "StandardOutput=file:", "IPAddressAllow=", "ReadWritePaths=", "AmbientCapabilities=CAP"} { if strings.HasPrefix(l, bad) { t.Errorf("the service loosens its sandbox: %q", l) } } } } // dbReadsHostEnv writes etc/carlos/host.env with the settings that arm the // db-reads gate (spec §7.1), plus extra lines. func dbReadsHostEnv(t *testing.T, etc string, extra ...string) { t.Helper() pub, _, _ := ed25519.GenerateKey(rand.Reader) lines := append([]string{"CARLOS_HIBERNATE_BUCKET=data-uk", "CARLOS_REGION=uk", "CARLOS_RELEASE_PUBKEY=" + base64.StdEncoding.EncodeToString(pub)}, extra...) if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil { t.Fatal(err) } } func TestEnsureSystemUnitsInstallsTheDBReadUnits(t *testing.T) { t.Parallel() etc := t.TempDir() os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755) dbReadsHostEnv(t, etc) var calls []string systemctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } probe := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) if args[0] == "is-active" { return "inactive\n", systemctlExit(3) } return "disabled\n", systemctlExit(1) } ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), systemctl, probe, t.Logf) for _, u := range []string{"carlos-dbread.socket", "carlos-dbread@.service"} { got, err := os.ReadFile(filepath.Join(etc, "systemd", "system", u)) want, _ := unitsFS.ReadFile("units/" + u) if err != nil || !bytes.Equal(got, want) { t.Errorf("%s not installed as embedded: %v", u, err) } } joined := strings.Join(calls, "\n") for _, want := range []string{"is-active carlos-dbread.socket", "carlos-dbread.socket"} { if !strings.Contains(joined, want) { t.Errorf("calls lack %q:\n%s", want, joined) } } // systemctlPlan starts every armed unit (sockets, paths, timers, mounts) // in the order passed (bootstrap.go armedUnits), and the service // template never. if !strings.Contains(joined, "start carlos-system-update.path carlos-instance-control.socket carlos-dbread.socket") { t.Errorf("the socket was not armed:\n%s", joined) } if strings.Contains(joined, "start carlos-dbread@") { t.Fatal("a worker service was started") } } // The socket follows the capability's dark gate (Astra round 1, finding 1): // a box whose host.env does not arm db reads (no hibernation, CARLOS_DB_READS=0, // host claims, no release key, or no host.env) has no listening worker // socket. The adopt pass never arms it there, and stops and disables one // left armed. func TestEnsureSystemUnitsKeepsTheDBReadSocketDarkWithoutTheGate(t *testing.T) { t.Parallel() for name, env := range map[string][]string{ "no host.env": nil, "CARLOS_DB_READS=0": {"CARLOS_DB_READS=0"}, "host claims": {"CARLOS_HOST_CLAIM=1"}, "no hibernation": {"CARLOS_HIBERNATE_BUCKET="}, } { for _, armed := range []bool{false, true} { t.Run(fmt.Sprintf("%s/armed=%v", name, armed), func(t *testing.T) { t.Parallel() etc := t.TempDir() os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755) if env != nil { dbReadsHostEnv(t, etc, env...) } var calls []string systemctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } probe := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) on := armed && args[1] == dbReadSocketUnit switch { case args[0] == "is-active" && on: return "active\n", nil case args[0] == "is-active": return "inactive\n", systemctlExit(3) case on: return "enabled\n", nil } return "disabled\n", systemctlExit(1) } ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), systemctl, probe, t.Logf) joined := strings.Join(calls, "\n") for _, c := range calls { if (strings.HasPrefix(c, "start ") || strings.HasPrefix(c, "enable ")) && strings.Contains(c, dbReadSocketUnit) { t.Fatalf("the dark socket was armed: %q\n%s", c, joined) } } if disabled := strings.Contains(joined, "disable --now "+dbReadSocketUnit); disabled != armed { t.Fatalf("armed %v, disabled %v:\n%s", armed, disabled, joined) } }) } } } // Review Focus 5: the pull's cap can only be lowered on a box. Mutation: // dbReadsSettings keeps a CARLOS_DB_PULL_MAX_BYTES above the cap (red). func TestPullCapOnlyLowers(t *testing.T) { t.Parallel() pub, _, _ := ed25519.GenerateKey(rand.Reader) base := map[string]string{"CARLOS_HIBERNATE_BUCKET": "d", "CARLOS_REGION": "uk", "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(pub)} get := func(v string) func(string) string { return func(k string) string { if k == "CARLOS_DB_PULL_MAX_BYTES" { return v } return base[k] } } if env, why := dbReadsSettings(get("")); why != "" || env.maxPull != dbreads.MaxPullBytes { t.Fatalf("default %d %q", env.maxPull, why) } if env, why := dbReadsSettings(get("1048576")); why != "" || env.maxPull != 1<<20 { t.Fatalf("lowered %d %q", env.maxPull, why) } if env, why := dbReadsSettings(get("99999999999999")); why != "" || env.maxPull != dbreads.MaxPullBytes { t.Fatalf("raised past the built-in cap: %d %q", env.maxPull, why) } for _, bad := range []string{"0", "-5", "2GB"} { if _, why := dbReadsSettings(get(bad)); why == "" { t.Fatalf("CARLOS_DB_PULL_MAX_BYTES=%s armed the loop", bad) } } }