Verdict: appliance guest is LAN-bridged; multicast discovery works only in the guest netns (guest-direct or docker --network host) — default bridge is deaf to LAN multicast. Real samba+wsdd on host-net: Windows 11 ProbeMatch + 445 + SMB round-trip PASS; SSDP MediaServer:1 reaches LAN clients. R-7 => host-network LAN-bound; R-8 Jellyfin-DLNA plausible if host-network. ROADMAP R-6 -> spiked, R-7/R-8 unblocked. S4.4 Explorer render pending human.
14 KiB
SPIKE — R-6: LAN service discovery from the guest (SSDP · WSD · mDNS · SMB) — 2026-07-18
STATUS: COMPLETE (objective legs) — verdict below. One human leg (S4.4 Explorer render) is PENDING Viktor and the samba+wsdd stack is intentionally left live for it. 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), 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. Recommendation for R-7: run the samba/wsdd stack as a host-network infra
stack, LAN-bound. 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 shows FELHOM-SPIKE, double-click opens the share | PENDING — human leg; stack left live |
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 and is the
subject of the human leg. Optional S4.5 (mDNS .local name + phone) was skipped.
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 | PENDING (S4.4) | n/a | not run |
(a) Recommended transport for the R-7 SMB stack — HOST-NETWORK, LAN-bound. The matrix is
unambiguous: WSD (Windows Explorer's actual discovery) and mDNS require the responder in the guest
netns; the default docker bridge is categorically deaf to LAN multicast. So samba + wsdd (and any
avahi) must run network_mode: host. Bind SMB to the LAN interface only; never expose 445 over the
tunnel (the LAN-only rule R-7 already states). This is the expected outcome — and the matrix, not
the expectation, now backs it.
(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)
- Explorer Network render (S4.4) — human leg, pending Viktor; stack left live.
- 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
Throwaway containers spike-hostnet-*, spike-bridge-*, spike-ssdp-ms and all guest-netns
responder processes were docker rm -f / pkill'd at the end of each leg. Intentionally left
live for the S4.4 human leg: spike-samba + spike-wsdd (host-network), share
\\192.168.0.127\spike-share as FELHOM-SPIKE. Teardown one-liner once Viktor confirms Explorer:
ssh felhom-pve "pct exec 9201 -- docker rm -f spike-samba spike-wsdd; \
pct exec 9201 -- rm -f /tmp/spike-*.py /tmp/spike-*.sh /tmp/spike-*.log"
ssh kisfenyo@192.168.0.180 "rm -f /tmp/spike-probe.py /tmp/spike-msearch.py"
Then pct exec 9201 -- docker ps -a | grep spike must be empty. The demo box is wiped in the
upcoming rehearsal regardless, but this leaves it report-clean.