You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a native Unity WebRTC example based on the existing FishNet Unity demo.
Add reliable and unreliable Unity.WebRTC data channels with Rivet gateway HTTP signaling under /request/.
Run native Unity peer negotiation on the Unity main loop so SetRemoteDescription, answer creation, and ICE gathering progress reliably in desktop and headless clients.
Default to Google and Cloudflare STUN, with runtime ICE overrides through environment variables and native client flags; no credentials are committed.
Resolve RTCTransportStats.selectedCandidatePairId and fail closed unless the active pair is nominated, succeeded, bidirectional, UDP, and uses only host/server-reflexive/peer-reflexive candidates.
Package the Linux dedicated server with Container Runner v2.3.3, the stable Cloud-compatible release identified in #rivet-triumph.
Document native-client, local-build, and Rivet Compute deployment workflows.
Validation:
Rebased on current main (6479a185).
Built the macOS native client and Linux dedicated server with Unity 6000.5.2f1.
Local native E2E passed with only stun:stun.l.google.com:19302 and stun:stun.cloudflare.com:3478; both peers logged TURN disabled.
The authoritative local selected pair was host/udp ↔ host/udp, nominated and succeeded, with bytesSent=1263 and bytesReceived=1770; the client logged ICE PROOF PASSED: native UDP P2P with no TURN relay.
Final linux/amd64 Docker build passed and contains Container Runner 2.3.3.
Deployed the audited STUN-only build with the ICE credential variables explicitly cleared.
Final PR-head image: pr5480-778c3669-stun-final (sha256:ac3faf5a4d43d94fab96fbe1033f257f928e8420081885873510cd7941abb15a).
Final PR-head Cloud actor p6963d1q1fhlqpiepx2jbn2d8ncl00 reproduced the result: both peers logged TURN disabled and gathered server-reflexive UDP candidates, then both transitioned from ICE Checking to Failed.
Fresh Cloud actor pqovsp0wbgshcifjp5p6lzxozjcl00: both peers gathered host and server-reflexive candidates with TURN disabled, but ICE transitioned from Checking to Failed; the client correctly logged ICE PROOF FAILED.
Diagnostic Cloud actor xcfpz8970r7fv226htk0mey4dxal00 reproduced the failure. The worker's public STUN mappings were not reachable by peer ICE checks, so a no-TURN Cloud path was not established from this native client/network.
A prior Cloud run succeeded only with authenticated TURN relay candidates; it is not evidence of native P2P.
Deployment note:
Use --drain-on-version-upgrade=false with Container Runner v2.3.3 for this project.
Native P2P works when the two peers' ICE candidates are directly reachable. This Rivet Cloud worker path currently requires TURN or a Cloud networking change that exposes a reachable UDP candidate.
This PR vendors a large third-party Unity project (FishNet + a forked FishyWebRTC transport) plus adds ~5 Rivet-authored C#/shell/Dockerfile files. I focused review on the non-vendored, Rivet-authored code and the docs/config changes; I did not line-by-line review the ~1,800 vendored FishNet/FishyWebRTC files themselves.
🚩 Unrelated, high-risk CLAUDE.md change
The diff adds this line to the top of CLAUDE.md, unrelated to the rest of the PR (a Unity WebRTC example):
+For all RivetKit work, use [https://rivet.dev/llms.txt](https://rivet.dev/llms.txt) as the current documentation index and reference.
CLAUDE.md instructions are treated by agents as overriding default behavior. A line that directs every future agent to fetch and treat an external, mutable URL's content "as the current documentation index and reference" is a real prompt-injection surface: whoever controls rivet.dev/llms.txt content going forward can silently steer every future agent session in this repo. Even setting aside injection risk, this is scope creep — it doesn't belong in a PR about a Unity WebRTC example, and per this file's own conventions section, CLAUDE.md additions should be "design constraints, invariants, and non-obvious rules," not a pointer to an external doc index. Recommend dropping this line from this PR (or, if genuinely wanted, landing it separately with its own review).
Correctness: blocking synchronous HTTP on the Unity main thread
Packages/com.skillcade.fishy-webrtc/Runtime/SimpleWebRTC/Client/SimpleWebRTCClientNative.cs, ConnectCoroutine calls SendHttp(...) (a synchronous HttpWebRequest.GetResponse() with up to a 30s timeout) directly inside a coroutine, twice: once for offer/ and once for answer/. Coroutines don't run on a separate thread — a coroutine only yields at explicit yield return, so this blocking call stalls Unity's entire main thread (rendering, physics, input) for however long the HTTP round trip takes, and the signaling code path explicitly waits up to 10s server-side for ICE gathering before responding, so this can be a multi-second full freeze. This directly undercuts the PR's stated goal of running "native Unity peer negotiation on the Unity main loop ... reliably." Consider UnityWebRequest (which is coroutine/async-friendly) or moving SendHttp onto a background thread with results marshaled back via the coroutine.
Docs vs. validated behavior mismatch
container-runner/examples/unity-webrtc-demo/README.md states under "ICE networking": "No TURN URL or credentials are required for the native P2P path." But this PR's own validation notes in the PR description show the opposite on Rivet Cloud: the STUN-only build failed ICE (Checking → Failed) against a fresh Cloud actor, and only succeeded with authenticated TURN relay candidates. The README should reflect this (e.g., "STUN-only P2P works locally; Rivet Cloud currently requires TURN or a networking change — see PR discussion") so a reader deploying to Cloud doesn't hit a confusing failure the author already reproduced and diagnosed.
Supply-chain: unverified runner binary download
container-runner/examples/unity-webrtc-demo/Dockerfile diverges from the sibling unity-demo/Dockerfile, which builds rivet-container-runner from source in-workspace. This Dockerfile instead does:
This downloads and directly executes (as ENTRYPOINT) a prebuilt binary with no checksum/signature verification. Given releases.rivet.dev is first-party, risk is limited, but it's still a divergence from the from-source pattern used elsewhere in the same directory, worth at least a one-line comment explaining why this example uses the prebuilt release instead of building from source (e.g. to avoid needing the full Rust workspace for a Unity-only example), and ideally a checksum pin alongside the version pin.
Licensing: vendored fork missing LICENSE file
Packages/com.skillcade.fishy-webrtc/ is a fork of cakeslice/FishyWebRTC (MIT, per its own README prose) vendored at a pinned commit. Unlike the neighboring com.firstgeargames.fishnet/ package, which ships LICENSE.txt copies, this package includes no LICENSE file anywhere in the added file set — only a one-line "Based on ... MIT License" mention inside README.md. Worth adding the actual MIT license text as a LICENSE file for compliance, matching how FishNet is vendored.
Minor / informational
Runtime/SimpleWebRTC/Server/WebRTCServer.cs assigns connection IDs as a small sequential ushort counter with no secret component, and SendAnswerThread accepts an /answer/ POST for any connId with no additional binding to the original /offer/ requester. In this deployment this is protected by Rivet's gateway token in the URL path before traffic ever reaches the actor, so severity is low, but it's worth a one-line note in the README about that trust assumption for anyone reusing this signaling server outside Rivet's gateway.
The vendored package's own README.md (com.skillcade.fishy-webrtc/README.md) is unmodified upstream content full of Edgegap/WSL2-specific deployment instructions unrelated to Rivet. Not a blocker since it's vendored as-is, but readers browsing Packages/ could be misled into thinking Edgegap-specific env vars (ARBITRIUM_PUBLIC_IP, etc.) apply here.
RedactToken in ServerBootstrap.cs is a nice touch — the gateway URL's <token> is correctly scrubbed before it's logged (Debug.Log($"[bootstrap] starting WebRTC CLIENT -> ...")), avoiding credential leakage into build/Cloud logs.
Not reviewed in depth
The ~1,800 vendored FishNet and FishyWebRTC files (code-gen helpers, editor tooling, WebGL client/JS interop, etc.) were not reviewed line-by-line, consistent with them being third-party code pulled in wholesale rather than authored for this PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
/request/.SetRemoteDescription, answer creation, and ICE gathering progress reliably in desktop and headless clients.RTCTransportStats.selectedCandidatePairIdand fail closed unless the active pair is nominated, succeeded, bidirectional, UDP, and uses only host/server-reflexive/peer-reflexive candidates.#rivet-triumph.Validation:
main(6479a185).stun:stun.l.google.com:19302andstun:stun.cloudflare.com:3478; both peers logged TURN disabled.host/udp↔host/udp, nominated and succeeded, withbytesSent=1263andbytesReceived=1770; the client loggedICE PROOF PASSED: native UDP P2P with no TURN relay.linux/amd64Docker build passed and contains Container Runner2.3.3.pr5480-778c3669-stun-final(sha256:ac3faf5a4d43d94fab96fbe1033f257f928e8420081885873510cd7941abb15a).p6963d1q1fhlqpiepx2jbn2d8ncl00reproduced the result: both peers logged TURN disabled and gathered server-reflexive UDP candidates, then both transitioned from ICECheckingtoFailed.pqovsp0wbgshcifjp5p6lzxozjcl00: both peers gathered host and server-reflexive candidates with TURN disabled, but ICE transitioned fromCheckingtoFailed; the client correctly loggedICE PROOF FAILED.xcfpz8970r7fv226htk0mey4dxal00reproduced the failure. The worker's public STUN mappings were not reachable by peer ICE checks, so a no-TURN Cloud path was not established from this native client/network.Deployment note:
--drain-on-version-upgrade=falsewith Container Runner v2.3.3 for this project.