docs(R-7): SMB sharing slice 1 shipped — feature doc, map row, ROADMAP

controller/sharing.md (code-verified vs controller v0.144.0 + felhom-samba
1.0.0); capability map 'Files from Windows Explorer / Mac Finder (SMB server)'
MISSING -> IMPLEMENTED (PROVEN-LIVE pending Viktor's Explorer leg); ROADMAP R-7
-> shipped-slice-1 with the slice-2 remainder, and the backup design fork split
out as R-7b (shares are classified but not in any live backup run yet).
This commit is contained in:
2026-07-18 12:05:15 +02:00
parent 7e0370f700
commit d3a564cbff
3 changed files with 172 additions and 2 deletions
@@ -85,7 +85,7 @@
| LAN access when internet is down (lan_resolver) | agent | **IMPLEMENTED** | — | Never drilled as a customer experience ("net down — can I reach my photos?") → R-19 |
| Phone photo backup | immich (classified) | **PROVEN-LIVE** | 6D end-to-end restore proof | |
| Documents/OCR | paperless-ngx (classified) | **PROVEN-LIVE** | `CAMPAIGN-6C` 4P-1 (deploy paperless-ngx, ingest 3 docs via consume flow, OCR + PDF/A ~90s) + 4P-2/3/5 | Consume-folder ingestion awkward without SMB → R-7 |
| Files from Windows Explorer / Mac Finder (SMB **server**) | — | **MISSING** | — | → R-6 (spike), R-7 (feature) |
| Files from Windows Explorer / Mac Finder (SMB **server**) | controller v0.144.0 + `felhom-samba:1.0.0` | **IMPLEMENTED** | `felhom-controller` REPORT.md (v0.144.0) + `controller/sharing.md`; transport verdict `audits/SPIKE-lan-discovery-2026-07-18.md` | „Megosztás" page: enable + one household password + shares (new folder or picked existing, per-share read-only). Fourth protected infra stack (host-net, smbd+**nmbd**+wsdd). Live on demo: 445 reachable, NetBIOS `FELHOM` resolves, write/read byte-compare PASS, write to a read-only share REFUSED, SMB writes land as uid 1000. **PROVEN-LIVE pending only Viktor's Explorer-render leg.** KNOWN GAP: share data is backup-**classified** (mandatory/optional per share) but not yet in a live tier-2/offsite RUN — reported design fork, see REPORT §4 |
| Media to TV via DLNA | — | **MISSING** | — | Jellyfin app exists; DLNA/SSDP unvalidated → R-6, R-8 |
| File access via browser | FileBrowser (infra app, auto-mount sync) | **IMPLEMENTED** | FileBrowser runs healthy + userdata-bound (`storage-lifecycle-acceptance-2026-06-15`, `CAMPAIGN-3`) | Actual browse/download **through FileBrowser** is exercised in no doc. (Cited `CAMPAIGN-2` T-PAGE-ALL renders only the controller dashboard pages, not FileBrowser.) Demoted |
| Forgot dashboard password → instant reset code | controller v0.123, hub | **PROVEN-LIVE** | `DRILL-day0-take2-2026-07-12` F-15 (live re-run of the exact failure path: hash applied 1s after request, code accepted first try) | |
+2 -1
View File
@@ -31,7 +31,8 @@
|----|------|------|--------|-------|
| R-5 | Hub: offsite storage visibility — RESTIC box aggregate (v0.64.0) + PBS DR datastore (v0.65.0), each with fill + operator alert; Offsite tabs + dual dashboard gauges | M | **SHIPPED (hub v0.64.0 + v0.65.0 + tenantsync v1.2.0, 2026-07-17)** | Restic: `monitor.OffsiteBoxChecker` (throttled Hetzner GET; fill 80/90% + oversub 2.0×; scope `"pool-box"`). PBS DR (v0.65.0, Option A): read-only `usage` op on the ep0 `felhom-tenantsync` forced command (`df`, no mutation) → `monitor.PBSDRBoxChecker` (fill 80/90%, states ok/unavailable/degraded, scope `"pbsdr-box"`); `/offsite` Restic/PBS-DR tabs + two gauges; hub deploy ⟂ ep0 update (graceful "n/a"). **Both data pipelines PROVEN-LIVE** (restic `0.2% full`; PBS `19.1% full, 7.1 GB of 37.2 GB` after ep0 → v1.2.0). Thresholds pending Viktor (named keys; separate restic/PBS pairs). OPEN: UI render unit-only (password-gated), alert emails unit + red-proof only (both nominal). See hub v0.64.0 + v0.65.0 REPORTs |
| R-6 | **Spike: LAN service discovery from the guest** — SSDP multicast (UDP 1900, DLNA), WSD (Windows discovery), mDNS; host-network vs macvlan; is the customer LXC LAN-bridged in appliance deployments? | M | **spiked (2026-07-18)** | **VERDICT: appliance guest IS LAN-bridged (own DHCP lease on the household /24); multicast discovery works ONLY in the guest netns — guest-direct or Docker `--network host` (SSDP/mDNS/WSD all PASS both ways); the default docker bridge is categorically DEAF to LAN multicast (WSD/mDNS RX FAIL, unicast-publish PASS). Real samba+wsdd on host-net → Windows 11 ProbeMatch + FELHOM-SPIKE renders in Explorer + 445 + authenticated SMB round-trip all PASS; real SSDP `MediaServer:1` advert reaches both LAN clients. → R-7 SMB stack MUST be host-network LAN-bound; R-8 Jellyfin-DLNA plausible if host-network. Caveat: `vmbr0 multicast_snooping=1` worked only because the household router is a live querier — customer LANs w/ snooping+no-querier, and Peti's BYO bridge, are UNTESTED gaps.** **S4b (human leg, the sharpest finding): wsdd makes the box VISIBLE but the Explorer double-click FAILS `0x80070035` — WSD gives no name resolution; the flat `\\FELHOM-SPIKE` resolved by no path. Adding `nmbd` (NetBIOS) fixed it live (flat name resolves + mounts). → R-7 needs smbd+wsdd+nmbd (+avahi/.local for modern clients), not wsdd alone.** Doc: `audits/SPIKE-lan-discovery-2026-07-18.md`. |
| R-7 | SMB **server** share (**gate input now exists — R-6 spiked 2026-07-18: host-network, LAN-bound, smbd+wsdd+nmbd**): samba + wsdd + nmbd, LAN-only binding (never tunnel), user model (single household user first), which roots are shared (dedicated `shares/` vs app userdata — every SMB-writable path needs a backup class), uid-1000 convention, paperless consume flow | L | idea (unblocked) | Flips map row E/MISSING. Alpha-relevant: "my box is a NAS" is a core household expectation. **R-6 settled the transport (`network_mode: host`) AND the service set: wsdd for WSD visibility is NOT enough — add `nmbd` so the Explorer double-click on `\\FELHOM-SPIKE` resolves (S4b proved wsdd-only fails `0x80070035`); ideally avahi/.local too. Pin daemons to eth0 (`bind interfaces only`). Carry the port-collision + snooping/querier caveats into the spec** |
| R-7 | SMB **server** share — LAN file sharing as an embedded controller feature („Megosztás") | L | **SHIPPED slice 1 (controller v0.144.0 + `felhom-samba:1.0.0`, 2026-07-18)** | Flips map row *Files from Windows Explorer / Mac Finder (SMB server)* MISSING → **IMPLEMENTED**. Delivered as the FOURTH protected infra stack (host-net, smbd+**nmbd**+wsdd, `bind interfaces only = lo eth0`), NOT a catalog app. „Megosztás" → „Hálózati megosztás": enable + ONE household password (STDIN→smbpasswd, never persisted — only `user_set`), shares table, create flow (new folder under `<storage>/shares/` or an existing folder via a guarded picker). Picker guard = absolute → EvalSymlinks → containment in a registered LIVE root → deny-listed system subtree (derived from `ProtectedHDDPaths`, provably a subset) → is-a-dir, with UNIFORM refusals (no filesystem oracle). Per-share read-only enforced in smb.conf AND as a `:ro` bind. `force user = felhom` → SMB writes land as uid 1000. Live-validated on demo through the real endpoints + a Win11 workstation (445 True, NetBIOS `FELHOM` resolves, write/read byte-compare PASS, **write to a read-only share REFUSED**, WSD ProbeMatch). **Explorer render = Viktor's remaining human leg.** Docs: `controller/sharing.md`; ship report `felhom-controller/REPORT.md`. **Slice 2 remainder:** avahi/`.local` for modern+Apple clients, curated app-folder presets (one-click paperless `consume/`), per-share users + guest access, recycle bin. **Backup gap split out → R-7b.** |
| R-7b | **Share data is backup-CLASSIFIED but not backed up** (design fork reported by R-7 slice 1, not improvised). `ClassifiedBinds("samba")` correctly returns mandatory/optional per share, but no live run consumes it: `backup.RunTier2` short-circuits on `os.Stat(unitDir)` BEFORE calling `GetStackClassifiedBinds`, and the offsite runner enumerates `settings.GetOffboxApps()`. Both engines are **recovery-unit shaped**, which a share-only infra stack has not. Decide the model: give samba a synthetic recovery unit, teach the tier engines a unit-less class-driven source, or keep shares tier-2-only. | M | **idea (needs Viktor ruling)** | Origin: R-7 slice 1 (`felhom-controller` REPORT §4). **Customer-facing consequence: until this lands, files dropped on a share are NOT in any backup — do not promise otherwise.** [R4] intended new shares to default to the full product backup promise (mandatory = offsite + tier-2); the classification honours that, the execution does not yet. Also blocks a clean "every SMB-writable path has a backup class" claim in the R-7 docs |
| R-8 | DLNA (**gate input now exists — R-6 spiked 2026-07-18: SSDP reaches LAN clients from host-net**): validate Jellyfin's built-in DLNA server first; only add minidlna to the catalog if Jellyfin-DLNA fails | S | idea (unblocked) | Don't add catalog weight before proving the cheap path. **R-6 confirmed the cheap path is physically viable — Jellyfin DLNA must run host-network (same multicast constraint as R-7)** |
| R-9 | Uninstaller trio (from 07-15 Peti session): cluster-aware `felhom_guests` guard (node-local `pct list` deletes cluster-wide pveum objects); saferemove detection + time estimate + opt-in `--quick-remove` (never mutate `storage.cfg`); smarter `restore_storage` default for BYO clusters (shared storage, not local-lvm) | M | idea | Second item's rejected alternative (temp-disable-and-restore) stays rejected — crash window silently downgrades cluster wipe policy |
| R-10 | T-6E-1: DB-dump dir-fsync asymmetry (LOW, confirmed in 6E) | XS | idea | One-line hardening; batch with the next controller task |
+169
View File
@@ -0,0 +1,169 @@
# Controller — LAN file sharing over SMB („Megosztás")
> **Code-verified feature doc.** Matches `felhom-controller` **v0.144.0** (R-7 slice 1) and the infra
> image **`gitea.dooplex.hu/admin/felhom-samba:1.0.0`**. Transport/discovery verdicts come from
> `documentation/audits/SPIKE-lan-discovery-2026-07-18.md` (R-6). Where this doc and the code
> disagree, the code wins — update this file.
## What the customer gets
The box behaves like a NAS on the home network. On **„Megosztás" → „Hálózati megosztás"** the customer
turns sharing on, sets ONE household SMB password, and exports folders. The box then appears in
Windows Explorer's *Network* view (and in Mac Finder) as `\\FELHOM`; opening a share and writing to it
works from any household device. Sharing is **LAN-only** — it is never exposed through the tunnel.
## Why it is NOT a catalog app
SMB ships as an **embedded controller feature** — the **fourth protected infra stack**
(traefik / cloudflared / filebrowser / **samba**). Three reasons, all structural:
1. **Host networking is mandatory.** The R-6 spike proved the default Docker bridge is categorically
deaf to the LAN multicast that WSD and mDNS discovery need; only a container in the guest's own
network namespace can send *and* receive it.
2. **Its config is a generated share list**, rendered into `smb.conf` — not a handful of env vars a
catalog template could carry.
3. **Its share roots must ride the backup classification**, which catalog metadata cannot express for
absolute, customer-chosen paths.
## The image (`felhom-samba:1.0.0`)
Built from `controller/infra-images/samba/` by `controller/scripts/build-samba-image.sh`. Pinned
alpine 3.21 (by digest) running **three** daemons under `tini`:
| Daemon | Port(s) | Why |
|---|---|---|
| `smbd` | 445 | the SMB/CIFS server itself |
| **`nmbd`** | 137/138 | **NetBIOS flat-name resolution** — without it the box is *visible* in Explorer but the double-click fails `0x80070035` (S4b of the spike: WSD hands Explorer an icon, not a name→IP mapping) |
| `wsdd` | 3702 / 5357 | WS-Discovery, so the box appears in Explorer's Network view at all |
The image is deliberately **dumb**: `/etc/samba/smb.conf` is bind-mounted READ-ONLY by the controller,
nothing is templated inside, and **no share name or password is ever baked in**. The passdb lives on
a named volume (`samba-passdb`) so the household password survives container recreation. Never
tagged `:latest`; the controller pins the exact tag in the compose it generates.
## Configuration model
Persisted in `settings.json` (`internal/settings/smb.go`):
- `smb: {enabled, server_name, user_set}``server_name` is the NetBIOS name (≤15 chars,
letters/digits/hyphen/underscore; default `FELHOM`).
- `smb_shares: [{name, path, read_only, offsite, created_at}]` — the share registry.
> **The SMB password is never persisted.** Only the boolean `user_set` is stored. The secret is passed
> to `smbpasswd` on **STDIN** (never argv, which is world-readable via `/proc`), is never logged, and
> never appears in `smb.conf`, the compose file, or `settings.json`.
## Generated `smb.conf`
Global block (hardened, all controller-managed):
```
server min protocol = SMB2 # no SMB1
security = user # named auth only
map to guest = never # no anonymous access, ever
disable netbios = no # nmbd on → \\NAME resolves
bind interfaces only = yes
interfaces = lo eth0 # LAN + loopback ONLY, never the docker bridges
```
Each share adds a section with `force user`/`force group = felhom` (uid:gid **1000**), so files written
over SMB carry the same ownership the apps and both backup tiers expect:
```
[dokumentumok]
path = /mnt/felhom-drives/hdd_1/shares/dokumentumok
read only = no
valid users = felhom
force user = felhom
force group = felhom
```
A read-only share gets `read only = yes` **and** a `:ro` bind in the compose file — defence in depth,
so a samba misconfiguration alone cannot make it writable.
## Lifecycle
`ensureSamba` runs inside `EnsureBaseStack` (boot + every health tick) after filebrowser, gated on
`smb.enabled` — the same conditional-deploy shape cloudflared uses. `ReconcileSamba` runs after every
share/settings mutation: re-render → atomic write (tmp + fsync + rename) → `compose up -d`.
- **Idempotent:** unchanged config plus a running container performs **zero** compose calls.
- **No password, no deploy:** with sharing on but no household password set, the stack stays
undeployed and the UI blocks with „először adj meg jelszót". (Setting the password brings the stack
up as part of that action — safe, because `security = user` + `map to guest = never` means nothing
is reachable until the password lands.)
- **Dead mounts are never exported:** a share whose drive is disconnected or decommissioned is
rendered ABSENT from `smb.conf` (publishing a missing mountpoint would show an empty share and let a
write land on the underlying root instead of the drive). Its configuration is retained, and the UI
row shows „A meghajtó nem elérhető."
### This feature never deletes customer data
There is no file-removal path in it at all. Disabling sharing is `compose down` (the passdb volume and
every shared folder are kept). Deleting a share is a **config-only** edit — the UI says so explicitly:
„A megosztás törölve — a mappa és a fájlok megmaradtak." The only directory creation is the guarded
new-share-folder create.
## Choosing what to share — the picker guard
Shares can be a **new folder** under `<storage>/shares/`, or **any existing folder**, with one
exception class: system data. Every customer-supplied path goes through `sharingResolvePath`:
1. must be **absolute**;
2. `EvalSymlinks` **before** the containment check — a symlink planted inside a storage root cannot
point outside it;
3. must resolve inside a **registered, live** storage root (decommissioned roots are out);
4. must not be inside a deny-listed **system subtree**`appdata/` (sharing a live app database
writable over SMB is a corruption foot-gun), `backups/`, the legacy `felhom-data/` nest;
5. the **drive root itself** is refused (a whole drive is never shareable) — an exact-match rule, so
ordinary user-data folders *under* it stay shareable;
6. must be a directory.
**Every refusal returns the same message** („Ez a mappa nem osztható meg.") so the picker can never be
used as an oracle for what exists outside the customer's storage.
The deny-list is **derived** from `stacks.ProtectedHDDPaths` (each entry is emitted only if that guard
already contains it), so it can only ever *shrink* relative to the delete guard — it can never drift
into a stale second source of truth. `media/` and `Dokumentumok/` are protected *there* as delete
targets but are customer data and remain shareable.
The new-folder flow uses a separate, strictly tighter check (`sharingResolveStorageRoot`) that accepts
**exactly** a registered live storage root — because there the root is the *parent*, not the share.
## Backup classification
`ClassifiedBinds("samba")` resolves from the shares registry instead of catalog metadata (samba has no
`.felhom.yml`, and its binds are absolute share paths):
| Per-share „Felhőmentés" | Class | Tiers |
|---|---|---|
| ON (default for new shares) | `mandatory` | offsite **and** tier-2 |
| OFF | `optional` | tier-2 only (never offsite) |
`smb.conf` and the passdb are config, not customer data, and are never classified.
> **KNOWN GAP — share data is classified but not yet backed up.** Making this seam correct does not by
> itself put share data into a live run: `backup.RunTier2` short-circuits on a missing *recovery unit*
> before it ever calls `GetStackClassifiedBinds`, and the offsite runner enumerates
> `settings.GetOffboxApps()`. Both engines are recovery-unit shaped, which a share-only infra stack
> has not. Teaching them about one is a structural change, so it was reported as a design fork rather
> than improvised inside the engines. **Until it is resolved, do not tell a customer that files
> dropped on a share are backed up.** See `felhom-controller/REPORT.md` §4.
## Operator notes
- samba is protected in **code** (`config.alwaysProtectedStacks`), because `cfg.Stacks.Protected` comes
from the golden-generated `controller.yaml` and predates the feature. Consequence:
`monitor.EffectiveProtected` does **not** monitor samba liveness — a dead samba container raises no
"protected container missing" issue. Deliberate for slice 1 (the safe direction: no false alarms
while the feature is off).
- Discovery depends on the household LAN. The R-6 spike's §7d caveat applies: a customer network with
IGMP snooping enabled and **no querier** could prune the discovery multicast, and a BYO topology may
put the guest on a bridge with no L2 path to the household LAN. The page carries an honest hint that
the device list can lag while `\\NÉV` always works.
- The customer authenticates as user **`felhom`** with the household password.
## Not in slice 1 (→ slice 2)
avahi / `.local` name publishing for modern + Apple clients; curated app-folder presets (one-click
paperless `consume/`); per-share users and guest access; a recycle bin. DLNA is a separate item (R-8).