From 37e12c82a77e91992e7862373955cea779b54e12 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 20 Jul 2026 13:38:07 +0200 Subject: [PATCH] =?UTF-8?q?v0.152.0=20+=20felhom-samba=201.1.0=20=E2=80=94?= =?UTF-8?q?=20mDNS=20for=20macOS,=20and=20the=20card=20stops=20offering=20?= =?UTF-8?q?a=20dead=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture on the box disproved the first theory: macOS DOES send a correct NBNS query for <20> and nmbd DOES answer it correctly in 140us (flags 0x8580, RCODE=0, right address) - macOS just never acts on it. NetBIOS there feeds legacy browsing, not smb:// URL resolution, so the bare name can never work on a Mac and nmbd was never the broken part. felhom-samba 1.1.0 adds avahi + dbus, with avahi-daemon.conf and the _smb._tcp service file templated from FELHOM_SERVER_NAME so a rename re-advertises. Both daemons are non-fatal on failure - a discovery gap must not become an outage. v0.151.0's card offered smb:// for Mac, which is exactly the dead form; now smb://.local. Windows keeps the flat \\, which nmbd serves correctly. Red-proofed both directions. NOT claimed: Finder-sidebar discovery - published and answering on the wire, but not observed working on the test Mac. Recorded OPEN. TestRenderSambaCompose pinned the literal 1.0.0 tag, so an image bump read as a renderer regression; now derives from SambaImage and asserts non-:latest. --- CHANGELOG.md | 47 ++++++++++++++++ controller/README.md | 21 +++++++- controller/infra-images/samba/Dockerfile | 21 ++++++-- controller/infra-images/samba/entrypoint.sh | 54 ++++++++++++++++++- controller/internal/infra/infra.go | 2 +- controller/internal/infra/samba_test.go | 9 +++- .../internal/web/sharing_connect_card_test.go | 45 +++++++++++++--- .../internal/web/templates/sharing.html | 2 +- 8 files changed, 184 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090b53e..cc81f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,52 @@ ## Changelog +### v0.152.0 — Megosztás on a Mac: mDNS in the image, and the page stops giving Mac users a dead form (2026-07-20) + +Closes **S-3** of `felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md`, and fixes a copy +defect v0.151.0 shipped the same day. **Pairs with felhom-samba 1.1.0** — the pin in +`infra.SambaImage` moves with it, so `Images()` and the golden bake follow automatically. + +**The finding that redirected the fix — macOS asks, gets a correct answer, and ignores it.** The +first theory was that modern macOS no longer does NetBIOS. A packet capture on the box disproved +that: on a bare `smb://FELHOM` the Mac broadcasts a well-formed NBNS query for `FELHOM<20>` (the +File Server Service suffix — exactly right for SMB), and nmbd answers in 140 microseconds with a +textbook positive response — flags `0x8580` (response, authoritative, RCODE=0), ANCOUNT 1, unique +B-node, the correct address. **macOS never opens a TCP connection.** Sixteen seconds later the same +Mac connected through `smb://FELHOM.local` on the first try. NetBIOS on macOS feeds legacy browsing, +not `smb://` URL resolution — so no change on our side can ever make the bare name work there, and +nmbd is not the thing that was broken. (nmbd answers twice per broadcast, because it holds +`0.0.0.0:137`, `:137` and `:137` and a broadcast lands on two of them. Standard Samba; +investigated and dismissed — a duplicated correct answer is still a correct answer.) + +**felhom-samba 1.1.0 — avahi + dbus, so the Mac has a mechanism at all.** The image's discovery set +was Windows-only: nmbd for flat-name resolution, wsdd for Explorer's Network view, and nothing +whatsoever for Bonjour. It now runs avahi, with `avahi-daemon.conf` and an `_smb._tcp` service file +**templated from `FELHOM_SERVER_NAME` in the entrypoint** — renaming the server in the UI +re-advertises under the new name, where a baked name would leave the box answering to something the +customer can no longer see anywhere. A static service file rather than smbd's own `multicast dns +register`: it needs no line in `smb.conf` (bind-mounted READ-ONLY, owned by the controller's +renderer) and it lets us publish `_device-info._tcp` for a sensible Finder icon. Both new daemons +are non-fatal on failure — sharing over an address must not become an outage because a discovery +daemon did not come up. Proven live from the operator's Mac before the image was built, then the +built image smoke-tested with all five daemons up and avahi registered as `.local`. + +**The page no longer tells Mac users to do the one thing that cannot work.** v0.151.0's connect card +offered `smb://` for Mac. That is precisely the dead form. It is now `smb://.local`; the +Windows line stays the flat `\\`, which nmbd serves correctly and which this release must not +disturb. Red-proofed: reverting the template to the bare name turns +`TestSharingConnectCard_MacLineIsDotLocalNotBareName` red on both the missing `.local` and the +present bare form, for two different configured names — and the same test asserts the Windows line +neither disappears nor wrongly gains `.local`. + +**NOT claimed: automatic Finder-sidebar discovery.** The `_smb._tcp` record is published and answers +browse queries on the wire, but the test Mac's sidebar stayed empty — it had no Network/Bonjour +section shown at all, which is a Finder Settings toggle rather than something the box controls. This +is recorded as OPEN in the DIAG, deliberately not as a shipped feature. + +`TestRenderSambaCompose` asserted the literal tag `felhom-samba:1.0.0`, so a routine image bump read +as a renderer regression. It now derives from `SambaImage` and separately asserts what actually +matters — that the tag is explicit and never `:latest`. + ### v0.151.0 — the Megosztás page stops reloading, and says how to connect (2026-07-20) Closes **S-1**, **S-2**, **S-5** and the core of **S-4** from diff --git a/controller/README.md b/controller/README.md index ff4fa9e..0a39a22 100644 --- a/controller/README.md +++ b/controller/README.md @@ -2350,9 +2350,9 @@ multicast that WSD/mDNS discovery needs), its configuration is a dynamic share l `smb.conf` (not env vars), and its share roots must ride the backup classification. It is therefore the **fourth protected infra stack**: traefik / cloudflared / filebrowser / **samba**. -#### The image (`controller/infra-images/samba/`, `felhom-samba:1.0.0`) +#### The image (`controller/infra-images/samba/`, `felhom-samba:1.1.0`) -Our own pinned image (alpine 3.21 by digest) running **three** daemons — `smbd` (445), **`nmbd`** +Our own pinned image (alpine 3.21 by digest) running **four** daemons (v1.1.0) — `smbd` (445), **`nmbd`** (NetBIOS flat-name resolution), `wsdd` (WS-Discovery), under `tini`. nmbd is not optional: the R-6 spike proved wsdd alone makes the box *visible* in Explorer while the double-click still fails `0x80070035`, because WSD supplies an icon, not a name→IP mapping. The image is deliberately dumb — @@ -2402,6 +2402,23 @@ used only as the new-folder parent. The picker endpoint is `/api/sharing/browse` main mux behind `RequireAuth`+`CsrfProtect` (the `/api/` subtree is routed there, not in the web `ServeHTTP` switch). +#### Discovery is per-platform, and the two halves do not overlap (v1.1.0) + +| Client | Working form | Served by | +|---|---|---| +| Windows | `\\` | **nmbd** (flat-name resolution) + wsdd (Network view) | +| macOS | `smb://.local` | **avahi/mDNS** | +| Any | `smb://` | direct — always works | + +**A Mac cannot use the bare `smb://`, and nothing we ship can change that.** Captured live +2026-07-20: macOS broadcasts a correct NBNS query for `<20>`, nmbd answers in 140 µs with a +textbook positive response (flags `0x8580`, RCODE=0, right address), and macOS never opens a TCP +connection — NetBIOS there feeds legacy browsing, not `smb://` URL resolution. avahi templates its +config and `_smb._tcp` service file from `FELHOM_SERVER_NAME` at entrypoint, so a rename +re-advertises; both it and dbus are non-fatal on failure, because a discovery gap must never become +a sharing outage. **Automatic Finder-sidebar appearance is NOT claimed** — the record is published +and answers browse queries, but was not observed working on the test Mac. + **„Csatlakozás a megosztáshoz" card (v0.151.0).** Shown only while sharing is enabled: the Windows form (`\\`), the Mac form (`smb://`), and — when derivable — the direct address `smb://` as the fallback for networks that do not resolve the name. The address comes from diff --git a/controller/infra-images/samba/Dockerfile b/controller/infra-images/samba/Dockerfile index cfb42f5..1f950a8 100644 --- a/controller/infra-images/samba/Dockerfile +++ b/controller/infra-images/samba/Dockerfile @@ -9,13 +9,28 @@ # in Explorer but the double-click fails 0x80070035 (no flat-name resolution); # nmbd is what makes \\ resolve + mount (S4b, proven live). # - wsdd : WS-Discovery, so the box appears in Windows Explorer's Network view. +# - avahi : mDNS/Bonjour (v1.1.0) — THE macOS path. Windows and macOS do not share a +# discovery mechanism, and nmbd does not cover the Mac: captured live on +# 2026-07-20, macOS broadcasts a correct NBNS query for FELHOM<20>, the box +# answers correctly in 140us (flags 0x8580, RCODE=0, the right address), and +# macOS REFUSES TO ACT ON IT — no TCP follows. NetBIOS feeds legacy browsing +# there, not smb:// URL resolution. With mDNS, `smb://.local` connects +# immediately — PROVEN live from a Mac on 2026-07-20. +# NOT proven: automatic appearance in the Finder sidebar. The _smb._tcp record +# is published and answers browse queries on the wire, but the test Mac's +# sidebar stayed empty (it had no Network/Bonjour section shown at all, which +# is a Finder Settings -> Sidebar toggle). Treat sidebar discovery as an OPEN +# question, not a shipped feature. +# Evidence: felhom.eu/documentation/audits/DIAG-sharing-2026-07-20.md. FROM alpine:3.21@sha256:48b0309ca019d89d40f670aa1bc06e426dc0931948452e8491e3d65087abc07d # samba = smbd + nmbd + smbpasswd/testparm (meta-package proven installable in the spike); -# wsdd = WS-Discovery daemon; tini = a proper PID1 to reap nmbd/wsdd and forward signals. -RUN apk add --no-cache samba wsdd tini \ +# wsdd = WS-Discovery daemon; tini = a proper PID1 to reap nmbd/wsdd/avahi and forward signals; +# avahi + dbus = mDNS/Bonjour (avahi-daemon talks to the system bus, so dbus is not optional). +RUN apk add --no-cache samba wsdd tini avahi dbus \ && rm -rf /var/cache/apk/* \ - && rm -f /etc/samba/smb.conf + && rm -f /etc/samba/smb.conf \ + && rm -f /etc/avahi/services/*.service # passdb on a named volume → the household SMB password survives container recreation # (share add/remove re-renders + `compose up -d`, which recreates the container). diff --git a/controller/infra-images/samba/entrypoint.sh b/controller/infra-images/samba/entrypoint.sh index 0720a06..c210d37 100644 --- a/controller/infra-images/samba/entrypoint.sh +++ b/controller/infra-images/samba/entrypoint.sh @@ -23,11 +23,61 @@ fi mkdir -p /var/lib/samba/private /run/samba -echo "[felhom-samba] launching nmbd + wsdd + smbd (server=${SERVER_NAME} iface=${IFACE} uid=${FELHOM_UID})" +# --- mDNS / Bonjour (v1.1.0) ------------------------------------------------------------- +# THE macOS path. Templated from SERVER_NAME rather than baked, so renaming the server in the +# UI re-advertises under the new name on the next container recreate — a baked name would +# leave the box answering to something the customer no longer sees anywhere. +# +# A STATIC service file, deliberately, rather than smbd's own `multicast dns register`: it +# needs no line in smb.conf (which is bind-mounted READ-ONLY and owned by the controller's +# renderer) and it lets us publish _device-info._tcp so the Finder shows a sensible icon +# instead of a generic globe. +mkdir -p /etc/avahi/services /run/dbus +cat > /etc/avahi/avahi-daemon.conf < resolves and mounts (the S4b fix). +[wide-area] +enable-wide-area=no + +[publish] +publish-addresses=yes +publish-hinfo=no +publish-workstation=no +CONF + +cat > /etc/avahi/services/smb.service < + + %h + + _smb._tcp + 445 + + + _device-info._tcp + 0 + model=RackMac + + +CONF + +echo "[felhom-samba] launching nmbd + wsdd + avahi + smbd (server=${SERVER_NAME} iface=${IFACE} uid=${FELHOM_UID})" + +# nmbd: NetBIOS flat-name resolution so \\ resolves and mounts on WINDOWS (the S4b fix). +# It does NOT serve macOS — see the Dockerfile header for the captured proof. nmbd --daemon --no-process-group # wsdd: WS-Discovery so the box appears in Windows Explorer's Network view. wsdd -i "$IFACE" -4 -H 4 -s -n "$SERVER_NAME" -w WORKGROUP & +# dbus + avahi: mDNS, so `smb://.local` resolves and the box appears in the Finder sidebar. +# Non-fatal on failure: sharing over an address still works, and refusing to start smbd because +# a discovery daemon did not come up would turn a convenience gap into an outage. +dbus-daemon --system --fork 2>/dev/null || echo "[felhom-samba] WARN: dbus failed to start — mDNS disabled" +avahi-daemon --daemonize --no-drop-root 2>/dev/null || echo "[felhom-samba] WARN: avahi failed to start — mDNS disabled" # smbd in the foreground = the container's main process. exec smbd --foreground --no-process-group diff --git a/controller/internal/infra/infra.go b/controller/internal/infra/infra.go index e24b785..d29c9ac 100644 --- a/controller/internal/infra/infra.go +++ b/controller/internal/infra/infra.go @@ -27,7 +27,7 @@ const ( FileBrowserImage = "gtstef/filebrowser:1.3.3-stable" // SambaImage is our own pinned LAN-sharing image (R-7 slice 1). Built by // controller/scripts/build-samba-image.sh from controller/infra-images/samba/. NEVER :latest. - SambaImage = "gitea.dooplex.hu/admin/felhom-samba:1.0.0" + SambaImage = "gitea.dooplex.hu/admin/felhom-samba:1.1.0" ) // Images returns every controller-managed infra image, derived from the pins above so there is diff --git a/controller/internal/infra/samba_test.go b/controller/internal/infra/samba_test.go index 6dfface..0f4cade 100644 --- a/controller/internal/infra/samba_test.go +++ b/controller/internal/infra/samba_test.go @@ -77,7 +77,7 @@ func TestRenderSambaCompose(t *testing.T) { got := RenderSambaCompose(sampleSambaData()) for _, want := range []string{ "network_mode: host", - "image: gitea.dooplex.hu/admin/felhom-samba:1.0.0", + "image: " + SambaImage, "- ./smb.conf:/etc/samba/smb.conf:ro", "- samba-passdb:/var/lib/samba", "FELHOM_SERVER_NAME=FELHOM", @@ -87,6 +87,13 @@ func TestRenderSambaCompose(t *testing.T) { t.Errorf("compose missing %q\n%s", want, got) } } + // The tag must be EXPLICIT — a floating :latest breaks reproducible pins and is the thing the + // constant exists to prevent. Asserted on the constant's shape, so an image bump is a one-line + // change in infra.go and never a test edit. + if strings.HasSuffix(SambaImage, ":latest") || !strings.Contains(SambaImage, ":") { + t.Errorf("SambaImage %q must carry an explicit non-latest tag", SambaImage) + } + // Read-only share → :ro bind (red-proof target: drop the :ro in the renderer and this fails). roBind := "- /mnt/felhom-drives/media/filmek:/mnt/felhom-drives/media/filmek:ro" if !strings.Contains(got, roBind) { diff --git a/controller/internal/web/sharing_connect_card_test.go b/controller/internal/web/sharing_connect_card_test.go index 4814891..97d3c4e 100644 --- a/controller/internal/web/sharing_connect_card_test.go +++ b/controller/internal/web/sharing_connect_card_test.go @@ -37,10 +37,10 @@ func TestSharingConnectCard_ShowsNameAndDirectAddress(t *testing.T) { body := getPage(t, s, "/sharing").Body.String() for _, want := range []string{ - "Csatlakoz", // the card heading (ASCII-safe substring) - `\\FELHOM`, // Windows form - "smb://FELHOM", // Mac form - "smb://192.0.2.10", // the fallback that did not exist before + "Csatlakoz", // the card heading (ASCII-safe substring) + `\\FELHOM`, // Windows form + "smb://FELHOM.local", // Mac form — .local is load-bearing, see the regression test below + "smb://192.0.2.10", // the fallback that did not exist before } { if !strings.Contains(body, want) { t.Errorf("page missing %q", want) @@ -83,7 +83,7 @@ func TestSharingConnectCard_NoAddressOmitsTheLine(t *testing.T) { s, _ := connectCardServer(t, "FELHOM", "") body := getPage(t, s, "/sharing").Body.String() - if !strings.Contains(body, "smb://FELHOM") { + if !strings.Contains(body, "smb://FELHOM.local") { t.Error("name lines must remain when no address can be derived") } if strings.Contains(body, "smb://\n") || strings.Contains(body, "smb://<") || strings.Contains(body, "smb://") { @@ -100,7 +100,7 @@ func TestSharingConnectCard_UsesConfiguredName(t *testing.T) { s, _ := connectCardServer(t, "OTTHON", "192.0.2.10") body := getPage(t, s, "/sharing").Body.String() - if !strings.Contains(body, "smb://OTTHON") || !strings.Contains(body, `\\OTTHON`) { + if !strings.Contains(body, "smb://OTTHON.local") || !strings.Contains(body, `\\OTTHON`) { t.Error("the configured server name is not on the page") } if strings.Contains(body, "FELHOM") { @@ -144,7 +144,38 @@ func TestSharingConnectCard_UnsetNameFallsBackToEffective(t *testing.T) { if want == "" { t.Skip("no product default server name to assert") } - if body := getPage(t, s, "/sharing").Body.String(); !strings.Contains(body, "smb://"+want) { + if body := getPage(t, s, "/sharing").Body.String(); !strings.Contains(body, "smb://"+want+".local") { t.Errorf("effective default name %q not rendered", want) } } + +// REGRESSION (v0.152.0) — the Mac line must offer `smb://.local`, never the bare name. +// +// v0.151.0 shipped `smb://FELHOM`, which is the ONE form that provably cannot work on a Mac. +// Captured live 2026-07-20: macOS broadcasts a correct NBNS query for FELHOM<20>, the box answers +// in 140us with a textbook positive response (flags 0x8580, RCODE=0, the right address), and macOS +// never opens a TCP connection — NetBIOS feeds legacy browsing there, not smb:// URL resolution. +// Sixteen seconds later the same Mac connected via `smb://FELHOM.local` on the first try. So this +// is not a choice between two working spellings: the bare form is a dead end the page was actively +// sending customers down. +func TestSharingConnectCard_MacLineIsDotLocalNotBareName(t *testing.T) { + for _, name := range []string{"FELHOM", "OTTHON"} { + s, _ := connectCardServer(t, name, "192.0.2.10") + body := getPage(t, s, "/sharing").Body.String() + + if !strings.Contains(body, "smb://"+name+".local") { + t.Errorf("%s: the Mac line must offer smb://%s.local", name, name) + } + // Anchored on the closing tag, so `smb://FELHOM.local` cannot satisfy this by accident. + if strings.Contains(body, "smb://"+name+"") { + t.Errorf("%s: the page still offers the bare smb://%s — macOS cannot resolve it", name, name) + } + // Windows is served by nmbd and is proven working: fixing the Mac must not touch it. + if !strings.Contains(body, `\\`+name) { + t.Errorf("%s: the Windows flat-name form went missing", name) + } + if strings.Contains(body, `\\`+name+".local") { + t.Errorf("%s: the Windows line gained .local — that is the Mac's mechanism, not nmbd's", name) + } + } +} diff --git a/controller/internal/web/templates/sharing.html b/controller/internal/web/templates/sharing.html index b4c15ff..eaabc13 100644 --- a/controller/internal/web/templates/sharing.html +++ b/controller/internal/web/templates/sharing.html @@ -75,7 +75,7 @@
Mac: Finder → Ugrás → Csatlakozás a szerverhez: - smb://{{.SMBServerName}} + smb://{{.SMBServerName}}.local
{{if .SMBDirectAddress}}