icecheck is a developer tool for testing whether two browsers can establish a direct WebRTC connection. It gathers ICE candidates, opens a data channel, sends synthetic video, and exposes the selected candidate pair and connection statistics.
The offer and answer are exchanged by copy and paste or the browser's native share sheet. There are no rooms, WebSockets, or process-local connection registries, so the application is compatible with stateless deployments such as Vercel Functions.
- Native
RTCPeerConnectionnegotiation between two browsers - LAN-only ICE with no configured ICE server
- STUN-assisted ICE using configured public STUN endpoints
- Host, peer-reflexive, and server-reflexive candidate discovery
- Selected candidate pair and round-trip time
- Ordered data-channel delivery with application-level pings
- Synthetic-video negotiation and received RTP statistics
No TURN server is configured, so icecheck does not test relayed connectivity or bandwidth.
Requirements: Node.js 24 and npm.
npm install
npm run devFor a production build:
npm run build
npm startThe development server defaults to http://localhost:4173. Open the root page in two browsers to begin the diagnostic.
- On browser A, select an ICE strategy and choose Create offer.
- Copy or share A's outbound payload, paste it into Offer or answer to apply on browser B, and choose Apply inbound payload.
- Copy or share B's generated answer back to browser A and apply it.
- Read the verdict, then inspect connection state, candidate counts, selected pair, data-channel state, video state, and the JSON report on both browsers.
- Use Start over before starting another exchange.
Each base64url payload contains a complete session description. icecheck waits for non-trickle ICE gathering before encoding it, so no persistent signaling channel is needed. Payloads are versioned JSON envelopes containing the role, ICE strategy, SDP type, and SDP body; malformed, mismatched, oversized, and replayed payloads are rejected.
The negotiation sequence is deliberately small: the offerer creates a complete offer, the answerer validates and applies it before creating a complete answer, and the offerer validates and applies that answer. A reset or unmount closes peer connections, tracks, channels, timers, listeners, and pending work before another exchange begins.
| Variable | Default | Purpose |
|---|---|---|
STUN_URLS |
stun:main.lohr.dev:3478,stun:stun.l.google.com:19302 |
Comma-separated STUN URLs returned by /config |
HOST |
0.0.0.0 |
Development-server bind address |
PORT |
runtime-defined | Development or production port |
ALLOWED_HOSTS |
empty | Comma-separated extra Vite development hosts |
Only stun: and stuns: entries are accepted. TURN credentials are intentionally outside this tool's scope.
Vercel is linked to the GitHub repository. Every push to main creates a production deployment and updates icecheck.vercel.app; other branches receive preview deployments.
The generated Nitro server exposes only stateless HTTP behavior:
- the single-page diagnostic route
/ GET /configfor public ICE configurationGET /healthfor readiness- static assets
There is no application-managed connection between the two browsers. A deployment can scale horizontally or restart without losing a room or socket because none exists. The copied SDP is never sent to the icecheck server.
HTTPS is recommended for consistent browser security behavior. For phone testing, use a URL reachable by both devices; localhost always refers to the device opening it.
| Observation | Likely meaning |
|---|---|
| LAN succeeds, STUN-assisted succeeds | The browsers found a direct path; inspect selected_pair to see which candidate type won |
| LAN fails, STUN-assisted succeeds | Server-reflexive discovery was needed |
| Both fail | Candidate pairs were not mutually reachable, or local policy/firewall/browser support blocked negotiation |
| ICE connects, data channel fails | Inspect SCTP/data-channel state and browser errors |
| Data works, video does not | Inspect RTP stats, codec support, and generated-track support |
Browser privacy protections may replace local addresses with mDNS names or redact details. Candidate types and connection states are more portable signals.
server/
diagnostic-runtime.ts Shared /config and /health middleware
middleware/ Nitro-wide response security headers
routes/ Nitro HTTP adapters
src/
components/ App-wide presentation
modules/icecheck/
components/ Diagnostic UI
hooks/ Client lifecycle adapter
lib/ Manual codec and native WebRTC implementation
routes/
(home)/ Single diagnostic page
test/
browser/ Browser lifecycle coverage
contracts.test.mjs Candidate-report contracts
server.test.mjs Built-server and codec integration coverage
The root route imports the feature's public React component. Browser-only WebRTC code is dynamically loaded by the feature hook and disposed whenever the route unmounts. See AGENTS.md for contributor guidance and module boundaries.
npm run verify
npm run test:e2everify runs ESLint, architecture checks, TypeScript, the production build, and Node integration tests.
Builds and npm run typecheck use TypeScript 7 through the typescript-compiler npm alias. The separate TypeScript 6 dependency supplies the compiler API required by typescript-eslint, which does not yet support TypeScript 7. The scripts select the compiler explicitly because both packages provide a tsc executable.
The browser suite starts the development server automatically and expects Chromium plus its system dependencies to be installed (npx playwright install --with-deps chromium). It performs a complete two-page offer/answer exchange in addition to lifecycle and failure-state checks. Set E2E_BASE_URL to run it against an existing deployment. Playwright output is kept under .playwright/.
Base64url is encoding, not encryption. SDP can expose network addresses, temporary ICE credentials, DTLS fingerprints, codecs, and browser metadata. Transfer payloads only through a channel appropriate for the peers involved, and review them before posting in public issues.
The eventual WebRTC transport is encrypted by the browser, but icecheck does not authenticate the person who supplied a copied payload.
The Vite development adapter, Nitro server, and Vercel deployment all apply the same referrer, content-type, framing, and browser-permission restrictions.