server: DF-mode big_send + uploaded-run storage with an operator policy

big_send now forces the Don't-Fragment bit for the whole burst by default, so
the largest size that arrives IS the downstream path MTU rather than "fragments
got through" — two different measurements the schema already separates. Sizes
above our own egress MTU (from the startup self-test) are refused up front and
reported as max_df_bytes, because absence caused by our kernel must not be read
as a limit of the client's path.

Uploads: one JSON file per run under the state dir, with the policy the operator
actually cares about — who may upload (off / anonymous / account), how large,
how long to keep, and the least anonymization accepted. The profile advertises
all of it so the app can present the switch honestly instead of discovering the
rules by failing. `account` refuses today rather than falling back to anonymous:
picking the strict setting before OIDC lands must not silently mean the loose one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-01 10:26:19 +02:00
co-authored by Claude Fable 5
parent 7e1015c211
commit 2521d39989
19 changed files with 1172 additions and 31 deletions
+197
View File
@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2026 Echolot contributors
// SPDX-License-Identifier: GPL-3.0-or-later
package runs
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func doc(id, anon string) []byte {
return []byte(fmt.Sprintf(
`{"run":{"id":%q,"started_at":"2026-08-01T10:00:00Z","privacy":{"anonymization":%q}},`+
`"findings":[{"id":"f1"},{"id":"f2"}],"summary":{"verdict":"warn"}}`, id, anon))
}
func open(t *testing.T, p Policy) (*Store, string) {
t.Helper()
dir := t.TempDir()
s, err := Open(dir, p)
if err != nil {
t.Fatalf("open: %v", err)
}
return s, dir
}
func TestModeOffRefusesEverything(t *testing.T) {
p := DefaultPolicy()
p.Mode = ModeOff
s, _ := open(t, p)
if _, err := s.Put("dev1", doc("run-1", AnonFull)); !errors.Is(err, ErrDisabled) {
t.Fatalf("want ErrDisabled, got %v", err)
}
}
// ModeAccount must refuse today rather than fall back to anonymous: an operator who selects the
// strict setting before accounts exist must not be silently running the permissive one.
func TestModeAccountRefusesUntilAccountsExist(t *testing.T) {
p := DefaultPolicy()
p.Mode = ModeAccount
s, _ := open(t, p)
if _, err := s.Put("dev1", doc("run-1", AnonFull)); !errors.Is(err, ErrNeedAccount) {
t.Fatalf("want ErrNeedAccount, got %v", err)
}
}
func TestMinAnonymizationEnforced(t *testing.T) {
p := DefaultPolicy()
p.MinAnonymization = AnonBalanced
s, _ := open(t, p)
if _, err := s.Put("dev1", doc("run-full", AnonFull)); !errors.Is(err, ErrNotAnonEnough) {
t.Fatalf("full should be refused when balanced is required, got %v", err)
}
// An undeclared level means nothing was stripped, so it must be treated as "full".
if _, err := s.Put("dev1", []byte(`{"run":{"id":"run-bare"},"summary":{}}`)); !errors.Is(err, ErrNotAnonEnough) {
t.Fatalf("undeclared level should be treated as full, got %v", err)
}
for _, lvl := range []string{AnonBalanced, AnonStrict} {
if _, err := s.Put("dev1", doc("run-"+lvl, lvl)); err != nil {
t.Fatalf("%s should be accepted: %v", lvl, err)
}
}
}
func TestSizeLimit(t *testing.T) {
p := DefaultPolicy()
p.MaxBytes = 200
s, _ := open(t, p)
big := append(doc("run-1", AnonFull), make([]byte, 400)...)
if _, err := s.Put("dev1", big); !errors.Is(err, ErrTooLarge) {
t.Fatalf("want ErrTooLarge, got %v", err)
}
}
func TestRetentionByCountKeepsNewest(t *testing.T) {
p := DefaultPolicy()
p.MaxRunsPerDevice = 3
s, _ := open(t, p)
for i := 0; i < 6; i++ {
if _, err := s.Put("dev1", doc(fmt.Sprintf("run-%d", i), AnonFull)); err != nil {
t.Fatalf("put %d: %v", i, err)
}
time.Sleep(2 * time.Millisecond) // distinct UploadedAt so "newest" is well defined
}
got := s.List("dev1")
if len(got) != 3 {
t.Fatalf("kept %d runs, want 3", len(got))
}
for i, want := range []string{"run-5", "run-4", "run-3"} {
if got[i].ID != want {
t.Fatalf("kept[%d] = %s, want %s (newest first)", i, got[i].ID, want)
}
}
// The documents themselves must be gone too, not just their index entries.
if _, err := s.Get("dev1", "run-0"); err == nil {
t.Fatal("purged run is still readable")
}
}
func TestRetentionByAge(t *testing.T) {
p := DefaultPolicy()
p.RetentionDays = 7
p.MaxRunsPerDevice = 0
s, dir := open(t, p)
if _, err := s.Put("dev1", doc("run-old", AnonFull)); err != nil {
t.Fatal(err)
}
// Backdate the index entry past the retention window.
metaPath := filepath.Join(dir, "runs", "dev1", "run-old.meta.json")
b, _ := os.ReadFile(metaPath)
var m Meta
_ = json.Unmarshal(b, &m)
m.UploadedAt = time.Now().Add(-30 * 24 * time.Hour)
nb, _ := json.Marshal(m)
if err := os.WriteFile(metaPath, nb, 0o600); err != nil {
t.Fatal(err)
}
if _, err := s.Put("dev1", doc("run-new", AnonFull)); err != nil {
t.Fatal(err)
}
got := s.List("dev1")
if len(got) != 1 || got[0].ID != "run-new" {
t.Fatalf("age retention did not drop the old run: %+v", got)
}
}
// Run and device ids reach the filesystem, so a hostile one must not be able to climb out of the
// store directory or overwrite another device's data.
func TestIDsCannotEscapeTheStoreDirectory(t *testing.T) {
s, dir := open(t, DefaultPolicy())
if _, err := s.Put("../../etc", doc("../../../passwd", AnonFull)); err != nil {
t.Fatalf("put: %v", err)
}
var found []string
_ = filepath.Walk(dir, func(p string, info os.FileInfo, err error) error {
if err == nil && !info.IsDir() {
rel, _ := filepath.Rel(dir, p)
found = append(found, filepath.ToSlash(rel))
}
return nil
})
for _, f := range found {
if strings.Contains(f, "..") {
t.Fatalf("path escaped the store: %s", f)
}
}
if len(found) == 0 {
t.Fatal("nothing written at all")
}
}
func TestListIsPerDevice(t *testing.T) {
s, _ := open(t, DefaultPolicy())
if _, err := s.Put("devA", doc("run-a", AnonFull)); err != nil {
t.Fatal(err)
}
if _, err := s.Put("devB", doc("run-b", AnonFull)); err != nil {
t.Fatal(err)
}
if got := s.List("devA"); len(got) != 1 || got[0].ID != "run-a" {
t.Fatalf("devA sees %+v", got)
}
if _, err := s.Get("devA", "run-b"); err == nil {
t.Fatal("devA could read devB's run")
}
}
func TestMetaSummarisesTheDocument(t *testing.T) {
s, _ := open(t, DefaultPolicy())
m, err := s.Put("dev1", doc("run-1", AnonBalanced))
if err != nil {
t.Fatal(err)
}
if m.FindingCount != 2 || m.Verdict != "warn" || m.Anonymization != AnonBalanced {
t.Fatalf("meta not extracted: %+v", m)
}
if m.StartedAt != "2026-08-01T10:00:00Z" {
t.Fatalf("started_at = %q", m.StartedAt)
}
}
func TestMalformedRejected(t *testing.T) {
s, _ := open(t, DefaultPolicy())
for _, body := range [][]byte{[]byte("not json"), []byte(`{"run":{}}`), []byte(`{}`)} {
if _, err := s.Put("dev1", body); !errors.Is(err, ErrMalformed) {
t.Fatalf("body %q: want ErrMalformed, got %v", body, err)
}
}
}