v0.152.0 + felhom-samba 1.1.0 — mDNS for macOS, and the card stops offering a dead form

Capture on the box disproved the first theory: macOS DOES send a correct NBNS
query for <NAME><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://<NAME> for Mac, which is exactly the dead form;
now smb://<NAME>.local. Windows keeps the flat \\<NAME>, 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.
This commit is contained in:
2026-07-20 13:38:07 +02:00
parent 5c105fb49b
commit 37e12c82a7
8 changed files with 184 additions and 17 deletions
+47
View File
@@ -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`, `<ip>:137` and `<bcast>: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 `<NAME>.local`.
**The page no longer tells Mac users to do the one thing that cannot work.** v0.151.0's connect card
offered `smb://<NÉV>` for Mac. That is precisely the dead form. It is now `smb://<NÉV>.local`; the
Windows line stays the flat `\\<NÉV>`, 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
+19 -2
View File
@@ -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 | `\\<NÉV>` | **nmbd** (flat-name resolution) + wsdd (Network view) |
| macOS | `smb://<NÉV>.local` | **avahi/mDNS** |
| Any | `smb://<IP>` | direct — always works |
**A Mac cannot use the bare `smb://<NÉV>`, and nothing we ship can change that.** Captured live
2026-07-20: macOS broadcasts a correct NBNS query for `<NÉV><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 (`\\<NÉV>`), the Mac form (`smb://<NÉV>`), and — when derivable — the direct address
`smb://<IP>` as the fallback for networks that do not resolve the name. The address comes from
+18 -3
View File
@@ -9,13 +9,28 @@
# in Explorer but the double-click fails 0x80070035 (no flat-name resolution);
# nmbd is what makes \\<NAME> 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://<NAME>.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).
+52 -2
View File
@@ -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 <<CONF
[server]
host-name=${SERVER_NAME}
use-ipv4=yes
use-ipv6=no
allow-interfaces=${IFACE}
ratelimit-interval-usec=1000000
ratelimit-burst=1000
# nmbd: NetBIOS flat-name resolution so \\<NAME> 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 <<CONF
<?xml version="1.0" standalone='no'?><!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=RackMac</txt-record>
</service>
</service-group>
CONF
echo "[felhom-samba] launching nmbd + wsdd + avahi + smbd (server=${SERVER_NAME} iface=${IFACE} uid=${FELHOM_UID})"
# nmbd: NetBIOS flat-name resolution so \\<NAME> 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://<NAME>.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
+1 -1
View File
@@ -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
+8 -1
View File
@@ -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) {
@@ -39,7 +39,7 @@ func TestSharingConnectCard_ShowsNameAndDirectAddress(t *testing.T) {
for _, want := range []string{
"Csatlakoz", // the card heading (ASCII-safe substring)
`\\FELHOM`, // Windows form
"smb://FELHOM", // Mac 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) {
@@ -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://</span>") {
@@ -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://<NEV>.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</span>` cannot satisfy this by accident.
if strings.Contains(body, "smb://"+name+"</span>") {
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)
}
}
}
@@ -75,7 +75,7 @@
</div>
<div class="form-hint">
Mac: Finder &rarr; Ugrás &rarr; Csatlakozás a szerverhez:
<span class="mono">smb://{{.SMBServerName}}</span>
<span class="mono">smb://{{.SMBServerName}}.local</span>
</div>
{{if .SMBDirectAddress}}
<div class="form-hint">