# 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` L313–335, 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** — **FIXED v0.151.0** | **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` L320–326, 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** — **SHIPPED v0.151.0** | **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` L30–32, 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** — **FIXED v0.152.0 / samba 1.1.0** | **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 | **S–M.** 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** — **CORE SHIPPED v0.151.0** | **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** — **SHIPPED v0.151.0** | **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. --- ## Addendum — 2026-07-20, controller v0.151.0 (S-1, S-2, S-4-core, S-5 closed) Shipped and live on demo guest 9201 the same day this diagnosis was written. Controller commit `badf17b`, image `gitea.dooplex.hu/admin/felhom-controller:0.151.0`, `Up (healthy)`. | # | Status | What changed | |---|---|---| | **S-1** | **FIXED v0.151.0** | The `idle`->`running` coercion is gone from `sharingStatusHandler`. The duty it was written for — "liveness must never be contradicted by a missing job" — was always discharged by the `running` LEVEL field beside it, and is now pinned by its own named regression test instead of by a phase-channel side effect. `sharing.html`'s `