From 900d3041fa9756c2a7f24f79d1976db1d81bfe3e Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 18 Jul 2026 10:38:46 +0200 Subject: [PATCH] =?UTF-8?q?spike(R-6):=20LAN=20service=20discovery=20from?= =?UTF-8?q?=20the=20guest=20=E2=80=94=20SSDP/WSD/mDNS/SMB=20matrix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../audits/SPIKE-lan-discovery-2026-07-18.md | 242 ++++++++++++++++++ documentation/backlog/ROADMAP.md | 6 +- 2 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 documentation/audits/SPIKE-lan-discovery-2026-07-18.md diff --git a/documentation/audits/SPIKE-lan-discovery-2026-07-18.md b/documentation/audits/SPIKE-lan-discovery-2026-07-18.md new file mode 100644 index 0000000..8daa408 --- /dev/null +++ b/documentation/audits/SPIKE-lan-discovery-2026-07-18.md @@ -0,0 +1,242 @@ +# 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: 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 ` 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 :/udp …`. + +| Protocol | multicast RX across bridge | unicast → published port (`192.168.0.127:`) | +|---|---|---| +| 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 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//`) 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. diff --git a/documentation/backlog/ROADMAP.md b/documentation/backlog/ROADMAP.md index f48acf5..69a1a1c 100644 --- a/documentation/backlog/ROADMAP.md +++ b/documentation/backlog/ROADMAP.md @@ -30,9 +30,9 @@ | ID | Item | Size | Status | Notes | |----|------|------|--------|-------| | R-5 | Hub: offsite storage visibility — RESTIC box aggregate (v0.64.0) + PBS DR datastore (v0.65.0), each with fill + operator alert; Offsite tabs + dual dashboard gauges | M | **SHIPPED (hub v0.64.0 + v0.65.0 + tenantsync v1.2.0, 2026-07-17)** | Restic: `monitor.OffsiteBoxChecker` (throttled Hetzner GET; fill 80/90% + oversub 2.0×; scope `"pool-box"`). PBS DR (v0.65.0, Option A): read-only `usage` op on the ep0 `felhom-tenantsync` forced command (`df`, no mutation) → `monitor.PBSDRBoxChecker` (fill 80/90%, states ok/unavailable/degraded, scope `"pbsdr-box"`); `/offsite` Restic/PBS-DR tabs + two gauges; hub deploy ⟂ ep0 update (graceful "n/a"). **Both data pipelines PROVEN-LIVE** (restic `0.2% full`; PBS `19.1% full, 7.1 GB of 37.2 GB` after ep0 → v1.2.0). Thresholds pending Viktor (named keys; separate restic/PBS pairs). OPEN: UI render unit-only (password-gated), alert emails unit + red-proof only (both nominal). See hub v0.64.0 + v0.65.0 REPORTs | -| R-6 | **Spike: LAN service discovery from the guest** — SSDP multicast (UDP 1900, DLNA), WSD (Windows discovery), mDNS; host-network vs macvlan; is the customer LXC LAN-bridged in appliance deployments? | M | idea | Shared prerequisite for R-7 + R-8. Spike-first: the traps are networking, not Samba/DLNA | -| R-7 | SMB **server** share (gated on R-6): samba + wsdd, LAN-only binding (never tunnel), user model (single household user first), which roots are shared (dedicated `shares/` vs app userdata — every SMB-writable path needs a backup class), uid-1000 convention, paperless consume flow | L | idea | Flips map row E/MISSING. Alpha-relevant: "my box is a NAS" is a core household expectation | -| R-8 | DLNA (gated on R-6): validate Jellyfin's built-in DLNA server first; only add minidlna to the catalog if Jellyfin-DLNA fails | S | idea | Don't add catalog weight before proving the cheap path | +| R-6 | **Spike: LAN service discovery from the guest** — SSDP multicast (UDP 1900, DLNA), WSD (Windows discovery), mDNS; host-network vs macvlan; is the customer LXC LAN-bridged in appliance deployments? | M | **spiked (2026-07-18)** | **VERDICT: appliance guest IS LAN-bridged (own DHCP lease on the household /24); multicast discovery works ONLY in the guest netns — guest-direct or Docker `--network host` (SSDP/mDNS/WSD all PASS both ways); the default docker bridge is categorically DEAF to LAN multicast (WSD/mDNS RX FAIL, unicast-publish PASS). Real samba+wsdd on host-net → Windows 11 ProbeMatch + 445 + authenticated SMB round-trip all PASS; real SSDP `MediaServer:1` advert reaches both LAN clients. → R-7 SMB stack MUST be host-network LAN-bound; R-8 Jellyfin-DLNA plausible if host-network. Caveat: `vmbr0 multicast_snooping=1` worked only because the household router is a live querier — customer LANs w/ snooping+no-querier, and Peti's BYO bridge, are UNTESTED gaps.** Doc: `audits/SPIKE-lan-discovery-2026-07-18.md`. Open sub-leg: S4.4 Explorer render (human, pending Viktor) | +| R-7 | SMB **server** share (**gate input now exists — R-6 spiked 2026-07-18: host-network, LAN-bound**): samba + wsdd, LAN-only binding (never tunnel), user model (single household user first), which roots are shared (dedicated `shares/` vs app userdata — every SMB-writable path needs a backup class), uid-1000 convention, paperless consume flow | L | idea (unblocked) | Flips map row E/MISSING. Alpha-relevant: "my box is a NAS" is a core household expectation. **R-6 settled the transport: `network_mode: host`; carry the port-collision + snooping/querier caveats from the spike into the spec** | +| R-8 | DLNA (**gate input now exists — R-6 spiked 2026-07-18: SSDP reaches LAN clients from host-net**): validate Jellyfin's built-in DLNA server first; only add minidlna to the catalog if Jellyfin-DLNA fails | S | idea (unblocked) | Don't add catalog weight before proving the cheap path. **R-6 confirmed the cheap path is physically viable — Jellyfin DLNA must run host-network (same multicast constraint as R-7)** | | R-9 | Uninstaller trio (from 07-15 Peti session): cluster-aware `felhom_guests` guard (node-local `pct list` deletes cluster-wide pveum objects); saferemove detection + time estimate + opt-in `--quick-remove` (never mutate `storage.cfg`); smarter `restore_storage` default for BYO clusters (shared storage, not local-lvm) | M | idea | Second item's rejected alternative (temp-disable-and-restore) stays rejected — crash window silently downgrades cluster wipe policy | | R-10 | T-6E-1: DB-dump dir-fsync asymmetry (LOW, confirmed in 6E) | XS | idea | One-line hardening; batch with the next controller task | | R-11 | Tester-facing one-pager: what the box does, known limitations, how to report (channel decision: Messenger group?) | S | idea | Pairs with R-3 |