Commit Graph
6 Commits
Author SHA1 Message Date
octoturgeandClaude Opus 5 ee737f2e47 Answer relayed WebRTC offers from the gateway's own go2rtc
Adds src/uplink/webrtc.rs: on a webrtc_offer from the cloud, POST the
offer to go2rtc on 127.0.0.1 and send the answer back as webrtc_answer,
or the reason as webrtc_error.

No credential is involved, because the request goes to loopback and
go2rtc exempts loopback from its API password. That is measured, and it
is also why a probe from the gateway cannot be used to check the
password is working - test from another host for that.

Spawned rather than awaited in the select! loop: go2rtc gathers ICE
candidates before answering, which includes a STUN round trip, so
awaiting inline would stall telemetry and heartbeats for a whole farm
while one person opened one camera. It rides the outbound queue added
earlier for exactly this kind of out-of-band reply.

The bind address is turned into a loopback URL by taking its port and
refusing anything it cannot read a port out of - guessing 1984 would
produce a gateway that silently signals to the wrong place when someone
has moved it. Stream names are validated rather than percent-encoded,
since a real one is a printer id and anything else is a bug or an
attempt at one.

camera.rs now states why there are no ice_servers and no TURN
credentials on this side: the viewer's browser is pinned to relay-only,
so it allocates the relay and go2rtc just sends to it. Only one side of
a session needs one. That asymmetry is load-bearing, not tidy - go2rtc
reads its config once at startup, so rotating short-lived credentials
here would mean restarting it and dropping every viewer on a timer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-19 00:32:57 +00:00
octoturgeandClaude Opus 5 a8f8dec955 Keep a test's doc comment attached to its test
Adding the test_auth helper put it between
`/// The URL shape confirmed from a real H2C's ipcam block.` and the test
that comment describes, so the comment documented the helper instead.
Clippy's empty_line_after_doc_comments caught it. Also takes clippy's
from_ref over a clone in the new render-stability test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-18 22:01:21 +00:00
octoturgeandClaude Opus 5 005b8a3337 Require a password on go2rtc's API
The API listener is what serves WebRTC, and it defaults to :1984 on every
interface, so anyone who could reach the gateway could watch any printer
with a camera. The RTSP server was closed on 2026-10-18; this was the
other half and the one that actually carries video.

Measured against go2rtc 1.9.14, feeding it this module's own rendered
config: from a non-loopback address /api/webrtc, /api/ws, /api/streams
and the web UI all answer 401 without credentials and 200 with them,
while :8554 stays closed. go2rtc exempts loopback, which is worth
knowing twice - it is why a probe from the gateway still works without
credentials, and why such a probe cannot be used to confirm auth is on.

The password is generated once per run from /dev/urandom, or taken from
CONTINUUM_GO2RTC_PASSWORD. Once per run matters: the config is
re-rendered every 30s and compared byte-for-byte to decide whether to
restart go2rtc, so a per-render credential would restart it and drop
every viewer every half minute, with no symptom but dying video. A test
pins that.

It is never logged - it goes into the config file, which already holds
every printer's access code and is already documented as a secret, so
there is one place to protect rather than two. An operator-supplied
password is escaped for YAML, because a config go2rtc cannot parse means
a camera that silently never appears.

No opt-out flag. The alternative is an unauthenticated video feed of
someone's workshop, which is not a thing to leave behind a setting, and
nothing in this crate calls go2rtc's API so nothing internal breaks.

Note this does NOT let a browser play a stream directly - see the next
commit's notes and PLAN.md. go2rtc sends no CORS headers and 401s the
unauthenticated preflight, so the relay is required, not optional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-18 22:00:51 +00:00
octoturgeandClaude Opus 5 2ca81401fc Close go2rtc's RTSP server, which was open all along
Found by installing go2rtc and running the real daemon against the live
H2C, which is also how the comment this replaces turned out to be wrong.

`camera.rs` said: "No `rtsp:` or `webrtc:` block: ... Opening go2rtc's
own RTSP server would publish every printer's camera on the LAN
unauthenticated, which is not something to do by default." The reasoning
was right and the conclusion was backwards. **Omitting the key does not
leave the server off - go2rtc defaults to :8554.** Measured against
go2rtc 1.9.14 with exactly the config this module generated:
`rtsp://<gateway>:8554/h2c` answered a DESCRIBE with a real video SDP,
with no credentials sent at all. Every printer with a camera was being
republished to anything that could reach the port.

The unit test asserted that the rendered config contained no `rtsp:`
key, and from that concluded no server was opened. It passed throughout.
A test that asserts a key is absent cannot tell "off" from "defaulted
on" - it now asserts the value that actually turns it off, and that an
empty fleet closes it too, since a gateway with no cameras is the config
most likely to be left running.

