Skip to content

Add rtcprobe, a pion WebRTC control test outside Unbounded - #476

Merged
myleshorton merged 4 commits into
mainfrom
feat/rtcprobe
Oct 24, 2026
Merged

myleshorton merged 4 commits into
mainfrom
feat/rtcprobe

Conversation

@myleshorton

Copy link
Copy Markdown
Contributor

Adds rtcprobe, a probe and responder for testing plain pion WebRTC connections with all of Unbounded's own machinery removed: no freddie, no matchmaking, no volunteers.

Why

freddie.producer.outcomes shows that most volunteer attempts fail, about 14% of them after ICE connects. Production data alone can't separate three causes:

  • hard NATs;
  • pion's own limits;
  • a censor interfering with the handshake.

This tool is the control. The responder runs somewhere with an open NAT, and the probe runs from the network under test.

There's a specific reason to suspect the third cause. common/covertdtls says Russia has DPI-filtered pion's DTLS fingerprint since 2026-03-30 (net4people/bbs#603), and covert-dtls shapes the ClientHello of native volunteers. But Unbounded's consumer makes the offer, so it's the DTLS server: it builds pion through newWebRTCAPI with no covert-dtls and sends pion's default ServerHello. The probe can test exactly that case.

What it does

  • Probe output: one JSON line per attempt (plus a summary on stderr) with:
    • how long gathering, signaling, ICE, DTLS and data-channel open took;
    • the step it stalled at, if any (gather, signal, ice, dtls or sctp);
    • the probe's srflx addresses, to confirm the vantage point is where it should be;
    • a short transfer's throughput over the whole send, the burst rate, and loss.
  • Probe settings:
    • transfer direction;
    • data-channel mode: unreliable (unordered, no retransmits, as Unbounded) or reliable;
    • message size (default 1,200 bytes, about one QUIC packet);
    • DTLS variant: default, alt (different cipher suites and curves), or covert (covert-dtls's randomize+mimic);
    • the responder's DTLS role. The default matches production, with the probe as DTLS server. With server, the probe sends the ClientHello instead.
  • Hardening, since the responder sends traffic on request from a public VPS:
    • a shared token;
    • a self-signed TLS certificate whose SHA-256 the probe pins;
    • the probe refuses plain HTTP to a remote responder and doesn't follow redirects;
    • HTTP deadlines;
    • a cap on concurrent sessions;
    • exactly one data channel and one control channel accepted per session.

Findings so far (loopback)

Every arm connects within about 260 ms. Throughput on loopback, with no loss and no latency, is about 43 Mbps with 1,200-byte messages and about 85 Mbps with 16 KB messages. Unbounded sends one QUIC packet per data-channel message, so pion's per-message cost caps a single connection before the network does.

Testing

  • go test -race ./rtcprobe/: ok. It covers:
    • probe against responder in process, for each direction and mode, the alt and covert variants, and both roles;
    • connect-only runs;
    • a bad token;
    • certificate pinning, both matching and mismatched;
    • refusal of remote plain HTTP and of redirects;
    • a peer opening duplicate control channels: the responder survives. I confirmed the test panics without the fix.
    • the throughput math when a transfer stalls;
    • the drain waiting for late messages.
  • Local runs of rtcprobe serve / rtcprobe probe across the matrix: all connected; numbers above.
  • go build ./..., and GOOS=js GOARCH=wasm go build ./cmd: ok. rtcprobe isn't part of the widget.
  • Not tested: real vantage points. That's what the tool is for.

Risk

None to production: this is a new standalone command, and nothing imports it. go.mod moves pion/dtls/v3 from indirect to direct.

How to revert

Revert the merge commit.

Pre-PR review (local Codex gate)

  • Rounds: 5, run at high reasoning effort for the public-facing responder. Final: CODEX GATE: verdict=approve critical=0 important=0 minor=0 preexisting=0 reviewed=14
  • Reviewed at: f27b319
  • Exit: approve
  • Step back: not needed. Round 3 had no findings; round 4 added the covert arm.
  • Round note: one round's range came out empty, because a failed build stopped the commit before the review ran. I discarded that round and reran it on the real commit (r5).
  • Fixed:
    • rtcprobe/serve.go — signaling had no read, write or idle deadlines, so slow clients could tie up connections. (r1)
    • rtcprobe/common.go — duplicate ctl channels made the responder send on a nil channel and panic. (r1)
    • rtcprobe/probe.go — the token was sent over plain HTTP. Now TLS with a pinned certificate. (r1)
    • rtcprobe/serve.go, probe.go — counts were finalized 500 ms after done, though data can arrive later. Now it waits for every message (reliable) or for quiet (unreliable), and flags a drain that ran out of time. (r1)
    • rtcprobe/common.go — throughput counted only the burst. Now it's over the sender's whole span. (r1)
    • rtcprobe/probe.go — result write errors were ignored, and a failed answer write didn't release the session slot. (r1)
    • rtcprobe/common.go — send returned its result before the deferred send-duration update, so the r1 throughput fix had no effect. (r2)
    • rtcprobe/common.go — the unreliable drain could stop at once when done overtook the data. (r2)
    • rtcprobe/probe.go — redirects would have carried the token elsewhere. (r2)
  • Dismissed: none
  • Pre-existing (not introduced here, worth filing): none here. Separately, as a product finding: the consumer's ServerHello is pion's default, with no covert-dtls coverage.
  • Proposed AGENTS.md additions: none

🤖 Generated with Claude Code

https://claude.ai/code/session_019DnqgLJf7p4sK87dDvktjx

myleshorton and others added 4 commits October 24, 2026 06:34
A probe and a responder that connect with plain pion: no freddie, no
volunteers. Each attempt reports how long gathering, signaling, ICE,
DTLS and the data channels took, where it stalled, the probe's srflx
addresses, and the throughput and loss of a short transfer. The probe
picks the data channel mode, message size, direction, DTLS fingerprint
variant and the responder's DTLS role, so runs from one vantage point can
A/B a censor fingerprinting pion's DTLS, and measure pion's own ceiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DnqgLJf7p4sK87dDvktjx
- Bound signaling connections with read, write and idle deadlines.
- Accept one data and one ctl channel per session, so a peer that opens two
  ctl channels can't make the responder send on a nil channel and crash.
- Serve signaling over TLS with a self-signed certificate the probe pins,
  and refuse plain HTTP to a remote responder, so the token isn't sent in
  the clear from the networks we probe.
- Wait for a transfer's stragglers before counting: every message on a
  reliable channel, or a quiet period on an unreliable one, and flag a
  drain that ran out of time.
- Rate the whole transfer over the sender's span, so a connection that
  stalls isn't credited with its pre-stall burst, and report the burst
  rate separately.
- Fail on result write errors, and release the session when the answer
  can't be written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DnqgLJf7p4sK87dDvktjx
- send returned a copy of its result before the deferred SendSecs update,
  so every transfer reported a zero send span and the whole-transfer rate
  fell back to the burst rate. Use named results, report send_secs, and
  check it in the end-to-end test.
- An unreliable drain counted quiet time from the last message even when
  that was before done arrived, so it could stop at once and miss
  stragglers. Count from settling's start at the earliest.
- Don't follow signaling redirects, which would carry the token with them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DnqgLJf7p4sK87dDvktjx
common/covertdtls disguises native producers' ClientHello, because Russia
DPI-filters pion's (net4people/bbs#603), but Unbounded's consumer is the
DTLS server and sends pion's ServerHello unshaped. With -dtls covert and
the responder role switch, one vantage point can compare pion's
ServerHello (production's consumer), pion's ClientHello, and covert-dtls's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019DnqgLJf7p4sK87dDvktjx
Copilot AI balanced review requested due to automatic review settings October 24, 2026 12:50
@coderabbitai

coderabbitai Bot commented Oct 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Only developers with an assigned seat can use this organization's usage-based review budget, and seats here are assigned manually. Ask an admin to assign a seat, or change the review continuation mode in Billing.

Next included review available in 19 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d125230b-926a-4aec-a2d7-8c4c30a65692
Commits

Reviewing files that changed from the base of the PR and between 42b2ef0 and f27b319.

Files selected for processing: 6
  • go.mod
  • rtcprobe/common.go
  • rtcprobe/main.go
  • rtcprobe/probe.go
  • rtcprobe/rtcprobe_test.go
  • rtcprobe/serve.go
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The public-facing responder and DTLS diagnostic controls warrant final human review beyond the reported loopback validation.

No open issues 🎉

What changed in this PR

Adds rtcprobe, a standalone WebRTC diagnostic tool that isolates connection failures and throughput from Unbounded’s signaling, matchmaking, and volunteer infrastructure.

Changes:

  • Adds probe and responder commands with connection timings, failure-stage reporting, and transfer measurements.
  • Supports configurable DTLS variants, roles, and data-channel modes, with authenticated signaling, certificate pinning, and responder limits.
  • Adds integration and regression tests for connections, signaling protections, duplicate channels, and measurement logic.
File Description
rtcprobe/​serve.go Implements the responder, session limits, signaling deadlines, and optional TLS.
rtcprobe/​rtcprobe_test.go Tests probe/responder interoperability, signaling protections, and transfer accounting.
rtcprobe/​probe.go Implements probe attempts, JSON results, summaries, and secure signaling.
rtcprobe/​main.go Adds command dispatch and usage documentation.
rtcprobe/​common.go Provides DTLS configuration, channel management, and transfer measurement helpers.
go.mod Promotes the existing Pion DTLS dependency from indirect to direct.

Review effort: Balanced

@myleshorton
myleshorton merged commit d1b168c into main Oct 24, 2026
5 checks passed
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.

2 participants