5fc09aef042c8cde22c205a946bc77f853f6185a
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
af8bac4bf3
|
Stop orphaning go2rtc, serve cameras sooner, keep its config private
Three defects found by running a real WebRTC viewer through the whole stack for the first time (continuum-backend/scripts/e2e-camera.sh). 1. go2rtc was orphaned on every ordinary stop. `kill_on_drop(true)` was set, but main handled no signals, so SIGTERM - what `kill`, `docker stop` and a service manager send - ended the process on the spot with no destructor run and the child was never dropped. The orphan keeps :1984 and :8555 bound, so the restarted proxy cannot start its own, and it keeps a printer's access code in memory. One had been running for 4.5 days. main now selects on SIGTERM/SIGINT so it returns normally and the supervisor is dropped. A SIGKILL or OOM kill still orphans it; PR_SET_PDEATHSIG would cover that but needs unsafe and a libc dependency, so it is left for a decision. 2. A camera was advertised before go2rtc could serve it. The comment on REBUILD_INTERVAL said the first useful rebuild is "a few seconds after startup", but a tokio interval ticks immediately - with the fleet still empty - and the next tick was 30s away. For that window the cloud offered a camera and go2rtc answered "stream not found". Polling every 5s costs nothing: it reads in-memory reports and only restarts go2rtc when the rendered config changes. 3. The generated go2rtc.yaml was created 0644 by tokio::fs::write. It holds the printer's access code and go2rtc's API password, so any local user could read both. It is now created owner-only and fresh, because a temp file left by a crash would otherwise keep its old mode. The new test pre-creates a stale 0644 temp file; against the old implementation it fails with mode 420 (0o644) where 384 (0o600) is expected. cargo test: 193 lib tests and all integration tests pass. |
||
|
|
1a0037af36
|
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. |
||
|
|
d13284b585
|
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. |
||
|
|
70763108f8
|
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. |
||
|
|
51ff768153
|
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.
|
||
|
|
3b93a1b4e1
|
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. |
||
|
|
6a0d80a381
|
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. |