//go:build linux package web import ( "context" "os" "os/exec" "strings" "syscall" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/system" ) // platformNetProbeFSClass is the real namespace classifier (statfs f_type) the probe child runs. // Unbounded on purpose: the child's whole run is already bounded by the parent's netProbeTimeout. func platformNetProbeFSClass(dir string) string { return system.ClassifyPathFS(dir) } // netProbeTimeout bounds one probe run (LAN write+readback is sub-second; a wedged share must not // hold the orchestrator — the NFS soft/retry=0 options error out well inside this). const netProbeTimeout = 30 * time.Second // runNetProbe re-execs this binary as `felhom-controller --netprobe ` with uid/gid 1000 // credentials (supplementary groups CLEARED — the probe must see exactly what a media app sees) // and maps the exit code to a verdict. This is the production netProbeFn seam value. func runNetProbe(ctx context.Context, dir string) probeOutcome { exe, err := os.Executable() if err != nil { return probeOutcome{OK: false, Category: "probe_io", Detail: "probe re-exec: executable path: " + err.Error()} } pctx, cancel := context.WithTimeout(ctx, netProbeTimeout) defer cancel() cmd := exec.CommandContext(pctx, exe, "--netprobe", dir) cmd.SysProcAttr = &syscall.SysProcAttr{ Credential: &syscall.Credential{Uid: 1000, Gid: 1000, Groups: []uint32{}}, } out, err := cmd.CombinedOutput() output := strings.TrimSpace(string(out)) if err != nil { if ee, ok := err.(*exec.ExitError); ok { return netProbeVerdict(ee.ExitCode(), output) } // Spawn-level failure (EPERM on setuid = capability dropped somewhere — spike Q2 proved the // default container HAS the caps, so this is a real config regression worth the raw detail). return probeOutcome{OK: false, Category: "probe_io", Detail: "probe spawn failed: " + err.Error() + " | " + output} } return netProbeVerdict(0, output) }