docs(playback): correct the claim that restream fingerprints are ready to consume

Three documents asserted that the re-stream heuristic just needs a consumer
because `ClientIP` and friends "already flow through streammonitor". Planning
Batch 6 against the actual code showed that is false, and it is exactly the
class of overstatement this audit exists to correct.

`ClientIP` is one value per session, not a set:

- Integrated mode stamps it at session creation and no media request updates
  it — `internal/api/handlers/stream.go` never touches `ClientIP`.
- The edge tracker overwrites `records[sessionID]` on every `Track` /
  `EnsureEphemeral`, so concurrent pours leave only the last writer's address.
- `mergeStreams` then collapses multi-node records to a single winner and only
  backfills an address when the winner has none.

So several devices pulling one session present as a single address, and a
consumer polling the existing snapshot cannot see fan-out at all. Detecting it
requires bounded viewer observations collected at authenticated media-serve
time.

Also records two limits worth knowing before anyone scopes this again:

- An IP-based signal can only catch shared session-URL fan-out (C16). C14 —
  a downstream proxy re-broadcasting one pulled stream — is invisible by
  construction, because Silo sees exactly one address no matter how wide the
  fan-out. C14 stays scored as no defense.
- `internal/proxy/server.go`'s `edgeClientIP` reads `RemoteAddr` and ignores
  `X-Forwarded-For`, unlike the native surface which uses the trusted-proxy
  resolver in `internal/clientip`. Behind ingress every edge viewer collapses
  to the ingress address, so the signal is blind there until that is fixed.

No behavior change; documentation only.

Part of #305
This commit is contained in:
CoffeeKnyte
2026-07-31 00:59:48 +00:00
parent 5b72176142
commit 2bbe60219f
3 changed files with 28 additions and 8 deletions
@@ -244,7 +244,7 @@ Captured per live stream (`streammonitor.LiveStream` / `nodesessions.SessionInfo
| play method (direct/remux/transcode) | `Type` | primary |
| **route (native/jellycompat)** | `Route` — `Session.Origin` (integrated) or token `Origin` claim (edge) | primary; native and jellycompat share the SessionManager and `Type`, so route is a distinct field seeded at the two `WithClientInfo` sites and carried to edges in the token |
| node serving | NodeName/NodeURL | integrated stamps the local host |
| client ip / client name | `Session.ClientIP/ClientName` (integrated) or edge request + `ClientName` claim | secondary; also the natural re-streaming fingerprint |
| client ip / client name | `Session.ClientIP/ClientName` (integrated) or edge request + `ClientName` claim | secondary. **Not usable as a re-streaming fingerprint as-is:** it is one value per session, stamped at session creation (integrated) or overwritten per request (edge), so concurrent viewers of one session collapse to a single address. A restream heuristic needs per-request observation at serve time — see abuse-matrix correction #9 |
| position | `Session.Position` | secondary timing |
| bytes served | edge `AddBytes` / integrated `SessionMeteredWriter` | server-observed throughput signal |
| hw/sw, resolution, codecs | Session / node record | secondary |
+19 -4
View File
@@ -329,8 +329,19 @@ Enforcement ⚠️ (fails open at the edge).**
The classic Stremio/Plex-share abuse: one account pulls a single stream and a proxy
fans it out to hundreds. It stays at concurrency 1, so admission and the enforcer
never fire, and the re-streaming heuristic that would catch it *does not exist*
(correction #2). The fingerprints to build it (`ClientIP`, `ClientName`,
`BytesServed`, distinct `MediaFileID`) are captured but unused. **Detection ❌ /
(correction #2). **Correction #9 (2026-07-31): the fingerprints are NOT ready to
consume, contrary to what this document and the architecture plan previously said.**
`ClientIP` is a *single* value per session, not a set: integrated mode stamps it once at
session creation and no media request ever updates it
(`internal/api/handlers/stream.go` never touches `ClientIP`), while the edge tracker
overwrites `records[sessionID]` on every `Track`/`EnsureEphemeral`
(`internal/nodesessions/tracker.go`). `mergeStreams` then collapses multi-node records
to one winner and only backfills an IP when the winner has none. So five devices pulling
one session still present as one address, and a polling consumer cannot see fan-out at
all. Building the heuristic requires collecting bounded viewer observations **at
authenticated media-serve time**, not consuming the existing snapshot. Note also that C14
proper — a downstream proxy re-broadcasting one pulled stream — is invisible to any
IP-based signal by construction: Silo sees exactly one address. **Detection ❌ /
Enforcement ❌.**
**C15. Many concurrent streams feeding a restream service (over cap).**
@@ -612,8 +623,12 @@ Then, as originally scoped:
connection cap / `LimitListener` (E25–E28).
4. **Add a per-node concurrent-transcode cap** and wire `nodepool.MaxJobs` defaults
(E29).
5. **Implement the restream heuristic** (C14/C16): the fingerprints already flow
through `streammonitor`; a distinct-viewer-IP rule is the missing consumer.
5. **Implement the restream heuristic** (C16 fan-out; **not** C14): the fingerprints do
**not** already flow in usable form — see correction #9. A distinct-viewer-IP rule
needs per-request observation at media-serve time plus a trust-boundary-correct client
address at the edge (`internal/proxy/server.go`'s `edgeClientIP` reads `RemoteAddr`
and ignores `X-Forwarded-For`, so behind ingress every viewer collapses to one
address).
Decisions are now settled: distinct viewer IPs per session as the signal, Redis
with a ~24h TTL when configured and in-memory otherwise, detection **on by
default and alert-only**, with auto-kill behind an operator setting that defaults
@@ -185,9 +185,14 @@ and direct-play/stream egress:
### Pillar 4 — Detection heuristics & perimeter (C14, E21–E29) — partly new, partly gap-closing
- **Re-streaming heuristic (C14/C16) — build it (it does not exist; matrix
correction #2).** The fingerprints already flow through `streammonitor`
(`ClientIP`, `ClientName`, `MediaFileID`, `BytesServed`). Add a consumer rule:
- **Re-streaming heuristic (C16 fan-out only — C14 is undetectable this way) — build it
(it does not exist; matrix correction #2).** ⚠️ **Superseded by abuse-matrix correction
#9 (2026-07-31): the fingerprints do NOT already flow in usable form.** `ClientIP` is a
single value per session — stamped once at session creation in integrated mode,
overwritten per request at the edge — so concurrent viewers collapse to one address and
no consumer of the existing snapshot can see fan-out. It needs per-request observation
at media-serve time. The rule sketched below is retained for its shape, not its
feasibility claim:
distinct client-IPs per `sid`, sustained throughput vs. media bitrate, distinct
concurrent titles per `uid`. Output = a flag → Tier-1 demotion. Ship **disabled**;
tune against real traffic before enabling (false-kill risk).