End-to-end WebRTC integration tests for str0m, exercising ICE, DTLS, SCTP, and DataChannel negotiation between a Rust server and real browsers (Chrome, Edge, Firefox, Safari). All sessions are packet-captured and analyzed to count round-trip times (RTTs) — making this the primary observatory for measuring the connection-setup improvements delivered by DTLS 1.3 and SNAP.
The current WebRTC connection setup (RFC 8829) incurs 4 RTTs before media can be sent and 6 RTTs before the data channel opens. Five separate protocols are stacked and serialized:
| # | Protocol | Purpose | Handshake | RTTs |
|---|---|---|---|---|
| 1 | Signaling (e.g. HTTP) | SDP offer/answer exchange | 1 flight | 1 |
| 2 | ICE (RFC 8445) | Find a viable transport path | Connectivity checks | 1 |
| 3 | DTLS 1.2 (RFC 6347) | Secure the transport | 4-way handshake | 2 |
| 4 | SCTP (RFC 4960) | Reliability layer over DTLS | 4-way handshake | 2 |
| 5 | DCEP (RFC 8832) | Map data channels to SCTP streams | Piggybacks on data | 0 |
| Total | 6 |
In 2011, this wasn't much worse than the 4 RTTs needed for a WebSocket over TCP/TLS. Today, compared to QUIC's (RFC 9000) 0-RTT setup, it seems incredibly slow — especially as WebRTC shifts from peer-to-peer calls (where human answer latency masks setup time) to client-server use cases (conferencing, game streaming, AI services, robotics) where every RTT is directly observable.
WebRTC connection setup can be optimized using orthogonal, backwards-compatible improvements:
| Optimization | What it Does | RTT Savings |
|---|---|---|
| SNAP | SCTP was designed as L4 with anti-hijack/DDoS mechanisms (cookie exchange). Under DTLS these are redundant. SNAP removes the SCTP 4-way handshake entirely, exchanging init params declaratively via SDP. | -2 RTT |
| DTLS 1.3 (RFC 9147) | Reduces the DTLS handshake from 2 RTTs to 1 RTT. | -1 RTT |
| Combined | SNAP + DTLS 1.3 | -3 RTT |
This repo captures pcaps from every test permutation so we can observe and quantify these improvements as str0m and browsers add support.
Note: DTLS 1.3 is enabled by default in Chrome/Edge since Oct 2025. str0m supports DTLS 1.3 via the
aws-lc-rsandrust-cryptobackends (usingDtlsVersion::Auto). SNAP is verified both in browser tests and native Rust-to-Rust tests. Safari is tested natively on macOS viasafaridriver, though explicit DTLS 1.3 and SNAP tests are currently skipped as Safari does not yet fully support these natively.
graph TB
subgraph CI["GitHub Actions CI"]
Lint["Lint (fmt + clippy)"]
Build["Build Server<br/>(3 OS × 3 crypto backends)"]
Test["Browser Tests<br/>(3 OS × 3 crypto × 7 test suites)"]
Analyze["RTT Analysis<br/>(Python pcap analyzer)"]
Lint --> Build --> Test --> Analyze
end
subgraph Server["Rust Server (str0m)"]
WS["WebSocket Signaling<br/>(tokio-tungstenite)"]
Peer["Peer Event Loop<br/>(str0m Rtc engine)"]
Pcap["Pcap Capture<br/>(pcapng writer)"]
WS -->|spawn| Peer
Peer --> Pcap
end
subgraph Browser["Headless Browser"]
Karma["Karma Test Runner<br/>(Jasmine)"]
WRTC["RTCPeerConnection<br/>(browser WebRTC)"]
DC["RTCDataChannel"]
Karma --> WRTC --> DC
end
Browser <-->|"WebSocket (signaling)"| WS
WRTC <-->|"UDP (ICE/DTLS/SCTP)"| Peer
graph LR
subgraph "Rust Crate"
bin_server["bin/server.rs<br/>CLI entry point"]
server["server.rs<br/>WS signaling handler"]
client["client.rs<br/>WS client helpers"]
peer["peer.rs<br/>Peer event loop"]
protocol["protocol.rs<br/>JSON message types"]
pcap["pcap.rs<br/>pcapng writer"]
net["net.rs<br/>IP detection"]
lib["lib.rs<br/>Session, UdpPortAllocator,<br/>shared DtlsCert"]
end
subgraph "TypeScript (web/src)"
spec_base["webrtc-client.spec.ts<br/>Base browser tests"]
spec_warp["webrtc-warp.spec.ts<br/>SNAP/DTLS 1.3 tests"]
ts_proto["protocol.ts<br/>Message types"]
ts_signal["signaling.ts<br/>WS client"]
end
subgraph "Infra"
karma_base["karma.conf.js<br/>Base config"]
karma_warp["karma.warp.conf.js<br/>Optimized config"]
plugin["plugins/<br/>karma-str0m-server"]
analyze["scripts/analyze_pcaps.py"]
end
bin_server --> server
server --> peer
server --> protocol
peer --> pcap
peer --> lib
spec_base --> ts_signal --> ts_proto
spec_warp --> ts_signal
karma_base --> plugin
karma_warp --> plugin
The test harness uses a simple JSON-over-WebSocket signaling protocol. Below shows the browser-as-offerer flow (the most common browser configuration):
sequenceDiagram
participant B as Browser (Karma)
participant WS as Rust Server (WebSocket)
participant P as Rust Peer (str0m)
Note over B,P: 1. Session Setup (WebSocket Signaling)
B->>WS: {"type":"create", "config":{sdp_role, ice_mode, dtls_role}}
WS-->>B: {"type":"created"}
Note over B: createDataChannel() + createOffer()
Note over B: waitForIceGathering()
B->>WS: {"type":"sdp", "sdp":"<offer>"}
WS-->>B: {"type":"sdp", "sdp":"<answer>"}
Note over B: setRemoteDescription(answer)
B->>WS: {"type":"ready"}
WS-->>B: {"type":"ready"}
Note over WS: Spawns Peer event loop (echo mode)
Note over B,P: 2. ICE → DTLS → SCTP (on the wire, over UDP)
Note over B,P: Exact RTT count depends on DTLS version & SNAP features
Note over B,P: 3. DataChannel Echo Test
B->>P: "hello from browser!"
P-->>B: "hello from browser!" (echo)
Note over B: Assert echo matches, measure RTT ✓
Note over B,P: 4. Teardown
B->>WS: {"type":"destroy"}
WS-->>B: {"type":"destroyed"}
Note over P: Shutdown → write pcapng to disk
When client_sdp_role = "answerer", the server creates the offer:
sequenceDiagram
participant B as Browser
participant WS as Server (WS)
participant P as Peer (str0m)
B->>WS: create (answerer config)
WS-->>B: created
WS-->>B: sdp (server offer)
Note over B: setRemoteDescription(offer)
Note over B: createAnswer() + ICE gather
B->>WS: sdp (browser answer)
B->>WS: ready
WS-->>B: ready
Note over B,P: ICE → DTLS → SCTP → Echo (same as above)
The following diagrams show the connection flow improvements.
sequenceDiagram
participant O as Offerer (Browser)
participant A as Answerer (Server)
O->>A: SDP Offer (actpass)
A-->>O: SDP Answer (active)
Note right of A: RTT 1 — Signaling
O->>A: ICE Connectivity Check
A-->>O: ICE Response
Note right of A: RTT 2 — ICE
A->>O: DTLS ClientHello
O-->>A: DTLS ServerHello
Note right of A: RTT 3 — DTLS flight 1
A->>O: DTLS Finished
Note left of O: Answerer media ready (3.5 RTT)
O-->>A: DTLS Finished
Note right of A: RTT 4 — Offerer media ready
O->>A: SCTP INIT
A-->>O: SCTP INIT-ACK
Note right of A: RTT 5 — SCTP flight 1
O->>A: SCTP COOKIE-ECHO
A-->>O: SCTP COOKIE-ACK
Note right of A: RTT 6 — Offerer data ready
O->>A: DCEP Open + "hello"
Note left of O: Answerer data ready (6.5 RTT)
A-->>O: DCEP ACK + "world"
With SNAP (SCTP params in SDP) and DTLS 1.3 (1-RTT handshake):
sequenceDiagram
participant O as Offerer (Browser)
participant A as Answerer (Server)
O->>A: SDP Offer (actpass, snap)
A-->>O: SDP Answer (passive, snap, lite)
Note right of A: RTT 1 — Signaling
O->>A: ICE Check
A-->>O: ICE Response
Note right of A: RTT 2 — ICE connected
O->>A: DTLS ClientHello
A-->>O: DTLS ServerHello/Fin
Note right of A: RTT 3 — DTLS done, data ready
O->>A: DTLS Finished + DCEP Open + "hello"
A-->>O: DCEP ACK + "world"
With DTLS 1.3 reducing the handshake to 1 RTT and SNAP eliminating the SCTP 4-way:
sequenceDiagram
participant C as Client (Browser)
participant S as Server (ICE Lite)
C->>S: SDP Offer (actpass, snap)
S-->>C: SDP Answer (passive, snap, lite)
Note right of S: RTT 1 — Signaling
C->>S: ICE Check + DTLS ClientHello
S-->>C: ICE Response + DTLS ServerHello/Fin
Note right of S: RTT 2 — ICE + DTLS done
C->>S: DTLS Finished + DCEP Open + "hello"
S-->>C: DCEP ACK + "world"
Note left of C: Data ready (2.5 RTT)
gantt
title WebRTC Connection Setup RTTs (Offerer → Data Ready)
dateFormat X
axisFormat %s
section Standard (DTLS 1.2)
Signaling :a0, 0, 1
ICE (STUN) :a1, after a0, 1
DTLS 1.2 :a2, after a1, 2
SCTP 4-way :a3, after a2, 2
Data Ready :milestone, after a3, 0
section DTLS 1.3 only
Signaling :b0, 0, 1
ICE (STUN) :b1, after b0, 1
DTLS 1.3 :b2, after b1, 1
SCTP 4-way :b3, after b2, 2
Data Ready :milestone, after b3, 0
section SNAP + DTLS 1.3
Signaling :c0, 0, 1
ICE + DTLS 1.3 :c1, after c0, 1
Data Ready :milestone, after c1, 0
section SNAP+DTLS1.3
Signaling :d0, 0, 1
ICE+DTLS 1.3 :d1, after d0, 1
Data Ready :milestone, after d1, 0
section Future 0-RTT
Signaling :e0, 0, 1
Data Ready :milestone, after e0, 0
Every base test verifies a full WebRTC connection by sending "hello from browser!" through a DataChannel and confirming the server echoes it back.
| Test Case | SDP Role | DTLS Role | ICE Mode |
|---|---|---|---|
offerer_active_lite |
Browser offers | Browser = DTLS client | Server ICE-Lite |
offerer_active_full |
Browser offers | Browser = DTLS client | Server ICE-Full |
offerer_passive_lite |
Browser offers | Browser = DTLS server | Server ICE-Lite |
offerer_passive_full |
Browser offers | Browser = DTLS server | Server ICE-Full |
answerer_active_lite |
Server offers | Browser = DTLS client | Server ICE-Lite |
answerer_active_full |
Server offers | Browser = DTLS client | Server ICE-Full |
Experimental Chromium field trials are enabled via browser flags:
| Feature | Chromium Flag | What it Does | Spec |
|---|---|---|---|
| SNAP | WebRTC-Sctp-Snap/Enabled/ |
Removes SCTP 4-way handshake; init params exchanged in SDP | draft-hancke-tsvwg-snap |
| Combined | SNAP + DTLS 1.3 | SNAP + DTLS 1.3 = minimal RTT setup | N/A |
Each feature test runs offerer and answerer variants against ICE-Lite.
ICE Lite is RECOMMENDED for optimized servers (for minimal RTTs): a Lite server can respond immediately without waiting for its own connectivity check, enabling minimal RTTs.
graph LR
subgraph OS["Operating Systems"]
Linux
macOS
Windows
end
subgraph Crypto["Crypto Backends"]
aws["aws-lc-rs"]
rust["rust-crypto"]
ossl["openssl (Linux)"]
apple["apple-crypto (macOS)"]
win["wincrypto (Windows)"]
end
subgraph Browsers["Browsers"]
Chrome
Edge
Firefox
Safari["Safari (macOS only)"]
end
subgraph Features["Feature Tests"]
SNAP["SNAP (Chrome)"]
DTLS13["DTLS 1.3 (Chrome)"]
end
OS --- Crypto
Crypto --- Browsers
Crypto --- Features
The full CI runs ~63 test jobs (after exclusions for platform-specific crypto backends and browser compatibility).
├── .github/workflows/
│ └── browser-tests.yml # CI: lint → build → test → analyze
├── scripts/
│ └── analyze_pcaps.py # Post-test RTT analysis & charts
├── src/
│ ├── bin/server.rs # CLI entry point (clap)
│ ├── lib.rs # Session state, DtlsCert cache, port allocator
│ ├── server.rs # WebSocket signaling handler
│ ├── client.rs # WS client helpers (used by Rust tests)
│ ├── peer.rs # str0m Rtc event loop + pcap capture
│ ├── protocol.rs # JSON signaling message types
│ ├── pcap.rs # pcapng file writer
│ └── net.rs # Network interface / IP detection
├── tests/
│ └── integration.rs # Native Rust-to-Rust integration tests
├── web/
│ ├── karma.conf.js # Karma config (base browser tests)
└── karma.warp.conf.js # Karma config (SNAP/DTLS 1.3 tests)
│ ├── plugins/ # karma-str0m-server, karma-edge-launcher
│ └── src/
│ ├── protocol.ts # TS mirror of protocol.rs
│ ├── signaling.ts # WS client for browsers
│ ├── webrtc-client.spec.ts # Base test suite (6 test cases)
│ └── webrtc-warp.spec.ts # Feature test suite (6 test cases)
├── target/pcap/ # Captured pcapng files (gitignored)
├── Cargo.toml # Rust deps (str0m from git)
└── package.json # Root npm scripts (delegates to web/)
flowchart TD
trigger["PR / Daily Schedule / Manual"] --> lint
lint["Lint<br/>cargo fmt + clippy"] --> build
build["Build Server<br/>3 OS × 3 crypto"]
build --> |artifact: server binary| test
test["Browser Tests<br/>63 jobs"]
test --> |artifact: pcapng files| analyze
analyze["RTT Analysis<br/>Python + matplotlib"]
analyze --> |artifact: charts + markdown| report["📊 GitHub Step Summary"]
build --> |pcapng| analyze
Each test job:
- Downloads the pre-built server binary
- Installs npm dependencies in
web/ - Karma launches the server via the
karma-str0m-serverplugin - Spawns the headless browser
- Runs the Jasmine test suites
- Uploads pcapng captures as artifacts
- Rust (stable, edition 2024)
- Node.js 20+
- Chrome, Edge, or Firefox installed
# Build the server
cargo build --release --bin server
# Run native Rust-to-Rust tests
cargo test --release
# Run browser tests (Chrome)
cd web && npm ci
npm run test:chrome
# Run DTLS1.3/SNAP feature tests
npm run test:warp:chrome
# Analyze captured pcaps
pip install matplotlib numpy
python scripts/analyze_pcaps.py target/pcap/ --output-dir analysis# Default (aws-lc-rs)
cargo build --release
# Explicit selection
cargo build --release --no-default-features --features rust-crypto
cargo build --release --no-default-features --features openssl # Linux
cargo build --release --no-default-features --features apple-crypto # macOS
cargo build --release --no-default-features --features wincrypto # WindowsEvery test captures a pcapng file at target/pcap/{session_id}_{role}.pcapng. The analysis script:
- Parses each pcapng file (custom minimal parser matching our writer format)
- Classifies packets:
STUN-REQ,STUN-RESP,DTLS-HS,DTLS-APP, etc. - Counts RTTs per phase (STUN, DTLS handshake, SCTP handshake)
- Generates a markdown summary table and PNG bar charts
- Outputs to
$GITHUB_STEP_SUMMARYfor in-PR visibility
| Session | Browser | Crypto | STUN RTTs | DTLS RTTs | SCTP RTTs | Total |
|---|---|---|---|---|---|---|
chrome_offerer_active_lite |
Chrome | aws-lc-rs | 1 | 1 | 2 | 4 |
chrome_dtls13_snap_offerer |
Chrome | aws-lc-rs | 1 | 0 | 0 | 1 |
Expected progression as str0m adds support:
| Milestone | Total RTTs | Breakdown | Notes |
|---|---|---|---|
| Baseline (DTLS 1.2) | 6 | 1 sig + 1–1.5 ICE + 2 DTLS + 2 SCTP | Full ICE with non-aggressive nomination |
| DTLS 1.3 | 5 | 1 sig + 1–1.5 ICE + 1 DTLS + 2 SCTP | Chrome/Edge default since Oct 2025 |
| + SNAP | 3 | 1 sig + 1 ICE + 1 DTLS | SNAP removes SCTP handshake entirely |
| Full DTLS 1.3 + SNAP | 2 | 1 sig + 1 ICE/DTLS | SNAP + DTLS 1.3 |
With ICE Lite (recommended for minimal RTTs), the server is ready to send at 1.5 RTT — it doesn't need to wait for its own triggered check.
- SNAP — draft-hancke-tsvwg-snap — SCTP Negotiation Acceleration Protocol
- DTLS 1.3 — RFC 9147
- str0m — github.com/algesten/str0m
- WebRTC Data Channels — RFC 8831 (the protocol sandwich, Section 5)
- DCEP — RFC 8832 — DataChannel Establishment Protocol
See str0m for the upstream library license.