Records the R-7 slice 1 ship, the four red-proof outcomes, live-validation evidence, the two bugs live validation caught, and the Part-4 Step-1 enumeration finding + the reported design fork (share data classified but not yet in a live backup run — needs a Viktor ruling, suggested R-7b).
11 KiB
REPORT — most recent implementation
felhom-controller v0.144.0 — „Megosztás": LAN SMB file sharing (R-7 slice 1) — 2026-07-18
Class: Implementation. Shipped: controller v0.144.0 + new infra image
gitea.dooplex.hu/admin/felhom-samba:1.0.0. Deployed + live-validated on demo guest 9201.
1. Baselines
| Repo | start main @ |
Version |
|---|---|---|
| felhom-controller | ac13966 (v0.143.0) |
→ v0.144.0 |
| felhom.eu | 7e0370f |
docs only (hub v0.66.0 / scripts v1.20.0 untouched) |
| felhom-agent | f222a7b |
NOT touched (v0.90.0) |
| app-catalog-felhom.eu | — | NOT touched (deliberately not a catalog app) |
(The brief listed a4a7de3d for the controller; the live baseline was ac13966 — the delta is the
2026-07-18 build-root relocation, paths/docs only.)
2. Files created / modified
Created: controller/infra-images/samba/{Dockerfile,entrypoint.sh},
controller/scripts/build-samba-image.sh, internal/settings/smb.go (+test),
internal/infra/samba.go (+test), internal/stacks/samba.go (+test),
internal/stacks/samba_classify.go (+test), internal/web/sharing_handlers.go (+test),
internal/web/templates/sharing.html.
Modified: settings/settings.go (2 fields), infra/infra.go (SambaImage pin),
stacks/infra.go, stacks/manager.go (3 test seams), stacks/metadata.go (samba branch),
config/config.go (alwaysProtectedStacks), web/server.go, web/inframeta.go,
cmd/controller/main.go (/api/sharing/ mux), templates/layout.html, templates/icons.html,
CHANGELOG.md.
3. Commits
| Hash | Phase |
|---|---|
f42f3e0 |
Part 0 — felhom-samba image + build helper |
b0c5ef4 |
Part 1 — settings registry + renderers |
0dcbea9 |
Part 2 — lifecycle (ensure/reconcile/password/disable) + protection |
1d26a69 |
Part 4 — backup classification from the shares registry |
4f08e5e |
Part 3 — „Megosztás" page + guarded picker |
2eef9b2 |
fix: /api/sharing/ mux registration (found by live validation) |
b409f5e |
fix: storage-ROOT vs share-TARGET validation split (found by live validation) |
4. Part-4 Step-1 enumeration finding (verbatim, as required)
GetStackClassifiedBinds is NOT inert — it is consumed by backup/offbox_capture.go:36
(offsite), backup/tier2_capture.go:45 (tier-2), appexport/fabplan.go:33,115 (.fab). The
enumeration nevertheless never reaches a share-only stack:
- Tier-2 —
RunAllTier2(tier2.go:399) iteratesListDeployedStacks()and does not skip protected stacks. ButRunTier2(tier2.go:269) resolvesGetAppDrivePath(stack)thenos.Stat(RecoveryUnitPath(...))and returns nil beforetier2CaptureSetis ever called when no recovery unit exists. samba has neither an app drive path nor a recovery unit. - Offsite —
offbox.go:600enumeratessettings.GetOffboxApps()(apps withAppBackup[name].Offbox); samba is not an app-backup app. - Volume dumps —
backup.go:437–439iteratesListDeployedStacks()andcontinues oncfg.IsProtectedStack(name); with samba protected this correctly skips it.
Verdict — design fork STOPPED and reported, not improvised (per the task's Part-4 clause): putting share data into a live tier-2/offsite run requires teaching both engines about a stack with no recovery unit and no single app drive path — a structural change inside the engines, far beyond an enumeration tweak. Slice 1 therefore delivers the correct classification seam only.
Consequence, stated plainly: [R4]'s intent — a customer dropping family files onto \\FELHOM gets
the product backup promise — is classified but not executed. Share data is in no live backup run
today. Needs a Viktor ruling (suggested new item R-7b).
5. Tests
Test functions 651 → 669 (+18). Green gate (go build ./... && go vet ./... && go test ./...) run
after every phase: 23/23 packages ok, zero failures. Template gates green
(template_id_gate.py, emoji_gate.py).
| Scenario | Tests | Result |
|---|---|---|
| A — enable + first share | TestSambaReconcile_HappyPath, TestRenderSambaConfig_Golden, settings CRUD |
PASS |
| B — read-only share | TestRenderSambaCompose (read only = yes + :ro bind) |
PASS |
| C — security gates | TestSharingResolvePath_{GuardMatrix,SymlinkEscapeRefused,DecommissionedRootRefused,UniformRefusal}, TestSharingResolveStorageRoot, TestPathWithin_*, name validation |
PASS (symlink case SKIPs on Windows — no privilege) |
| D — classification | TestSambaClassifiedBinds_{TierMembership,ExcludesConfigMounts,NoShares} |
PASS |
| E — delete/disable keep data | TestSambaShareDeleteAndDisableKeepData (byte-identical tree) |
PASS |
| Idempotency | TestSambaReconcile_IdempotentNoComposeCall (seam asserts 0 compose calls) |
PASS |
| Secrets | TestSambaPasswordNeverPersisted, TestSambaPasswordRefusedWhenDisabled |
PASS |
Red-proofs — all four run, confirmed, restored (git diff clean each time)
- Classification — inverted Offsite→mandatory →
TestSambaClassifiedBinds_TierMembershipFAILED:Felhőmentés ON must be mandatory, got "optional"+mandatory share must be in the OFFSITE set: []. :robind — dropped the read-only bind mapping →TestRenderSambaComposeFAILED:read-only share must get a :ro bind, missing "- …/filmek:…/filmek:ro".- Picker guard — removed the containment assert in
sharingOwningRoot→TestSharingResolvePath_GuardMatrixFAILED:outside every registered root: must be refused, got "…\Temp\…\002". - Idempotency — removed config change-detection →
TestSambaReconcile_IdempotentNoComposeCallFAILED:unchanged registry must perform NO compose call: calls went 1 → 2.
Password-leak check (rule 4)
TestSambaPasswordNeverPersisted asserts the secret reaches the smbpasswd seam and appears in none
of settings.json, smb.conf, docker-compose.yml. Live: the demo settings.json carries only
"user_set": true. The password is STDIN-only (never argv) and never logged.
6. Deployment (live)
felhom-controller: gitea.dooplex.hu/admin/felhom-controller:0.144.0 Up (healthy)
felhom-samba: gitea.dooplex.hu/admin/felhom-samba:1.0.0 Up
Anonymous pull of the samba image verified from guest 9201 without registry creds (Part-0 gate).
Binds (docker inspect felhom-samba) — Scenario B proof:
/opt/docker/stacks/samba/smb.conf -> /etc/samba/smb.conf rw=false
/mnt/felhom-drives/hdd_1/media/filmek -> …/media/filmek rw=false (read-only share)
/mnt/felhom-drives/hdd_1/shares/dokumentumok -> …/shares/dokumentumok rw=true
samba-passdb volume -> /var/lib/samba rw=true
Listeners LAN-only (docker 172.x bridges absent — bind interfaces only working):
445 on 127.0.0.1 + 192.168.0.127, 137/138 NetBIOS, 3702 WSD, 5357.
7. Live validation
Method: the UI is password-gated and no browser leg was used, so validation drove the exact
endpoints the UI posts to (curl with a real session cookie + _csrf token scraped from the rendered
page) from inside guest 9201. No server logic skipped — only client rendering.
| Leg | Evidence |
|---|---|
login + GET /sharing |
302 login; page renders (title + nav present) |
POST /sharing/enable |
303; smb.enabled=true, server_name=FELHOM |
POST /sharing/password |
303; user_set=true; log household SMB password applied for user=felhom |
POST /sharing/shares (new folder) |
303; …/hdd_1/shares/dokumentumok created owned 1000:1000 |
POST /sharing/shares (existing, read-only) |
303; stored read_only: true |
Guard refusals (/api/sharing/browse) |
appdata, backups, /etc, drive-root → all 400, identical {"error":"Ez a mappa nem osztható meg."} |
| Guard accept | …/hdd_1/media → 200 with entries |
Generated smb.conf |
hardened global verbatim; [filmek] read only = yes, [dokumentumok] read only = no, force-user block on both |
| Windows 11 (192.168.0.110) | Test-NetConnection 445 → True |
| NetBIOS flat name (nmbd) | nbtstat: FELHOM <00>/<03>/<20> Registered; ping FELHOM → Reply from 192.168.0.127 |
| SMB round-trip (writable) | write → readback BYTE-COMPARE: PASS |
| Write-refused (read-only) | write to \\192.168.0.127\filmek rejected; folder still empty on the box (non-effect proven) |
force user promise |
SMB-written r7.txt owned 1000:1000 on the box |
| WS-Discovery | ProbeMatch from 192.168.0.127 PASS |
Two real bugs caught by live validation (fixed + regression-tested)
/api/sharing/browseunreachable — the/api/subtree is routed on the main mux inmain.go, so the case added to the webServeHTTPswitch was shadowed by the apiRouter catch-all and returned401 authentication required. Fixed by registeringmux.Handle("/api/sharing/", RequireAuth(CsrfProtect(ServeSharingAPI))), matching/api/storage/.- Share creation silently failed — the "new folder" flow passed the storage ROOT through
sharingResolvePath, which correctly refuses the drive root as a share TARGET. Split outsharingResolveStorageRoot(accepts exactly a registered live root — strictly tighter);TestSharingResolveStorageRootasserts both halves.
8. NOT yet live-validated
- Explorer render — Viktor's human leg. Network view shows
FELHOM, double-click opens both shares. Everything objective around it passed (ProbeMatch, 445, NetBIOS name resolution, by-name mount), so this is UX confirmation, not a mechanism gate. Stack + both shares are live on the demo box. - Share data in a real backup run — blocked by the §4 design fork (classified, not executed).
- Symlink-escape guard on Linux — unit test SKIPs on Windows (no privilege).
- Drive-disconnect behaviour live (unit-tested only); multi-root share sets; SMB throughput/signing.
9. Observations (not acted on)
monitor.EffectiveProtectedderives only fromcfg.Stacks.Protected(golden-generated), so samba is not liveness-monitored — a dead samba container raises no "protected container missing" issue. Fixing it needs the golden controller.yaml to list samba, or the SMB-enabled flag threaded through 4RunHealthCheckcall sites. Deferred deliberately; the shipped direction is the safe one (no false alarms while the feature is off).- A classified bind's
RelPathis relative to its owning storage root; a share set spanning multiple roots has no single resolution root in the current(Root, RelPath) + hddPathvocabulary — part of the §4 fork. sambaWriteAtomicis a fourth atomic-write helper (withbackup.atomicWrite,bootstrap.writeFileAtomic,setup.atomicWriteFile) — a shared util is worth a cleanup pass.- A throwaway
r7.txtfrom the round-trip test remains in the demodokumentumokshare (harmless; left so the Explorer leg has visible content).