Viktor re-tried the double-click after the nmbd fix: Network -> FELHOM-SPIKE -> spike-share opened, README.txt displayed. Full visible-and-openable one-click UX works. Spike containers + /tmp scripts removed on guest and DooPlex; ports free; verified report-clean. Spike COMPLETE.
19 KiB
SPIKE — R-6: LAN service discovery from the guest (SSDP · WSD · mDNS · SMB) — 2026-07-18
STATUS: COMPLETE — verdict below. All legs incl. the human Explorer leg PASSED; stack torn down, box report-clean (§CLEANUP). Empirically maps which Docker network mode inside the appliance LXC can send AND receive each LAN discovery protocol on the real household LAN, proves the negative for the default bridge, and stands up a real samba+wsdd stack that a Windows 11 client discovers and mounts. No production code shipped; every live mutation is a
spike--prefixed throwaway container or a/tmpscript, inventoried in §CLEANUP. The verdict matrix is the architecture input R-7 (SMB) and R-8 (DLNA) were gated on.
Class: SPIKE (empirical; no product code). Repos touched: felhom.eu (this doc + the ROADMAP
R-6 row only). felhom-agent / felhom-controller read-only for the premise.
1. Baselines (verified at session start, git HEAD)
| Repo | live main @ |
spec-brief expected @ | Version | Delta |
|---|---|---|---|---|
| felhom.eu | 9e13ead |
ee4e061 |
hub v0.66.0 / scripts v1.20.0 | docs/iso-out-path commits since the brief (07-18) |
| felhom-agent | f222a7b |
8208a80c |
v0.90.0 | docs-only build-root-move commit (07-18) |
| felhom-controller | ac13966 |
a4a7de3d |
v0.143.0 | build.sh path repoint (07-18) |
The deltas vs the brief are all the 2026-07-18 "DooPlex build root moved off SSD" doc/path commits; none touch the guest-network premise. Premise re-confirmed LIVE in S0, not from memory.
2. Probe topology (all four actors on one L2, 192.168.0.0/24)
| Actor | IP | Role |
|---|---|---|
Appliance guest LXC 9201 (demo-vm-felhom) |
192.168.0.127 | probe target — responders live here |
| DooPlex (192.168.0.180) | 192.168.0.180 | scripted Linux LAN peer |
| Windows 11 workstation | 192.168.0.110 | the REAL client (WSD/mDNS/SMB probes + Explorer) |
| felhom-pve host / vmbr0 | 192.168.0.162 | read-only bridge facts only |
VERDICT (one paragraph)
The appliance guest IS LAN-bridged and multicast discovery works from it — but ONLY when the
responder shares the guest's network namespace (guest-direct or a Docker --network host
container). Docker's default bridge cannot receive LAN multicast at all, so WSD and mDNS discovery
are dead there while a published unicast port still works. Empirically, guest-netns (S1) and
host-network (S2) are byte-for-byte equivalent and pass SSDP/mDNS/WSD in both directions; the
default bridge (S3) fails multicast RX for all three. A real samba + wsdd stack on host-network
is discovered by Windows 11 (ProbeMatch confirmed + FELHOM-SPIKE renders in Explorer), answers on TCP
445, and serves an authenticated read/write SMB round-trip. A real SSDP MediaServer:1
advertisement from host-network is received by both LAN clients. But the human leg exposed a second
requirement wsdd alone does not meet (S4b): the Explorer double-click on FELHOM-SPIKE failed with
0x80070035 because the flat name resolved by no path — WSD gives visibility, not name resolution.
Adding nmbd (NetBIOS name service) fixed it live (\\FELHOM-SPIKE then resolves and mounts).
Recommendation for R-7: run the stack host-network, LAN-bound — smbd + wsdd (visibility) +
nmbd (flat-name resolution), and ideally avahi/.local for modern clients. For R-8: Jellyfin's
built-in DLNA is physically viable on this topology if it runs host-network. One caveat R-7/R-8
must carry: vmbr0 runs with multicast_snooping=1 and no STP — it worked here because the household
router (.1) is an active IGMP querier, but a customer LAN with snooping-on-and-no-querier could prune
the groups (§7d).
S0 — live topology facts (GATE PASSED)
# pct config 9201
features: nesting=1,keyctl=1
net0: name=eth0,bridge=vmbr0,hwaddr=BC:24:11:9F:70:B4,ip=dhcp,type=veth
unprivileged: 1
# pct exec 9201 -- ip -4 addr show eth0
2: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
inet 192.168.0.127/24 brd 192.168.0.255 scope global dynamic eth0
# vmbr0 (host /etc/network/interfaces)
iface vmbr0 inet static
address 192.168.0.162/24
bridge-ports enp1s0
bridge-stp off
# /sys/class/net/vmbr0/bridge/multicast_snooping => 1
# pct exec 9201 -- ss -lntu | grep -E ':(445|139|1900|5353|3702|5357) ' => ports free
Confirmed: guest holds its own DHCP lease on the household /24 (same subnet as the workstation
and DooPlex), eth0 carries the MULTICAST flag, all six target ports are free. Premise HOLDS.
Anchor caveat recorded: vmbr0 multicast_snooping = 1, STP off — the querier-dependence risk the
matrix must settle.
S1 — raw multicast matrix, guest netns directly (no Docker)
Responder python3 /tmp/spike-mcast.py <proto> in pct exec 9201; probers on DooPlex (.180) and the
workstation (.110), multicast egress pinned to each host's LAN IP (IP_MULTICAST_IF).
| Protocol | guest RX (responder logged probe) | guest TX (prober got unicast reply) |
|---|---|---|
| SSDP 239.255.255.250:1900 | PASS (both probers) | PASS (both probers) |
| mDNS 224.0.0.251:5353 | PASS | PASS |
| WSD 239.255.255.250:3702 | PASS | PASS |
Evidence (SSDP, representative):
[probe .180] GOT REPLY from 192.168.0.127:1900 :: b'SPIKE-REPLY proto=ssdp from=responder'
[probe .110] GOT REPLY from 192.168.0.127:1900 :: b'SPIKE-REPLY proto=ssdp from=responder'
[guest RX] from 192.168.0.180:48169 :: 'M-SEARCH * HTTP/1.1 ...' -> [TX] replied unicast
from 192.168.0.110:64537 :: 'M-SEARCH * HTTP/1.1 ...' -> [TX] replied unicast
Finding: all three pass in both directions despite multicast_snooping=1 on vmbr0 — no
querier-absence pruning observed on this LAN (see §7d for why, and the BYO risk).
S2 — same matrix from a Docker --network host container in the guest
Responder in docker run --network host … python:3-alpine. Result: identical to S1 — all PASS.
Host netns = guest netns, proven not assumed.
Bonus corroboration (mDNS leg): the host-net responder also logged unsolicited real LAN mDNS
reverse-lookup queries from the router 192.168.0.1 (…192.168.0.162.in-addr.arpa … QTYPE PTR) —
independent proof the container genuinely receives live LAN multicast, not just our own probes.
S3 — the negative proof: default bridge + -p published ports
Same responder, docker run --network bridge -p <port>:<port>/udp ….
| Protocol | multicast RX across bridge | unicast → published port (192.168.0.127:<port>) |
|---|---|---|
| SSDP 1900 | FAIL (container never logged the M-SEARCH — see note) | PASS (both probers) |
| mDNS 5353 | FAIL ([NO REPLY] 3s timeout) |
PASS |
| WSD 3702 | FAIL ([NO REPLY] 3s timeout) |
PASS |
The container's own log shows only the unicast datagrams, never the multicast probe — the multicast simply never crosses the docker bridge. The port-publish itself works, so the failure is specifically multicast, exactly the negative R-7 needs.
SSDP note (a tell-tale, not a pass): the SSDP multicast probe did get a reply — but from
192.168.0.1:1900withST: upnp:rootdevice, USN: uuid:e4c7…, i.e. the household router's real UPnP IGD answering the broadcast M-SEARCH, NOT our container (whose log confirms it never received the datagram). Recorded as FAIL for the container; the stray reply is corroboration that the M-SEARCH genuinely propagated on the LAN while the bridged container stayed deaf to it.
S4 — the real thing: samba + wsdd (host network), Windows 11 client
Throwaway host-network stack in the guest: spike-samba (alpine + apk add samba, share
spike-share on a /tmp-backed dir, user spikeuser, throwaway password — NOT a real secret,
disable netbios=yes, smb ports=445) and spike-wsdd (alpine + apk add wsdd,
wsdd -i eth0 -4 -H 4 -n FELHOM-SPIKE -w WORKGROUP). Listeners confirmed:
udp 192.168.0.127:3702, tcp 192.168.0.127:5357, tcp 0.0.0.0:445.
| Leg | Method | Result |
|---|---|---|
| 1. WS-Discovery ProbeMatch | scripted Probe (Types=wsdp:Device) from the workstation |
PASS — ProbeMatch from 192.168.0.127; wsdd logged our exact MessageID from 192.168.0.110 |
| 2. Port 445 reachability | Test-NetConnection 192.168.0.127 -Port 445 |
PASS — TcpTestSucceeded: True |
| 3. SMB round-trip | New-SmbMapping as spikeuser → write roundtrip.txt → read back → byte-compare |
PASS — BYTE-COMPARE: PASS; dir listed README.txt (24B) + roundtrip.txt (28B) |
| 4. Explorer Network view (human) | Viktor: Explorer → Network | SPLIT then RESOLVED: device VISIBLE (WSD PASS); double-click FAILED 0x80070035 with wsdd-only → after the S4b nmbd fix, Viktor's double-click opened spike-share and README.txt (screenshot) — full one-click UX PASS |
wsdd log for leg 1:
08:32:29 joined multicast group 239.255.255.250 on 192.168.0.127%eth0 ; scheduling Hello
08:32:44 192.168.0.110:55639(eth0) - - "Probe urn:uuid:aaaaaaaa-...-000000000001 UDP" - -
The scripted client's friendly-name auto-fetch reported FAIL only because python-wsdd's metadata
HTTP endpoint (:5357/<uuid>/) requires the native WS-Discovery Resolve→Transfer/Get handshake
that Windows Explorer performs itself (a direct throwaway Get 404s). The objective discovery gate
is the ProbeMatch (PASS); the FELHOM-SPIKE name is what wsdd is configured to present. Optional
S4.5 (mDNS .local name + phone) was skipped.
S4b — the Explorer double-click failure (THE sharpest R-7 finding)
Viktor's human leg surfaced a split result Windows-side that the by-IP scripted tests had masked:
- Visibility: PASS. Explorer → Network → Computer renders
FELHOM-SPIKE(wsdd/WSD works as a discovery beacon — screenshot on file). - Double-click: FAIL →
Windows cannot access \\FELHOM-SPIKE·0x80070035"network path not found."
Diagnosis from the workstation (192.168.0.110) isolates the cause as flat-name resolution, not SMB:
| Check | Result |
|---|---|
Test-NetConnection 192.168.0.127 -Port 445 + New-SmbMapping \\192.168.0.127\spike-share |
PASS — mounts by IP, lists files. SMB/auth/445 are fine. |
Resolve-DnsName FELHOM-SPIKE (DNS) |
fail — "DNS name does not exist" |
Resolve-DnsName FELHOM-SPIKE -LlmnrNetbiosOnly |
fail — timeout (nobody answers LLMNR) |
ping FELHOM-SPIKE |
"could not find host" |
nbtstat -A 192.168.0.127 |
"Host not found" — no NetBIOS name service (disable netbios=yes) |
Root cause: WSD hands Explorer a device icon named FELHOM-SPIKE, but the double-click issues a
plain UNC \\FELHOM-SPIKE, whose flat name Windows must resolve via DNS → LLMNR → NetBIOS. With
disable netbios=yes and no mDNS/LLMNR responder, nothing answers → 0x80070035. Mounting by IP
bypasses all name resolution and works — proving the SMB server itself is healthy.
Fix verified live (S4b relaunch: samba with disable netbios=no + nmbd):
| Check (after nmbd) | Result |
|---|---|
nbtstat -A 192.168.0.127 |
FELHOM-SPIKE <00>/<03>/<20> Registered |
ping FELHOM-SPIKE |
Reply from 192.168.0.127 — flat name resolves via NetBIOS broadcast |
New-SmbMapping \\FELHOM-SPIKE\spike-share |
MOUNT BY NAME: OK — lists the share |
| Viktor re-tried the Explorer double-click | PASS — Network → FELHOM-SPIKE → spike-share opened; README.txt ("hello-from-felhom-spike") displayed. The full visible-and-openable one-click UX now works. |
Conclusion for R-7: wsdd alone makes the box visible but is not sufficient for the
one-click UX — a flat-name resolver is required for \\FELHOM-SPIKE to open. Options, in order of
how well they serve the exact WSD-shown flat name:
- nmbd / NetBIOS name service (samba's
disable netbios=no+ run nmbd) — directly resolves the flat\\FELHOM-SPIKE, proven here. The catch: NetBIOS-over-TCP is legacy (Microsoft is slowly deprecating it) and can be off on hardened client networks; needs UDP 137/138 andinterfaces = eth0+bind interfaces only = yesin production (nmbd bound the docker 172.x bridges too here — must be pinned to the LAN iface). - mDNS / avahi advertising
felhom-spike.local— Windows 10/11 resolve.localnatively and it is the future-proof path, but it serves\\felhom-spike.local\…, not the flat name the WSD icon double-clicks — so it's an additional access path, not a drop-in fix for the Explorer double-click. (Not tested live — R-7 should.) - Accept "mount by IP / by
.local" and treat WSD as visibility-only — the weakest UX.
R-7 recommendation: ship wsdd + nmbd (LAN-bound) for the classic one-click experience and
avahi/.local for the modern clients, so both the flat name and .local resolve.
S5 — SSDP / DLNA pre-check for R-8 (scripted only)
Host-network responder advertising urn:schemas-upnp-org:device:MediaServer:1, answering only
that ST (so a reply can't be the router IGD). Device-specific M-SEARCH from DooPlex and the
workstation:
[.180] reply from 192.168.0.127:1900 felhom-spike=True :: HTTP/1.1 200 OK ... SERVER: FelhomSpike/1.0
[.110] reply from 192.168.0.127:1900 felhom-spike=True :: HTTP/1.1 200 OK ...
[responder] RX M-SEARCH ST='...MediaServer:1' from .180 -> replied ; from .110 -> replied
PASS from both clients; the router did NOT answer (it is not a MediaServer). A DLNA advertisement physically reaches clients on this LAN. No Jellyfin/catalog touched — R-8 owns the real-media validation.
S6 — macvlan fallback
NOT RUN — conditional on S2 failing, and S2 PASSED. Host-network fully satisfies the requirement, so the macvlan-on-veth-in-unprivileged-LXC path was not needed. Left as a documented fallback if a future deployment must avoid host-network port collisions (§7c).
7. Verdict matrix + prose
| Protocol / mode | guest netns (S1) | docker host-net (S2) | docker bridge+publish (S3) | macvlan (S6) |
|---|---|---|---|---|
| SSDP 1900 RX / TX | PASS / PASS | PASS / PASS | FAIL / n-a · unicast-publish PASS (router-IGD false reply) | not run |
| mDNS 5353 RX / TX | PASS / PASS | PASS / PASS | FAIL / FAIL · unicast-publish PASS | not run |
| WSD 3702 RX / TX | PASS / PASS | PASS / PASS | FAIL / FAIL · unicast-publish PASS | not run |
| SMB 445 unicast | n/a | PASS (S4) | publish PASS | not run |
| Explorer visibility (human) | n/a | PASS — FELHOM-SPIKE renders (S4.4) | n/a | not run |
| Explorer double-click → open share | n/a | FAIL w/ wsdd-only → PASS w/ nmbd (S4b) | n/a | not run |
(a) Recommended transport + services for the R-7 SMB stack — HOST-NETWORK, LAN-bound, THREE
daemons. The matrix is unambiguous on transport: WSD (Explorer's actual discovery) and mDNS require
the responder in the guest netns; the default docker bridge is categorically deaf to LAN multicast.
So the stack must run network_mode: host, SMB bound to the LAN interface only, never over the
tunnel. On services, S4b proved wsdd is not enough: ship **smbd (445) + wsdd (WSD visibility)
nmbd(NetBIOS flat-name resolution, so the Explorer double-click on\\FELHOM-SPIKEactually opens) + ideallyavahi(.localfor modern/Apple clients)**. Pin every daemon to the LAN iface (interfaces = eth0+bind interfaces only = yes) — nmbd otherwise also binds the docker 172.x bridges. Without nmbd/mDNS the box is visible but not openable by name (0x80070035).
(b) R-8 "try Jellyfin built-in DLNA first" — PLAUSIBLE on this topology. SSDP MediaServer:1
adverts physically reach both LAN clients (S5). The only structural requirement carried over from
(a): Jellyfin's DLNA server must also run host-network to send/receive SSDP — the same constraint,
not a new one. The cheap path is worth trying before adding minidlna.
(c) Bridge/snooping caveat for R-7's spec. Two things must appear in the R-7 spec: (i) host-network binds 445 / 3702 / 5357 directly on the guest's LAN IP — the appliance owns those ports; a customer running a second SMB/WSD service on the box would collide (macvlan is the escape hatch, untested here); (ii) the multicast-snooping dependency below.
(d) Open question for BYO topologies — NOT tested, named as a doc-level gap. Everything here was
measured on the operator's own household LAN, where vmbr0 has multicast_snooping=1 but the
router (.1) is a live IGMP querier + UPnP IGD, so groups are not pruned. Two untested risks for real
customers: (i) a customer LAN with snooping on and no querier could prune SSDP/mDNS/WSD groups —
discovery would silently fail even though this spike passed; (ii) Peti's BYO cluster bridge is
unknown — a BYO guest may sit on an isolated/routed bridge with no L2 path to the household LAN, where
discovery is impossible regardless of network mode. R-7/R-8 specs should require a per-deployment
"discovery reachability" check (a snooping/querier probe) rather than assuming the appliance result
generalizes.
NOT tested (explicit)
- mDNS / avahi
.localas a flat-name/.localresolver alternative to nmbd (S4b option 2) — NOT tested; R-7 should validate\\felhom-spike.localon Win11 + Apple/phone clients. - LLMNR responder as a third name-resolution path — not tested (client LLMNR is on, but nothing on the box answered it).
- macvlan (S6) — not needed (host-net passed).
- Real DLNA client / TV rendering actual media — R-8 owns.
- Phone mDNS
.localresolution (optional S4.5) — skipped. - BYO bridge topology (Peti's arc) — untested by design; §7d gap.
- IPv6 discovery — probes were IPv4-only (
wsdd -4). - Customer LAN with snooping-on + no-querier — the §7d structural risk; not reproducible on this LAN.
- SMB signing / large-file throughput / multi-user — R-7 owns.
- wsdd friendly-name via direct HTTP Get — 404 without the native Resolve handshake; Windows does it natively.
CLEANUP
TORN DOWN — report-clean verified 2026-07-18 (after Viktor's Explorer PASS). All throwaway
containers (spike-hostnet-*, spike-bridge-*, spike-ssdp-ms, spike-samba, spike-wsdd) removed
and every guest-netns responder process killed; /tmp/spike-* deleted on both the guest and DooPlex.
Verified:
pct exec 9201 -- docker ps -a --filter name=spike- => (none)
pct exec 9201 -- ls /tmp/spike-* => (none)
pct exec 9201 -- ss -lntu | grep :(445|3702|5357|137|139) => (free)
DooPlex ls /tmp/spike-* => (none)
No systemd units, no compose files, no controller-visible stacks were ever created. Note: nmbd bound
the docker 172.x bridge subnets too during S4b — a production R-7 stack must set
interfaces = eth0 + bind interfaces only = yes.