Files
felhom.eu/documentation/audits/SPIKE-lan-discovery-2026-07-18.md
T
admin 7e0370f700 spike(R-6): human Explorer leg PASSED (nmbd) + stack torn down, report-clean
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.
2026-07-18 10:50:14 +02:00

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 /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 + 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: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 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: FAILWindows 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 answers0x80070035. 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 PASSNetwork → 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:

  1. 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 and interfaces = eth0 + bind interfaces only = yes in production (nmbd bound the docker 172.x bridges too here — must be pinned to the LAN iface).
  2. mDNS / avahi advertising felhom-spike.local — Windows 10/11 resolve .local natively 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.)
  3. 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-SPIKE actually opens) + ideally avahi (.local for 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 .local as a flat-name/.local resolver alternative to nmbd (S4b option 2) — NOT tested; R-7 should validate \\felhom-spike.local on 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 .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

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.