Files
felhom.eu/documentation/audits/SPIKE-lan-discovery-2026-07-18.md
T
admin 900d3041fa spike(R-6): LAN service discovery from the guest — SSDP/WSD/mDNS/SMB matrix
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.
2026-07-18 10:38:46 +02:00

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 /tmp script, 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:1900 with ST: 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 PASSTcpTestSucceeded: True
3. SMB round-trip New-SmbMapping as spikeuser → write roundtrip.txt → read back → byte-compare PASSBYTE-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 .local resolution (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.