docs: DIAG — Megosztás reload loop root-caused (b5d78d1 phase coercion) + SMB endpoint is the guest, not .162

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nhk3eBHT8Mg5L8c2aj57aU
This commit is contained in:
2026-07-20 10:11:58 +02:00
parent 1e1181939b
commit 0c4886958e
2 changed files with 320 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
# REPORT — Megosztás diagnosis (2026-07-20)
Topic-scoped report (parallel-session rule: shared `REPORT.md` untouched).
**Run:** RUNBOOK "Megosztás diagnosis — SMB unreachable from Mac + sharing-page reload loop".
Read-only diagnosis. **No code changes, no version bumps, no builds, no restarts.**
**Deliverable:** `documentation/audits/DIAG-sharing-2026-07-20.md`.
## Verdicts
* **Reload loop — ROOT-CAUSED (HIGH).** `sharingStatusHandler` (`sharing_handlers.go:246`, added in
`b5d78d1`, controller v0.147.0, 2026-07-19) coerces `idle``running` whenever the samba
container is alive. `sharing.html` L320326 treats `running` as a one-shot job-success edge and
calls `location.reload()` 1.2 s later. The first `tick()` fires synchronously on every page load,
so the page reloads forever. Unconditional for any customer with sharing enabled — the Megosztás
page is currently unusable. Proven live: 6 consecutive `/sharing/status` polls all returned
`{"phase":"running","running":true}`, and the controller log shows **no ensure job ran at all**,
so the phase is manufactured by that line rather than left over from a stuck job.
* **`smb://192.168.0.162` — ROOT-CAUSED.** `.162` is the Proxmox host and never was an SMB endpoint.
smbd runs in guest 9201 and binds `192.168.0.104:445`. `nc` from the host: `.104:445` **open**,
`.162:445` **refused**. Wrong target, stale Finder favourite.
* **`smb://FELHOM` — OPEN, narrowed.** NetBIOS resolution works on the wire
(`nmblookup -B 192.168.0.255 FELHOM``192.168.0.104 FELHOM<00>`), but the stack advertises **no
mDNS/Bonjour** (nothing on udp/5353; the R-6 spike selected `smbd + nmbd + wsdd` only) — the
mechanism macOS Finder prefers. Closing this needs one probe from the Mac (`smbutil lookup FELHOM`
/ `dns-sd -B _smb._tcp`), listed in the audit's Mac test matrix.
* **H2 (container down/crash-looping) — RULED OUT** at the first probe: `felhom-samba` Up 3 h, clean
logs, smbd/nmbd/wsdd all bound as `infra/samba.go` intends, live `smb.conf` matches the renderer
with no baked address literal, no PVE firewall in the path.
## Findings (full table + evidence in the audit doc)
S-1 HIGH reload loop (XS fix: latch a `sawInFlight` flag in the JS; red-proof required) ·
S-2 MED the UI never shows the connect address, only `\\FELHOM` — customers guess IPs ·
S-3 MED no mDNS advertisement (image slice, needs republish) ·
S-4 LOW ensure-job phase never resets — fold into the async-job-feedback roadmap item ·
S-5 INFO the guest's LAN IP is DHCP, so any displayed address must be read live, never cached.
Recommended packaging: S-1 as an immediate patch task (it bricks a shipped page), S-2 alongside it
if the guest IP is reachable from the sharing handler, S-3 as its own slice.
## Also noticed
The **remote site's LAN is `192.168.0.0/24` — the same prefix as the DooPlex home LAN** that the
Tailscale subnet router advertises (`192.168.0.180` shows `FAILED` in felhom-pve's neighbour table).
A successful `ping 192.168.0.162` therefore does not by itself prove the Mac is on the remote
segment; the Mac matrix starts by confirming which network it is on.
## Actions taken
None. Every command was a read, except a `POST /login` to obtain a session for the status polls.
No secrets are recorded in either document.
@@ -0,0 +1,267 @@
# DIAG — Megosztás: SMB unreachable from Mac + `/sharing` reload loop (2026-07-20)
**Class:** diagnosis run (RUNBOOK). Read-only. **No code changes, no version bumps, no builds, no
sanctioned restart was needed** (H2 was ruled out at the first probe — the container was already up).
**Baselines at run time**
| Repo | `main` @ | Deployed |
|---|---|---|
| felhom-controller | `8db9232` | `gitea.dooplex.hu/admin/felhom-controller:0.150.0` on 9201 — Up 23 min (healthy) |
| felhom-samba | — | `gitea.dooplex.hu/admin/felhom-samba:1.0.0` — Up 3 hours |
Site: felhom-pve at the remote location, `vmbr0` static `192.168.0.162/24`, gw `192.168.0.1`.
---
## Verdicts
| Symptom | Verdict | Root cause |
|---|---|---|
| 1. `smb://192.168.0.162` fails from the Mac | **ROOT-CAUSED** | `.162` is the **Proxmox host**, never an SMB endpoint. smbd lives in guest 9201 and binds `192.168.0.104:445`. `.162:445` = connection refused, by design. |
| 1b. `smb://FELHOM` also fails | **OPEN — narrowed** | NetBIOS name resolution *works on the wire* (`nmblookup``FELHOM<00> = 192.168.0.104`), but the stack advertises **no mDNS/Bonjour** (no avahi, nothing on udp/5353) — the mechanism macOS Finder actually prefers. Cannot be closed without one test from the Mac (§ Mac matrix). |
| 2. `/sharing` full-page reloads ~1× per second | **ROOT-CAUSED** | `GET /sharing/status` returns `phase:"running"` on *every* load (liveness coercion added in `b5d78d1`, v0.147.0, 2026-07-19); the page's JS treats `running` as "the job just finished" and fires `location.reload()` 1.2 s later. Unconditional loop whenever sharing is up. |
**Hypothesis outcome:** **H1 confirmed, in a stronger form than stated** (not a *sticky in-memory
job* — there was no job at all this process; the handler *manufactures* the terminal phase).
**H2 ruled out.** **H3 confirmed for the `.162` half of symptom 1**; the `\\FELHOM` half is a new,
separate defect (no Bonjour advertisement) and stays OPEN pending the Mac-side probe.
---
## Phase A — Live state snapshot (guest 9201)
```
$ docker ps -a
felhom-samba Up 3 hours gitea.dooplex.hu/admin/felhom-samba:1.0.0
felhom-controller Up 23 minutes (healthy) …/felhom-controller:0.150.0
```
No `Exited`, no `Restarting`, no restart churn → **H2 dead on arrival.**
```
$ docker logs felhom-samba --tail 80
[felhom-samba] launching nmbd + wsdd + smbd (server=FELHOM iface=eth0 uid=1000)
[felhom-samba] launching nmbd + wsdd + smbd (server=FELHOM iface=eth0 uid=1000)
```
(Two lines = the two lifetimes since creation. Clean, no errors.)
```
$ ip -4 addr
2: eth0@if7 … inet 192.168.0.104/24 brd 192.168.0.255 scope global dynamic eth0
```
**The guest's LAN IP is `192.168.0.104`** (DHCP lease, ~54 min remaining at probe time).
```
$ ss -tlnp | grep ':445'
LISTEN 192.168.0.104:445 users:(("smbd",pid=2137,fd=28))
LISTEN 127.0.0.1:445 users:(("smbd",pid=2137,fd=29))
LISTEN [::1]:445 users:(("smbd",pid=2137,fd=30))
$ ss -ulnp | grep -E ':137|:138'
UNCONN 192.168.0.255:137 / 192.168.0.104:137 / 0.0.0.0:137 nmbd pid=2146
UNCONN 192.168.0.255:138 / 192.168.0.104:138 / 0.0.0.0:138 nmbd pid=2146
$ ss -tlnp | grep ':5357'
LISTEN 192.168.0.104:5357 users:(("python3",pid=2147,fd=10)) # wsdd
```
All three daemons up and bound exactly as `infra/samba.go` intends. **Nothing on udp/5353** — no
mDNS responder anywhere in the guest.
Reachability from the Proxmox host (same L2 segment as the Mac):
```
$ nc -zv -w3 192.168.0.104 445 → (UNKNOWN) [192.168.0.104] 445 (microsoft-ds) open rc=0
$ nc -zv -w3 192.168.0.162 445 → demo-felhom.local [192.168.0.162] 445 : Connection refused rc=1
```
**SMB is reachable on the LAN at `192.168.0.104:445`.** `.162:445` refused — documents that the
host IP was never the SMB endpoint.
No packet filter in the path: `/etc/pve/firewall/cluster.fw` and `/etc/pve/firewall/9201.fw` do not
exist; `pct config 9201` shows `net0: name=eth0,bridge=vmbr0,ip=dhcp` with no firewall flag.
Service-level proof from inside the container:
```
$ docker exec felhom-samba smbclient -L 127.0.0.1 -N
Anonymous login successful
$ docker exec felhom-samba nmblookup -B 192.168.0.255 FELHOM
querying FELHOM on 192.168.0.255
192.168.0.104 FELHOM<00> rc=0
```
**NetBIOS name resolution answers correctly on the LAN broadcast.**
---
## Phase B — The reload loop
### The decisive code
`controller/internal/web/sharing_handlers.go``sharingStatusHandler` (blame `b5d78d1`,
"v0.147.0 — feedback slice 1", 2026-07-19):
```go
phase := sambaPhaseIdle
if job := s.sambaEnsure.snapshot(); job != nil { phase, errMsg = job.Phase, job.Error }
running := s.stackMgr != nil && s.stackMgr.SambaRunning()
// A stale `idle`/`running` job must never contradict reality: liveness wins on a fresh page.
if phase == sambaPhaseIdle && running {
phase = sambaPhaseRunning // ← line 246
}
```
`controller/internal/web/templates/sharing.html` L313335, the 1.5 s tick:
```js
if (ph === 'running') {
stop();
show('alert-success', 'A megosztási szolgáltatás fut.');
/* Repaint the „Állapot" badge, which was rendered server-side as „áll". */
setTimeout(function(){ location.reload(); }, 1200);
return;
}
tick(); // ← fires IMMEDIATELY on load, before the interval
timer = setInterval(tick, 1500);
```
**The defect.** The two halves were written for different worlds and contradict each other:
* The JS reads `running` as an **edge** — "the ensure job I was watching has just succeeded, so the
server-rendered badge is stale; reload once to repaint it."
* Since `b5d78d1` the handler reports `running` as a **level** — "the container is alive", true on
every page load forever after, with no job in sight.
`tick()` is called synchronously at script end, so the very first poll of every fresh page load
returns `running` → reload scheduled at +1200 ms → new page → new `tick()` → reload. Period ≈
1.2 s + load time, matching Viktor's "roughly every second". The `stop()` is irrelevant: the timer
dies, but the page it lived on is destroyed 1.2 s later anyway.
It is **unconditional** — any customer with sharing enabled and the container up cannot use the
Megosztás page at all. `setTimeout` also survives `stop()`, so there is no in-page escape.
### The live signal (6 polls, 2 s apart, authenticated)
```
{"data":{"error":"","phase":"running","running":true},"ok":true} ×6, identical
```
Constant terminal phase, never `idle`, never flapping ⇒ **H1**, and the constancy is structural
rather than a leftover job.
### Onset correlation
```
$ docker logs felhom-controller --since 3h | grep -iE 'samba|sharing|megoszt'
(no output)
```
**No ensure job ran in this controller process at all**`s.sambaEnsure.snapshot()` is `nil`, so
`phase` starts at `idle` and the reported `running` comes *purely* from line 246. This rules out
"today's deploy restarts kicked a job that got stuck": there is nothing stuck; the loop needs only a
running container and a page load.
Onset is therefore **v0.147.0 (2026-07-19)**, the commit that added line 246 — one day before the
report, consistent with "didn't do this before". Today's 0.148→0.149→0.150 deploys are innocent.
---
## Phase C — SMB service correctness
Live `smb.conf` inside the container matches `RenderSambaConfig` byte-for-byte in the global block:
```
[global]
workgroup = WORKGROUP
netbios name = FELHOM
security = user
map to guest = never
server min protocol = SMB2
disable netbios = no
bind interfaces only = yes
interfaces = lo eth0
smb ports = 445
[Share]
path = /mnt/felhom-drives/hdd_1/shares/Share
read only = no
```
* **No baked address literal.** `interfaces = lo eth0` is *interface*-scoped, not IP-scoped — it
follows the DHCP lease automatically. The `.162` mismatch is not config drift; `.162` is simply a
different machine.
* One share is exported: `Share`.
* Discovery daemons: nmbd ✅ (answers NBNS, § Phase A), wsdd ✅ (tcp/5357 bound — the Windows path),
**avahi/mDNS ❌ — absent by design of the R-6 spike verdict, which selected `smbd + nmbd + wsdd`.**
---
## The subnet-collision hazard (noticed while mapping the LAN)
```
$ ip route (felhom-pve)
default via 192.168.0.1 dev vmbr0
192.168.0.0/24 dev vmbr0 proto kernel scope link src 192.168.0.162
$ ip neigh show dev vmbr0
192.168.0.1 REACHABLE 192.168.0.104 REACHABLE (the guest)
192.168.0.230 STALE 192.168.0.11 STALE
192.168.0.180 FAILED ← DooPlex is NOT on this segment
```
**The remote site's LAN is also `192.168.0.0/24`** — the same prefix as the DooPlex home LAN, which
the Tailscale subnet router advertises. Consequences to keep in mind while the box is away:
* A successful `ping 192.168.0.162` **does not prove** the Mac is on the remote segment — if the Mac
were at home, the same address could resolve to a home device or route over the tailnet.
* Any 192.168.0.x address is ambiguous across the two sites for the duration of the trip.
This does not change the verdicts above, but it is the reason the Mac-side probe in the next section
starts with "which network am I actually on".
---
## Findings
| # | Sev | Finding | Evidence | Proposed fix | GO needed? |
|---|---|---|---|---|---|
| **S-1** | **HIGH** | `/sharing` reload-loops forever for every customer with sharing enabled — the page is unusable. `sharingStatusHandler` reports `running` as a level; the JS treats it as a one-shot success edge and calls `location.reload()`. | `sharing_handlers.go:246` (blame `b5d78d1`), `sharing.html` L320326, 6× identical `phase:"running"` poll | **XS.** Make the JS reload only when it has *observed a transition* — latch a `sawInFlight` flag on the first tick and reload on `running` only if it is set; otherwise just paint the success alert and stop. Server-side alternative (also XS): report the coerced level as a distinct phase (`running_steady`) the JS renders but never reloads on. Prefer the JS latch — it keeps the honest "liveness wins" contract the handler comment states. **Needs a red-proof:** a template/JS test or handler test asserting a fresh page load with a live container does not schedule a reload. | no — plain bug fix |
| **S-2** | **MED** | The UI never tells the customer *where* to connect. `sharing.html` L32/L61 show only `\\FELHOM`; there is no IP anywhere on the page, so a customer who cannot resolve the name has nothing to fall back on and will guess (Viktor guessed the Proxmox host IP). | `sharing.html` L3032, L61; symptom 1 itself | **S.** Render the guest's current LAN address beside the name: `\\FELHOM` *(vagy `smb://192.168.0.104`)*, sourced from the same place the dashboard already knows the guest IP. Add a one-line macOS hint (Finder → Ugrás → Csatlakozás a szerverhez). | no |
| **S-3** | **MED** | No mDNS/Bonjour advertisement. `smbd + nmbd + wsdd` covers Windows (WSD) and legacy NetBIOS, but macOS Finder's preferred path is Bonjour — `FELHOM.local` does not resolve and the share never appears in the Finder sidebar. Likely the whole of symptom 1b. | nothing on udp/5353 (§ Phase A); container log daemon list; `infra/samba.go` header comment citing the R-6 spike | **SM.** Add avahi-daemon (or `wsdd`'s mDNS sibling) to the felhom-samba image publishing `_smb._tcp`, host-net so multicast reaches the LAN. Re-runs the R-6 spike's constraint set — treat as a slice, not a hotfix. Confirm with the Mac matrix first (below) so we fix the mechanism that is actually failing. | yes — image change + republish |
| **S-4** | **LOW** | Success-state semantics: the ensure job's phase never returns to `idle` after a terminal phase, and `SambaRunning()` masks that anyway. Harmless today only because S-1's fix removes the consumer that cared. Worth folding into the **unified async-job feedback** roadmap item the `samba_ensure_job.go` header already names. | `samba_ensure_job.go` (no reset path); handler line 246 | Fold into the roadmap item; no standalone task. | no |
| **S-5** | **INFO** | The guest holds its LAN address by **DHCP** (`ip=dhcp`, lease ~54 min at probe). Any printed IP is not stable across a lease change or a site move, which weakens S-2 unless the page re-reads it live (it should — render it, never persist it). | `pct config 9201`; `valid_lft 3251sec` | Render live, never cache. Consider a DHCP reservation at the customer's router as a documented install step. | no |
**Recommended packaging:** S-1 alone as an immediate patch task (it bricks a shipped page). S-2 with
it if the guest IP is already available to the sharing handler — same page, same release. S-3 as its
own slice with an image republish. S-4 stays on the roadmap.
---
## Mac test matrix (for Viktor — CC cannot drive the Mac)
Run in Terminal, in order. First one is the important one.
| # | Command | Expected if the diagnosis is right | What a surprise means |
|---|---|---|---|
| 1 | `ifconfig \| grep 'inet 192'` | an address on `192.168.0.0/24` **and** `route -n get 192.168.0.104` pointing at the Wi-Fi/Ethernet interface, not `utun*` | if it leaves via `utun*`, you are at home on the tailnet and the whole test is against the wrong site — see the subnet-collision section |
| 2 | `nc -zv 192.168.0.104 445` | `succeeded!` | a timeout with #1 green = something between the Mac and the guest filters 445 (AP client isolation / guest Wi-Fi) — a **new** finding, tell CC |
| 3 | Finder → Ugrás → Csatlakozás a szerverhez → `smb://192.168.0.104` | login prompt; user `felhom`, the household password (out-of-band) → the `Share` folder mounts | **this is the fix for symptom 1** |
| 4 | `smbutil lookup FELHOM` | returns `192.168.0.104` | empty/error ⇒ macOS is not doing NBNS at all ⇒ **S-3 confirmed as the cause of symptom 1b**, and the mDNS slice is the real fix |
| 5 | `dns-sd -B _smb._tcp` | **FELHOM will not appear** — predicted by S-3 | if it *does* appear, S-3 is wrong and 1b needs re-opening |
| 6 | Remove the stale `smb://…162` Finder favourite (Finder → Ugrás → Csatlakozás → select → ``), and `Kilépés a szerverről` any half-mounted entry | stops the recurring failure dialog | — |
Also expect the `/sharing` page to keep reloading until S-1 ships. To read it in the meantime, load
it with JavaScript disabled, or read the state from `docker ps` in the guest.
---
## Actions taken
**None.** No restart was needed (H2 excluded by the very first probe — the samba container had been
up 3 h with clean logs). No files, settings, `smb.conf`, containers, or guest network state were
modified. Every command in this document is a read, except `curl -X POST /login` to obtain a session
for the `/sharing/status` polls, which mutates nothing.
## Observations (noticed, not acted on)
* `nmblookup -U 127.0.0.1 -R FELHOM` mis-parses: `-R` is `--name-resolve=ORDER` in this Samba build,
not `--recursion`, so the invocation errors out. Diagnostic-tooling trivia only — `-B` works and
was used for the real proof. Worth remembering for future runbooks.
* `smbclient -L 127.0.0.1 -N` reports "Anonymous login successful" and then lists nothing — correct
and intended under `map to guest = never`, but it reads like a failure. If a health probe is ever
built on this command, it must not treat the empty list as an error.
* `192.168.0.180 FAILED` in the felhom-pve neighbour table is the clean, one-line proof that the box
is off the DooPlex segment — a handy check for the rest of the trip.
* No secrets appear in this document: the household SMB password and the controller login password
are stored out-of-band.