Skip to content

feat: honor codec preference when selecting variants from a multicodec HLS source - #92

Merged
birme merged 2 commits into
mainfrom
issue-65/codec-preference
Sep 11, 2026
Merged

birme merged 2 commits into
mainfrom
issue-65/codec-preference

Conversation

@birme

@birme birme commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Changes

  • Add OPTS_CODEC_PREFERENCE (avc1/hvc1) to keep only the preferred video codec family from a multicodec source master, so bandwidth buckets never mix codecs before reaching @eyevinn/hls-vodtolive.
  • Filter at VOD-load time inside docker-fast via a self-hosted /codecfilter/master.m3u8 endpoint; variant URIs are absolutized so segment resolution still targets the upstream origin.
  • Default is off — single-codec sources and existing behavior are unaffected.
  • Flip the chore: obtain multicodec HEVC+AVC HLS fixture and add a failing repro test #63 repro test to assert advertised CODECS matches served-segment codec for every variant; add avc1/hvc1/passthrough cases.
  • Document OPTS_CODEC_PREFERENCE and OPTS_MASTER_FILTER_BASE_URL in docs/plugins.md.

Design note: the upstream engine fetches VodResponse.uri itself and exposes no per-VOD codec-filter input (confirming the #64 trace finding), so the only in-docker-fast interception point is rewriting the VOD URI to a filtered master served locally — hence the small proxy endpoint rather than an engine option.

Test plan

  • PROOF npm run lint → eslint . → no errors
  • PROOF npm run pretty → All matched files use Prettier code style!
  • PROOF tsc --project ./ → BUILD_EXIT=0
  • PROOF npm test → Test Suites: 2 passed; Tests: 8 passed — including "FIXED: every advertised CODECS matches the served-segment codec" and "leaves a single-codec master unchanged (no preference set)"

Closes #65

🤖 Automated via Channel Engine Dev daily-backlog-pr skill

…c HLS source (closes #65)

Multicodec HLS masters that advertise both avc1 and hvc1 variants at the same
bandwidth caused the engine to advertise one codec while serving the other,
because @eyevinn/hls-vodtolive indexes variant playlists by bandwidth only.

Filter the source master to a single codec family at VOD-load time inside
docker-fast, before it reaches the engine, so bandwidth buckets never mix
codecs. Controlled by OPTS_CODEC_PREFERENCE (avc1/hvc1), defaulting to off so
single-codec sources are unaffected. Flips the #63 repro test to assert every
advertised CODECS matches the served-segment codec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@birme birme left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr-reviewer verdict: REQUEST CHANGES

The functional side is solid — npm ci/lint/pretty clean, npm test green (8/8), the #63 multicodec repro is genuine (not tautological), the feature defaults OFF, and single-codec masters are provably untouched. But there is a critical security hole that blocks merge:

  • critical — src/server.ts /codecfilter/master.m3u8?src=<url>: the handler does await fetch(src) on a fully attacker-controlled src with no validation (no scheme allowlist, no block on loopback/private/link-local/metadata ranges like 169.254.169.254, 127.0.0.1, 10/8, 192.168/16), and echoes the upstream body back verbatim. This is a read-SSRF / open proxy. The route is registered unconditionally (live even when OPTS_CODEC_PREFERENCE is unset), and the UI server binds 0.0.0.0. Pre-PR this port only served static UI files — this PR introduces the outbound-fetch surface.
  • minor — src/server.ts 500 handler: returns the raw error message to the client, acting as an SSRF oracle. Return a generic error; log detail server-side only.

Required before approval:

  1. Validate src before fetching: http(s)-only scheme; reject private/loopback/link-local/metadata address ranges (re-check post-DNS-resolution to avoid rebinding); and/or restrict to an operator-configured origin allowlist.
  2. Stop echoing raw error messages to the client.
  3. Recommended (not strictly blocking): only register /codecfilter when OPTS_CODEC_PREFERENCE is set, and/or bind the UI server to loopback unless explicitly exposed.

Everything else (tests, lint, defaults-off, non-tautological repro, commit message, no trademarks) passes — re-review should be quick once src validation lands.

🤖 Automated via Channel Engine Dev daily-backlog-pr skill

)

Guard the self-hosted /codecfilter master endpoint against read-SSRF /
open-proxy abuse:

- Add validateSourceUrl (src/plugins/utils.ts): accepts only http/https,
  rejects hosts in private/loopback/link-local/unique-local/metadata ranges
  (incl. IPv4-mapped IPv6), and resolves the hostname (dns.lookup all) to
  re-check every address. Returns a node-fetch agent that pins the connection
  to the validated IP, defeating DNS rebinding.
- Wire the validator into src/server.ts before fetching; only register the
  route when a codec preference is configured, and return a generic error
  string to the client (log detail server-side only) so it cannot act as an
  SSRF oracle.
- Add jest tests for the validator (mocked dns).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@birme birme left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pr-reviewer verdict: APPROVE

Independent re-review of the SSRF fix (fa2dd99) on top of the codec-preference feature (06e5008). The prior REQUEST CHANGES critical (read-SSRF / open-proxy on /codecfilter/master.m3u8?src=) is resolved.

Deterministic proof (all on branch issue-65/codec-preference, HEAD fa2dd99)

  • PROOF: npm run linteslint . → LINT_EXIT=0 (no errors)
  • PROOF: npm run prettyAll matched files use Prettier code style! → PRETTY_EXIT=0
  • PROOF: npm run build (tsc --project ./) → BUILD_EXIT=0
  • PROOF: npm testTest Suites: 3 passed, 3 total; Tests: 28 passed, 28 total (incl. validate_source_url.test.ts and the multicodec CODECS-match assertions)

SSRF verification (I probed the compiled validator with a mocked resolver — no bypass found)

  • PROOF: node probe → literal 127.0.0.1, 0.0.0.0, 10.x, 172.16-31.x, 192.168.x, 169.254.169.254, [::1] all → REJECT. 172.15.0.1 (public) correctly ACCEPT — range boundaries are exact.
  • PROOF: alternate IP encodings decimal 2130706433, octal 0177.0.0.1, hex 0x7f000001/0x7f.0.0.1, short 127.1, metadata decimal 2852039166 → all REJECT (Node's URL parser normalizes to dotted-quad, then net.isIP + range check catches them).
  • PROOF: IPv4-mapped IPv6 both forms [::ffff:127.0.0.1] and [::ffff:7f00:1] → REJECT (normalizeAddress unwraps both). IPv6 zone-id [fe80::1%eth0] → REJECT (URL parse fails). Trailing-dot host localhost. resolving to metadata → REJECT. userinfo trick expected@127.0.0.1 → REJECT (host parsed as 127.0.0.1). 127.0.0.1@evil.com → host correctly parsed as evil.com (that IS the real host; validated/pinned normally).
  • Redirect / DNS-rebinding analysis (node-fetch v2, default redirect:'follow'): the connection-pinning agent is passed as a FUNCTION and node-fetch re-invokes agent(parsedURL) on every redirect hop (lib/index.js:1363-1366, reused via agent: request.agent at :1557). The custom lookup is hostname-agnostic and always returns the ORIGINAL validated IP, so a 3xx redirect to an internal hostname (e.g. 169.254.169.254) is NOT independently resolved — the socket still targets the pinned public IP. The "redirect rebinds to a freely-resolved new host" concern does not apply here. Fail-closed throughout (unresolvable host, any-private-among-all, unknown IP kind → reject).
  • Route now gated behind getCodecPreference() being set (server.ts), shrinking attack surface to when the feature is enabled; on rejection a generic Source URL not permitted is returned and detail is logged server-side only — no SSRF oracle. Other fetch/resolveRedirect call sites (loop/webhook/playlist/barker) consume operator-configured URLs, not per-request attacker input, so they are out of scope for #65.

Checklist

  • Trademarks: none in the diff (grep for known commercial/DRM names → 0 hits).
  • Conventions: matches repo eslint/prettier/jest/tsc; node-fetch v2 agent usage is correct.
  • Commit messages: feat: honor codec preference … (closes #65) and fix: validate and pin codecfilter source URL to prevent SSRF (closes #65) — both conform.

Minor (non-blocking) suggestions for a follow-up, not required for merge:

  • suggestion src/plugins/utils.ts:578 — pinning to resolved[0] only; if that single address later becomes unreachable there's no failover, but all addresses were validated so it is safe. Consider pinning to the full validated set.
  • suggestion server.ts — consider redirect:'manual' + explicit re-validation per hop for defense-in-depth, though the hostname-agnostic pinned lookup already blocks the rebinding path.

No critical or major findings remain. Approving.

@birme
birme merged commit b5c2eaf into main Sep 11, 2026
3 checks passed
@birme
birme deleted the issue-65/codec-preference branch September 11, 2026 07:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: honor codec preference when selecting variants from a multicodec HLS source

1 participant