7e0370f700
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.
307 lines
19 KiB
Markdown
307 lines
19 KiB
Markdown
# 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` | **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:
|
|
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`.
|