bb8737a81f
Controller half of the verify pipeline (SPIKE-nas-verify b57f6c1): AddNetStorage gains verify/job fields + typed NetAddRefusedError; NetVerifyStatus polls the agent slot; --netprobe hidden re-exec mode (SysProcAttr.Credential uid/gid 1000, no shell) proves in-guest writability; the add handler starts a detached single-flight job (agent_add → verifying → probing → registering LAST) with full rollback on any failure incl. verify-lost-after-restart (Scenario F); §3.2 Hungarian error map server-side; live-but-unregistered shares surface as remove- only 'Árva megosztás' rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
82 lines
3.4 KiB
Go
82 lines
3.4 KiB
Go
package web
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// In-guest uid-1000 write probe (NAS verify-before-commit, SPIKE-nas-verify Q2/Q3). The agent's
|
|
// verify proves the MOUNT is real; this probe proves a uid/gid-1000 app can actually WRITE through
|
|
// it — the squash trap (Q3 row c: an export that mounts fine but denies every write). The probe is
|
|
// a RE-EXEC of this binary (`felhom-controller --netprobe <dir>`) dropped to uid/gid 1000 via
|
|
// SysProcAttr.Credential (spike Q2: SETUID/SETGID caps present in the container) — no shell, no
|
|
// in-process setuid (which would poison the whole Go runtime's credentials).
|
|
|
|
// Probe child exit codes (the parent maps them to verify categories).
|
|
const (
|
|
netProbeExitOK = 0 // write + readback + delete all fine
|
|
netProbeExitNoWrite = 2 // create/write failed → not_writable (the squash trap)
|
|
netProbeExitMismatch = 3 // readback failed or differed → probe_io
|
|
netProbeExitCleanup = 4 // wrote fine but delete failed → OK + warn (never a failure)
|
|
)
|
|
|
|
// netProbeReadBack is the child's readback seam (package var — the child is a re-exec'd process in
|
|
// production, so a struct seam can't reach it; tests override in-process).
|
|
var netProbeReadBack = os.ReadFile
|
|
|
|
// NetProbeChild is the --netprobe body, run AS uid/gid 1000 by the re-exec parent: create a
|
|
// dot-file with a random name + nonce in dir, read it back, compare, remove. Pure file logic —
|
|
// unit-tested directly in t.TempDir(). Exposed for cmd/controller's hidden mode.
|
|
func NetProbeChild(dir string) int {
|
|
name := filepath.Join(dir, ".felhom-proba-"+randHexToken(8))
|
|
nonce := randHexToken(32)
|
|
if err := os.WriteFile(name, []byte(nonce), 0o644); err != nil {
|
|
return netProbeExitNoWrite
|
|
}
|
|
back, err := netProbeReadBack(name)
|
|
if err != nil || string(back) != nonce {
|
|
_ = os.Remove(name) // best-effort — the verdict is already mismatch
|
|
return netProbeExitMismatch
|
|
}
|
|
if err := os.Remove(name); err != nil {
|
|
return netProbeExitCleanup
|
|
}
|
|
return netProbeExitOK
|
|
}
|
|
|
|
// probeOutcome is the parent-side verdict of one probe run.
|
|
type probeOutcome struct {
|
|
OK bool
|
|
Category string // failure category (not_writable | probe_io) when !OK
|
|
Detail string
|
|
Warn string // set on OK when cleanup failed (§8: a failed delete is a WARN, not a failure)
|
|
}
|
|
|
|
// netProbeVerdict maps the child's exit code to the outcome (pure — unit-tested).
|
|
func netProbeVerdict(exitCode int, output string) probeOutcome {
|
|
switch exitCode {
|
|
case netProbeExitOK:
|
|
return probeOutcome{OK: true}
|
|
case netProbeExitCleanup:
|
|
return probeOutcome{OK: true, Warn: "a próbafájl törlése nem sikerült a megosztáson"}
|
|
case netProbeExitNoWrite:
|
|
return probeOutcome{OK: false, Category: "not_writable", Detail: "uid-1000 write probe: create/write refused | " + output}
|
|
case netProbeExitMismatch:
|
|
return probeOutcome{OK: false, Category: "probe_io", Detail: "uid-1000 write probe: readback failed or differed | " + output}
|
|
default:
|
|
return probeOutcome{OK: false, Category: "probe_io", Detail: "uid-1000 write probe: unexpected exit | " + output}
|
|
}
|
|
}
|
|
|
|
// randHexToken returns n random bytes hex-encoded (2n chars); crypto/rand, panics never — a rand
|
|
// failure degrades to a constant (the probe still functions, names just stop being random).
|
|
func randHexToken(n int) string {
|
|
b := make([]byte, n)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "felhom-static"
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|