`webrtc:` is left at its default on purpose: it is not a second stream
server, because a session only exists after an SDP offer through the
API, and the browser needs those candidates. The API listener itself is
unauthenticated and stays that way for now - the browser reaches it
directly, there is no relay, and the frontend is handed only a URL and a
stream name - so anyone on the gateway's LAN can watch. That is a real
limit, now written down here and in PLAN.md rather than implied.

Two smaller things from the same session:

- The logged stream count was `matches("\n  ").count() - 1`, which
  reported one stream for a config with none: the `{}` of an empty
  mapping indents exactly like a stream does. It made the log say a
  camera was being served when none was, and it hid that the two writes
  on startup are genuinely different configs (empty, then the fleet once
  it reports) rather than a pointless rewrite. Counted from the sources
  now, collected once so the number describes the config actually
  written.

- Recorded what is now confirmed: go2rtc connects to
  `rtsps://bblp:<access_code>@<ip>:322/streaming/live/1` and pulls video
  - 984,604 bytes over `rtsp+tcp`, no certificate complaint, no auth
  failure. The printer runs LIVE555 and sends H.264 High profile level
  4.1, which a browser plays natively over WebRTC, so no transcoding and
  no ffmpeg on the gateway. The `rtspx://` prefix go2rtc documents for
  skipping TLS verification is not needed.

Verified after the fix with the real daemon: the generated config now
leaves :8554 closed while :1984 and the WebRTC port stay up. 65 tests,
clippy unchanged, licenses ok.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-18 13:43:19 +00:00
octoturgeandClaude Opus 5 9d046abf19 Confirm at the network layer that a P1P has no RTSP endpoint
`camera.rs` said a P1/A1 camera is reachable only over Bambu's
port-6000 JPEG stream, inferred from the absence of `rtsp_url` in what
the printer reports. Checked directly today: the H2C accepts TCP on 322
and the P1P *refuses* 322 while listening on 6000.

So the inference was right and is now a fact. Worth writing down,
because it closes off the tempting shortcut - there is no URL to guess
for a P1P, the endpoint does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 23:10:57 +00:00
octoturgeandClaude Opus 5 bda5e95b66 Generate go2rtc's config from what the printers report
go2rtc has been supervised since before this commit, with nothing to
serve: `CONTINUUM_GO2RTC_CONFIG` pointed at `./go2rtc.yaml`, and that
file did not exist in the repository or anywhere else. A watchdog was
restarting a process that had no cameras configured.

`camera.rs` writes it, from the `rtsp_url` a printer publishes in its own
`print.push_status` `ipcam` block rather than from `printers.toml`. The
URL is already something the machine tells us, so asking an operator to
keep a second copy of it - and to update it by hand when a printer's
address changes - would be asking for drift.

Two tasks, not one: the builder decides what should be served, the
watchdog keeps the thing serving it alive. They meet at one `Notify`,
because go2rtc reads its configuration only at startup, so a changed
config needs a restart. The watchdog now selects the child's exit against
that notification and skips its backoff on a deliberate change.

What this does *not* cover is most printers, and that is the honest
limit. Only the X1/H2 class publishes an RTSP URL - confirmed against two
real machines: the H2C reports
`rtsps://192.168.0.21:322/streaming/live/1`, the P1P reports an `ipcam`
block with `ipcam_dev: "1"` and no URL at all. "A camera exists" and "a
camera you can connect to" are different facts. A P1/A1 needs Bambu's
proprietary port-6000 JPEG stream, which this crate does not speak, so
those printers report no `camera_stream` and a UI should offer a player
only when one is present.

Details worth keeping:

- The config is rendered sorted so an unchanged fleet is byte-identical,
  and on the first pass it is compared against the file on disk - without
  that, every daemon restart rewrites an identical file and drops every
  live viewer.
- Written through a `.tmp` and renamed. The watchdog can restart go2rtc at
  any moment, and a half-written config fails to parse, which takes every
  working stream down rather than one.
- Never logged, and gitignored: the credentials go in the URL because
  that is the only place go2rtc takes them for an RTSP source, so the file
  holds printer access codes.
- A reported URL with no scheme is refused rather than repaired. One bad
  entry makes go2rtc fail to load the whole file.
- No `rtsp:` or `webrtc:` block. The API listener is what serves WebRTC to
  a browser, which is the only consumer here; opening go2rtc's own RTSP
  server would publish every printer's camera on the LAN unauthenticated.
- `stream_base_url` is reported in `hello`, not derived from the listen
  address: `:1984` is a bind address and not something a browser can open.
  Unset means "advertise nothing", which is the right default, because a
  wrong URL makes the UI offer a player that cannot connect. It is a LAN
  address either way - there is no relay, so this works for an operator in
  the workshop and not for anyone outside it.

63 tests, clippy unchanged at the two deliberate variant-size warnings,
`cargo deny check licenses` ok. go2rtc itself stays operator-installed and
is not distributed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-15 23:03:02 +00:00