Skip to content

chore: adopter quick fixes — portable defaults, public-IP sweep, srt-listen docs, proxy-trust opt-in - #38

Draft
guycochran wants to merge 1 commit into
masterfrom
chore/adopter-quick-fixes
Draft

guycochran wants to merge 1 commit into
masterfrom
chore/adopter-quick-fixes

Conversation

@guycochran

Copy link
Copy Markdown
Owner

⚠️ Deploy note — set MXL_TRUST_PROXY=1 on any deployment behind a proxy/tunnel BEFORE this lands

backend/mxl-auth.js now ignores CF-Connecting-IP / X-Forwarded-For by default. A backend that sits behind cloudflared (or any reverse proxy) must set MXL_TRUST_PROXY=1 (the old MXL_TRUST_PROXY_HEADERS=1 also works) in its service environment first. Otherwise every visitor shares the proxy's single IP for the /repair rate limit and the failed-auth lockout. One bad actor could then lock everyone out of control routes for the lockout window.

Other deploy checks for an existing facility:

  • scripts/bring-up-mxl.sh now defaults MXL_BACKEND_URL → http://127.0.0.1:3100 and MXL_FEED_URL → http://127.0.0.1:8889. Set both explicitly in the facility env.
  • web/commentary.html now posts WHIP to the same-origin /mxlfeed/webrtc2mxl*/whip. Confirm the proxy forwards WHIP POST/PATCH/DELETE on /mxlfeed/.
  • The kiosk DOMAIN link in web/mxl.html now points at the serving host on :9608.
  • The kiosk's Larix QR images now come from GET /api/mxl/ingest/qr/guestN.png. That route is served by backend/local-server.js. On a backend that doesn't serve it, the QR is hidden and the typed SRT details still show.

What

These are small adopter-facing fixes from a review of the public core. There are no media-path changes.

  1. Static QR images removed. web/qr-larix-guest1.png / guest2.png encoded one fixed facility address. web/mxl.html now loads the per-box QR from the existing /api/mxl/ingest/qr/guestN.png route and hides it (onerror) if unavailable.

  2. Routable IP → placeholder in docs/GCP-BUILD-PLAN.md, using RFC 5737 203.0.113.10.

  3. Tripwire extended in tests/test_no_hardcoded_hosts.py:

    • The site-hostname / IP check now also covers web/ (vendored JS excluded). Public project-site links are allowlisted with reasons. Stale allowlist entries were removed, since the allowlist only ever shrinks.
    • New repo-wide public-IPv4 sweep over every tracked text file (docs, README, web, code; tests/ and vendored JS excluded). It allows RFC 1918, loopback, link-local, unspecified, CGNAT, and the RFC 5737 placeholder ranges, and has a self-test.
    • New guard: no QR image may be committed under web/ again. A QR hides its address in pixels, where the text scans can't see it.
    • Both new tests were checked to fail when the old file and old IP are put back.
  4. Docs match the srt-listen default:

    • README.md: Guest 1 on :8890, Guest 2 on :8891, Caller mode, no stream ID.
    • docs/QUICKSTART.md: open UDP 8890 and 8891; no stream ID in listen mode; srt-direct described as the opt-in.
    • SECURITY.md: in listen mode the passphrase options are not applied, so restrict the UDP ports in the firewall.
  5. Defaults no longer point at a specific facility. MXL_BACKEND_URL now defaults to http://127.0.0.1:3100 (the local control plane) in:

    • tools/layout_pgm.py, audio_pgm.py, mxl_multiview.py
    • selector-doctor.sh, guest-leg-doctor.sh
    • scripts/bring-up-mxl.sh
    • the Companion module's editable base URL

    Also changed:

    • MXL_FEED_URL → http://127.0.0.1:8889.
    • Commentary WHIP and the DOMAIN link are same-origin / serving-host. On purpose, neither takes a host from the query string: a crafted link must not be able to redirect a commentator's mic or inject a link.
    • docs/CONFIG.md updated, and tests/test_env_config.py now pins the new portable defaults.
  6. Proxy-header trust is opt-in (backend/mxl-auth.js). The MXL_TRUST_PROXY=1 setting (alias MXL_TRUST_PROXY_HEADERS=1) is documented in docs/CONFIG.md and SECURITY.md. A new test shows that rotating X-Forwarded-For no longer dodges the failed-auth lockout by default.

  7. CONTRIBUTING.md: removed the claim that CI checks doc links. It doesn't.

Tests

  • node --test tests/js/*.test.js → 38/38 pass (was 36; +2 proxy-trust tests).
  • python -m pytest tests/ → 136 passed, 2 skipped (+3 new tripwire tests).
  • bash -n is clean on all shell scripts, and node --check passes on the Companion module.

Follow-ups (deliberately not in this PR)

  • tools/nmos_node.py still defaults --facility / MXL_BACKEND_URL to the original facility host. It's skipped here because an open PR is rewriting that file. Flip it to http://127.0.0.1:3100 after that lands, then drop its allowlist entry.
  • web/lower-third.html polls the backend cross-origin from inside the keyer, so it needs a configurable host (allowlisted for now).
  • The exported Companion page (*.companionconfig), and the comment examples in scripts/mxl-doctor / tools/contribution_core.py.
  • An SRT passphrase on the srt-listen listener itself, so listen mode can be gated, not just firewalled.

@guycochran

Copy link
Copy Markdown
Owner Author

Reviewed — this is a clean adopter/security hardening pass, no media-path changes, well-scoped. The proxy-trust default flip (opt-in MXL_TRUST_PROXY=1 instead of trusting client-controlled X-Forwarded-For by default) is the right call. 👍

Two interaction points with the open contribution PR (#37), which I'll handle on #37 so this PR stays clean:

  1. tools/nmos_node.py facility default (your flagged follow-up): I'll flip --facility / MXL_BACKEND_URL from the facility host to http://127.0.0.1:3100 on contribution: 60fps restamp fix (HW-verified) + IN-005 registry/soak + IS-05 shim + ANC data-flow + v1.1.0 pin #37 (it owns that file now), and drop the need for its allowlist entry. So it'll be done by the time contribution: 60fps restamp fix (HW-verified) + IN-005 registry/soak + IS-05 shim + ANC data-flow + v1.1.0 pin #37 lands — no second pass needed.

  2. docs/CONFIG.md overlap (the only file both PRs touch): contribution: 60fps restamp fix (HW-verified) + IN-005 registry/soak + IS-05 shim + ANC data-flow + v1.1.0 pin #37 added an IN-005 ingress-registry section and still documents the OLD proxy default (MXL_TRUST_PROXY_HEADERS … | 1 (on)) — which your PR correctly flips to unset/off. I'll remove that stale proxy-trust line from contribution: 60fps restamp fix (HW-verified) + IN-005 registry/soak + IS-05 shim + ANC data-flow + v1.1.0 pin #37's CONFIG.md so your authoritative version wins; contribution: 60fps restamp fix (HW-verified) + IN-005 registry/soak + IS-05 shim + ANC data-flow + v1.1.0 pin #37 keeps only its additive IN-005 block. That should leave no real conflict — if there's a textual one at merge, mine yields to yours on the proxy-trust row.

Whichever merges first, the other rebases cleanly with those two adjustments. I'd suggest #38 merges first (it's small, security-relevant, and independent); #37 then rebases trivially.

@guycochran

Copy link
Copy Markdown
Owner Author

Correction to my previous comment — I re-checked the actual diffs and overstated the CONFIG.md overlap:

Net: the two PRs are effectively independent. Either merge order works with a trivial auto-merge. 👍

guycochran added a commit that referenced this pull request Oct 8, 2026
…s follow-up

PR #38 (adopter hardening) swept MXL_BACKEND_URL defaults off the facility host but skipped
tools/nmos_node.py because this PR rewrites it. Doing it here so it's resolved when #37 lands:
--facility / MXL_BACKEND_URL now default to the LOCAL control plane http://127.0.0.1:3100, not
a specific facility (a cloned core must not point at someone else's backend). Override with
--facility / MXL_BACKEND_URL for a real deployment.

After this, nmos_node.py has no prodbots.com reference; #38's tripwire allowlist entry for it
(tests/test_no_hardcoded_hosts.py) is stale and can be dropped (harmless if left — allowlists
over-permit). IS-05 tests set facility explicitly, unaffected. 152 py + 36 node green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwSKEZZz4yEc2CGq5hcTmT
guycochran added a commit that referenced this pull request Oct 8, 2026
…ity-default flip)

The prior commit removed prodbots.com from tools/nmos_node.py (portable --facility default),
which made its tripwire allowlist entry stale → test_allowlist_has_no_stale_entries failed (the
ratchet: the allowlist only shrinks). Remove the entry. This is exactly PR #38's "then drop its
allowlist entry" follow-up, done here since #37 owns the reason.

⚠️ COORDINATION: PR #38 also edits tests/test_no_hardcoded_hosts.py (new IPv4 sweep + QR guard).
This is a 1-line deletion of the nmos_node.py ALLOW row; it should not collide with #38's
additions, but whoever merges second should confirm the row is gone (not re-added).

152 py + 36 node green (full suite clean — the prior push had this one test red; fixed here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwSKEZZz4yEc2CGq5hcTmT
@guycochran

Copy link
Copy Markdown
Owner Author

One more accurate note (I was wrong that the allowlist entry was purely your follow-up):

Flipping nmos_node.py to the 127.0.0.1:3100 default removed its last prodbots.com reference, which made tests/test_no_hardcoded_hosts.py's test_allowlist_has_no_stale_entries fail on #37 (the ratchet fires on stale entries, not just on merge). So I had to drop the "tools/nmos_node.py": {"prodbots.com"} allowlist row on #37 to keep its suite green — can't defer it.

⚠️ This means #37 now also edits tests/test_no_hardcoded_hosts.py (a 1-line deletion of that ALLOW row) — a file your PR rewrites heavily. It shouldn't collide with your additions (IPv4 sweep / QR guard), but whoever merges second should confirm that nmos_node.py row stays gone, not re-added. If #38 merges first with the row still present, #37's deletion applies cleanly on rebase; if #37 merges first, make sure your #38 version doesn't reintroduce it.

Everything on #37 is green again (152 py + 36 node).

…docs, proxy-trust off

- Remove the two static Larix QR PNGs from web/ (they encoded a fixed facility
  address); web/mxl.html now loads the per-box QR from
  GET /api/mxl/ingest/qr/guestN.png and hides it if unavailable.
- docs/GCP-BUILD-PLAN.md: replace a routable IP with an RFC 5737 placeholder.
- tests/test_no_hardcoded_hosts.py: scan web/ for site hosts too; add a
  repo-wide public-IPv4 sweep (docs, README, web, code; RFC 1918 / loopback /
  RFC 5737 allowed) and a guard against committing static QR images in web/.
- Docs match the srt-listen default: README, QUICKSTART (open UDP 8890 AND
  8891, no stream id in listen mode), SECURITY (passphrase not applied in
  listen mode — restrict the ports).
- Tool/web defaults no longer point at a specific facility: MXL_BACKEND_URL
  defaults to http://127.0.0.1:3100, MXL_FEED_URL to http://127.0.0.1:8889;
  commentary WHIP goes via the same-origin /mxlfeed/ proxy; the DOMAIN link
  uses the serving host :9608. (tools/nmos_node.py left for a follow-up.)
- backend/mxl-auth.js: CF-Connecting-IP / X-Forwarded-For are trusted only
  when MXL_TRUST_PROXY=1 (alias MXL_TRUST_PROXY_HEADERS=1). Default OFF so a
  directly exposed server can't have its throttles dodged by spoofed headers.
- CONTRIBUTING.md: drop the claim that CI checks doc links.

Signed-off-by: Guy Cochran <guycochran@gmail.com>
@guycochran
guycochran force-pushed the chore/adopter-quick-fixes branch from 49ca43b to fd22b63 Compare October 8, 2026 02:24
@guycochran

Copy link
Copy Markdown
Owner Author

Coordination note re: #37. #37 (commits 2d49d7d + 4b49227) flips tools/nmos_node.py to the portable http://127.0.0.1:3100 default and deletes its line from the ALLOW block in tests/test_no_hardcoded_hosts.py. This PR rewrites that same block, so the two conflict.

This PR keeps the tools/nmos_node.py entry on purpose. On master, nmos_node.py still carries the old facility default, so dropping the entry here would make this PR's own CI fail: it is a ratchet allowlist.

Rule: whichever of #37/#38 merges second removes the "tools/nmos_node.py": {...} line while resolving the conflict. Everything else in both PRs merges cleanly.

Also: the commit here is now signed off (Signed-off-by) for the DCO check proposed in #39. The tree is unchanged; this was a force-with-lease push to this draft's own branch.

@guycochran

Copy link
Copy Markdown
Owner Author

Reviewed backend/mxl-auth.js closely (the security-critical bit) — LGTM, no blocking findings. The default flip is the right call: fail-closed (MXL_TRUST_PROXY unset ⇒ ignore CF-Connecting-IP/X-Forwarded-For, key on req.ip) closes the spoof-a-fresh-XFF-per-request bypass on a directly-exposed server. _truthy is null-safe and the MXL_TRUST_PROXY || MXL_TRUST_PROXY_HEADERS alias keeps existing =1 deployments working. Clean.

One non-blocking note (only relevant when trustHeaders is opted in): _clientKey takes X-Forwarded-For.split(',')[0] — the first hop, which a client can prepend/forge even behind a proxy. For the documented deployment (cloudflared tunnel → localhost-bound backend) this is fine because (a) you only opt in when every request truly transits the tunnel, and (b) CF-Connecting-IP is checked first and is the trustworthy value on Cloudflare. So it's correct as scoped. If you ever support a multi-hop / non-Cloudflare proxy chain, the hardened form is to count trusted proxies from the right end of XFF rather than trust [0]. Not worth changing now — just flagging for the SECURITY.md threat model.

Everything on the #37 side that interacts with your PR is settled (nmos facility default flipped, stale allowlist row dropped, CONFIG.md non-conflicting). No further action needed from me on #38.

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.

1 participant