mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
82
.agents/skills/cloudflare-deploy/references/turn/README.md
Normal file
82
.agents/skills/cloudflare-deploy/references/turn/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Cloudflare TURN Service
|
||||
|
||||
Expert guidance for implementing Cloudflare TURN Service in WebRTC applications.
|
||||
|
||||
## Overview
|
||||
|
||||
Cloudflare TURN (Traversal Using Relays around NAT) Service is a managed relay service for WebRTC applications. TURN acts as a relay point for traffic between WebRTC clients and SFUs, particularly when direct peer-to-peer communication is obstructed by NATs or firewalls. The service runs on Cloudflare's global anycast network across 310+ cities.
|
||||
|
||||
## Key Characteristics
|
||||
|
||||
- **Anycast Architecture**: Automatically connects clients to the closest Cloudflare location
|
||||
- **Global Network**: Available across Cloudflare's entire network (excluding China Network)
|
||||
- **Zero Configuration**: No need to manually select regions or servers
|
||||
- **Protocol Support**: STUN/TURN over UDP, TCP, and TLS
|
||||
- **Free Tier**: Free when used with Cloudflare Calls SFU, otherwise $0.05/GB outbound
|
||||
|
||||
## In This Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| [api.md](./api.md) | Credentials API, TURN key management, types, constraints |
|
||||
| [configuration.md](./configuration.md) | Worker setup, wrangler.jsonc, env vars, IP allowlisting |
|
||||
| [patterns.md](./patterns.md) | Implementation patterns, use cases, integration examples |
|
||||
| [gotchas.md](./gotchas.md) | Troubleshooting, limits, security, common mistakes |
|
||||
|
||||
## Reading Order
|
||||
|
||||
| Task | Files to Read | Est. Tokens |
|
||||
|------|---------------|-------------|
|
||||
| Quick start | README only | ~500 |
|
||||
| Generate credentials | README → api | ~1300 |
|
||||
| Worker integration | README → configuration → patterns | ~2000 |
|
||||
| Debug connection | gotchas | ~700 |
|
||||
| Security review | api → gotchas | ~1500 |
|
||||
| Enterprise firewall | configuration | ~600 |
|
||||
|
||||
## Service Addresses and Ports
|
||||
|
||||
### STUN over UDP
|
||||
- **Primary**: `stun.cloudflare.com:3478/udp`
|
||||
- **Alternate**: `stun.cloudflare.com:53/udp` (blocked by browsers, not recommended)
|
||||
|
||||
### TURN over UDP
|
||||
- **Primary**: `turn.cloudflare.com:3478/udp`
|
||||
- **Alternate**: `turn.cloudflare.com:53/udp` (blocked by browsers)
|
||||
|
||||
### TURN over TCP
|
||||
- **Primary**: `turn.cloudflare.com:3478/tcp`
|
||||
- **Alternate**: `turn.cloudflare.com:80/tcp`
|
||||
|
||||
### TURN over TLS
|
||||
- **Primary**: `turn.cloudflare.com:5349/tcp`
|
||||
- **Alternate**: `turn.cloudflare.com:443/tcp`
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Create TURN key via API**: see [api.md#create-turn-key](./api.md#create-turn-key)
|
||||
2. **Generate credentials**: see [api.md#generate-temporary-credentials](./api.md#generate-temporary-credentials)
|
||||
3. **Configure Worker**: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)
|
||||
4. **Implement client**: see [patterns.md#basic-turn-configuration-browser](./patterns.md#basic-turn-configuration-browser)
|
||||
|
||||
## When to Use TURN
|
||||
|
||||
- **Restrictive NATs**: Symmetric NATs that block direct connections
|
||||
- **Corporate firewalls**: Environments blocking WebRTC ports
|
||||
- **Mobile networks**: Carrier-grade NAT scenarios
|
||||
- **Predictable connectivity**: When reliability > efficiency
|
||||
|
||||
## Related Cloudflare Services
|
||||
|
||||
- **Cloudflare Calls SFU**: Managed Selective Forwarding Unit (TURN free when used with SFU)
|
||||
- **Cloudflare Stream**: Video streaming with WHIP/WHEP support
|
||||
- **Cloudflare Workers**: Backend for credential generation
|
||||
- **Cloudflare KV**: Credential caching
|
||||
- **Cloudflare Durable Objects**: Session state management
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Cloudflare Calls Documentation](https://developers.cloudflare.com/calls/)
|
||||
- [Cloudflare TURN Service Docs](https://developers.cloudflare.com/realtime/turn/)
|
||||
- [Cloudflare API Reference](https://developers.cloudflare.com/api/resources/calls/subresources/turn/)
|
||||
- [Orange Meets (Open Source Example)](https://github.com/cloudflare/orange)
|
||||
239
.agents/skills/cloudflare-deploy/references/turn/api.md
Normal file
239
.agents/skills/cloudflare-deploy/references/turn/api.md
Normal file
@@ -0,0 +1,239 @@
|
||||
# TURN API Reference
|
||||
|
||||
Complete API documentation for Cloudflare TURN service credentials and key management.
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require Cloudflare API token with "Calls Write" permission.
|
||||
|
||||
Base URL: `https://api.cloudflare.com/client/v4`
|
||||
|
||||
## TURN Key Management
|
||||
|
||||
### List TURN Keys
|
||||
|
||||
```
|
||||
GET /accounts/{account_id}/calls/turn_keys
|
||||
```
|
||||
|
||||
### Get TURN Key Details
|
||||
|
||||
```
|
||||
GET /accounts/{account_id}/calls/turn_keys/{key_id}
|
||||
```
|
||||
|
||||
### Create TURN Key
|
||||
|
||||
```
|
||||
POST /accounts/{account_id}/calls/turn_keys
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "my-turn-key"
|
||||
}
|
||||
```
|
||||
|
||||
**Response includes**:
|
||||
- `uid`: Key identifier
|
||||
- `key`: The actual secret key (only returned on creation—save immediately)
|
||||
- `name`: Human-readable name
|
||||
- `created`: ISO 8601 timestamp
|
||||
- `modified`: ISO 8601 timestamp
|
||||
|
||||
### Update TURN Key
|
||||
|
||||
```
|
||||
PUT /accounts/{account_id}/calls/turn_keys/{key_id}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "updated-name"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete TURN Key
|
||||
|
||||
```
|
||||
DELETE /accounts/{account_id}/calls/turn_keys/{key_id}
|
||||
```
|
||||
|
||||
## Generate Temporary Credentials
|
||||
|
||||
```
|
||||
POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/generate
|
||||
Authorization: Bearer {key_secret}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"ttl": 86400
|
||||
}
|
||||
```
|
||||
|
||||
### Credential Constraints
|
||||
|
||||
| Parameter | Min | Max | Default | Notes |
|
||||
|-----------|-----|-----|---------|-------|
|
||||
| ttl | 1 | 172800 (48hrs) | varies | API rejects values >172800 |
|
||||
|
||||
**CRITICAL**: Maximum TTL is 48 hours (172800 seconds). API will reject requests exceeding this limit.
|
||||
|
||||
### Response Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"iceServers": {
|
||||
"urls": [
|
||||
"stun:stun.cloudflare.com:3478",
|
||||
"turn:turn.cloudflare.com:3478?transport=udp",
|
||||
"turn:turn.cloudflare.com:3478?transport=tcp",
|
||||
"turn:turn.cloudflare.com:53?transport=udp",
|
||||
"turn:turn.cloudflare.com:80?transport=tcp",
|
||||
"turns:turn.cloudflare.com:5349?transport=tcp",
|
||||
"turns:turn.cloudflare.com:443?transport=tcp"
|
||||
],
|
||||
"username": "1738035200:user123",
|
||||
"credential": "base64encodedhmac=="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Port 53 Warning**: Filter port 53 URLs for browser clients—blocked by Chrome/Firefox. See [gotchas.md](./gotchas.md#using-port-53-in-browsers).
|
||||
|
||||
## Revoke Credentials
|
||||
|
||||
```
|
||||
POST https://rtc.live.cloudflare.com/v1/turn/keys/{key_id}/credentials/revoke
|
||||
Authorization: Bearer {key_secret}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "1738035200:user123"
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: 204 No Content
|
||||
|
||||
Billing stops immediately. Active connection drops after short delay (~seconds).
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
```typescript
|
||||
interface CloudflareTURNConfig {
|
||||
keyId: string;
|
||||
keySecret: string;
|
||||
ttl?: number; // Max 172800 (48 hours)
|
||||
}
|
||||
|
||||
interface TURNCredentialsRequest {
|
||||
ttl?: number; // Max 172800 seconds
|
||||
}
|
||||
|
||||
interface TURNCredentialsResponse {
|
||||
iceServers: {
|
||||
urls: string[];
|
||||
username: string;
|
||||
credential: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RTCIceServer {
|
||||
urls: string | string[];
|
||||
username?: string;
|
||||
credential?: string;
|
||||
credentialType?: "password";
|
||||
}
|
||||
|
||||
interface TURNKeyResponse {
|
||||
uid: string;
|
||||
key: string; // Only present on creation
|
||||
name: string;
|
||||
created: string;
|
||||
modified: string;
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Function
|
||||
|
||||
```typescript
|
||||
function validateRTCIceServer(obj: unknown): obj is RTCIceServer {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const server = obj as Record<string, unknown>;
|
||||
|
||||
if (typeof server.urls !== 'string' && !Array.isArray(server.urls)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server.username && typeof server.username !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (server.credential && typeof server.credential !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Safe Credential Generation
|
||||
|
||||
```typescript
|
||||
async function fetchTURNServers(
|
||||
config: CloudflareTURNConfig
|
||||
): Promise<RTCIceServer[]> {
|
||||
// Validate TTL constraint
|
||||
const ttl = config.ttl ?? 3600;
|
||||
if (ttl > 172800) {
|
||||
throw new Error('TTL cannot exceed 172800 seconds (48 hours)');
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://rtc.live.cloudflare.com/v1/turn/keys/${config.keyId}/credentials/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.keySecret}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ttl })
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`TURN credential generation failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Filter port 53 for browser clients
|
||||
const filteredUrls = data.iceServers.urls.filter(
|
||||
(url: string) => !url.includes(':53')
|
||||
);
|
||||
|
||||
const iceServers = [
|
||||
{ urls: 'stun:stun.cloudflare.com:3478' },
|
||||
{
|
||||
urls: filteredUrls,
|
||||
username: data.iceServers.username,
|
||||
credential: data.iceServers.credential,
|
||||
credentialType: 'password' as const
|
||||
}
|
||||
];
|
||||
|
||||
// Validate before returning
|
||||
if (!iceServers.every(validateRTCIceServer)) {
|
||||
throw new Error('Invalid ICE server configuration received');
|
||||
}
|
||||
|
||||
return iceServers;
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [configuration.md](./configuration.md) - Worker setup, environment variables
|
||||
- [patterns.md](./patterns.md) - Implementation examples using these APIs
|
||||
- [gotchas.md](./gotchas.md) - Security best practices, common mistakes
|
||||
@@ -0,0 +1,179 @@
|
||||
# TURN Configuration
|
||||
|
||||
Setup and configuration for Cloudflare TURN service in Workers and applications.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# .env
|
||||
CLOUDFLARE_ACCOUNT_ID=your_account_id
|
||||
CLOUDFLARE_API_TOKEN=your_api_token
|
||||
TURN_KEY_ID=your_turn_key_id
|
||||
TURN_KEY_SECRET=your_turn_key_secret
|
||||
```
|
||||
|
||||
Validate with zod:
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
CLOUDFLARE_ACCOUNT_ID: z.string().min(1),
|
||||
CLOUDFLARE_API_TOKEN: z.string().min(1),
|
||||
TURN_KEY_ID: z.string().min(1),
|
||||
TURN_KEY_SECRET: z.string().min(1)
|
||||
});
|
||||
|
||||
export const config = envSchema.parse(process.env);
|
||||
```
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "turn-credentials-api",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"vars": {
|
||||
"TURN_KEY_ID": "your-turn-key-id" // Non-sensitive, can be in vars
|
||||
},
|
||||
"env": {
|
||||
"production": {
|
||||
"kv_namespaces": [
|
||||
{
|
||||
"binding": "CREDENTIALS_CACHE",
|
||||
"id": "your-kv-namespace-id"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Store secrets separately**:
|
||||
```bash
|
||||
wrangler secret put TURN_KEY_SECRET
|
||||
```
|
||||
|
||||
## Cloudflare Worker Integration
|
||||
|
||||
### Worker Binding Types
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
TURN_KEY_ID: string;
|
||||
TURN_KEY_SECRET: string;
|
||||
CREDENTIALS_CACHE?: KVNamespace;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
// See patterns.md for implementation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Basic Worker Example
|
||||
|
||||
```typescript
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
if (request.url.endsWith('/turn-credentials')) {
|
||||
// Validate client auth
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response('Unauthorized', { status: 401 });
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`https://rtc.live.cloudflare.com/v1/turn/keys/${env.TURN_KEY_ID}/credentials/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${env.TURN_KEY_SECRET}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ ttl: 3600 })
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return new Response('Failed to generate credentials', { status: 500 });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Filter port 53 for browser clients
|
||||
const filteredUrls = data.iceServers.urls.filter(
|
||||
(url: string) => !url.includes(':53')
|
||||
);
|
||||
|
||||
return Response.json({
|
||||
iceServers: [
|
||||
{ urls: 'stun:stun.cloudflare.com:3478' },
|
||||
{
|
||||
urls: filteredUrls,
|
||||
username: data.iceServers.username,
|
||||
credential: data.iceServers.credential
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## IP Allowlisting (Enterprise/Firewall)
|
||||
|
||||
For strict firewalls, allowlist these IPs for `turn.cloudflare.com`:
|
||||
|
||||
| Type | Address | Protocol |
|
||||
|------|---------|----------|
|
||||
| IPv4 | 141.101.90.1/32 | All |
|
||||
| IPv4 | 162.159.207.1/32 | All |
|
||||
| IPv6 | 2a06:98c1:3200::1/128 | All |
|
||||
| IPv6 | 2606:4700:48::1/128 | All |
|
||||
|
||||
**IMPORTANT**: These IPs may change with 14-day notice. Monitor DNS:
|
||||
|
||||
```bash
|
||||
# Check A and AAAA records
|
||||
dig turn.cloudflare.com A
|
||||
dig turn.cloudflare.com AAAA
|
||||
```
|
||||
|
||||
Set up automated monitoring to detect IP changes and update allowlists within 14 days.
|
||||
|
||||
## IPv6 Support
|
||||
|
||||
- **Client-to-TURN**: Both IPv4 and IPv6 supported
|
||||
- **Relay addresses**: IPv4 only (no RFC 6156 support)
|
||||
- **TCP relaying**: Not supported (RFC 6062)
|
||||
|
||||
Clients can connect via IPv6, but relayed traffic uses IPv4 addresses.
|
||||
|
||||
## TLS Configuration
|
||||
|
||||
### Supported TLS Versions
|
||||
- TLS 1.1
|
||||
- TLS 1.2
|
||||
- TLS 1.3
|
||||
|
||||
### Recommended Ciphers (TLS 1.3)
|
||||
- AEAD-AES128-GCM-SHA256
|
||||
- AEAD-AES256-GCM-SHA384
|
||||
- AEAD-CHACHA20-POLY1305-SHA256
|
||||
|
||||
### Recommended Ciphers (TLS 1.2)
|
||||
- ECDHE-ECDSA-AES128-GCM-SHA256
|
||||
- ECDHE-RSA-AES128-GCM-SHA256
|
||||
- ECDHE-RSA-AES128-SHA (also TLS 1.1)
|
||||
- AES128-GCM-SHA256
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - TURN key creation, credential generation API
|
||||
- [patterns.md](./patterns.md) - Full Worker implementation patterns
|
||||
- [gotchas.md](./gotchas.md) - Security best practices, troubleshooting
|
||||
231
.agents/skills/cloudflare-deploy/references/turn/gotchas.md
Normal file
231
.agents/skills/cloudflare-deploy/references/turn/gotchas.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# TURN Gotchas & Troubleshooting
|
||||
|
||||
Common mistakes, security best practices, and troubleshooting for Cloudflare TURN.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Issue | Solution | Details |
|
||||
|-------|----------|---------|
|
||||
| Credentials not working | Check TTL ≤ 48hrs | [See Troubleshooting](#issue-turn-credentials-not-working) |
|
||||
| Connection drops after ~48hrs | Implement credential refresh | [See Connection Drops](#issue-connection-drops-after-48-hours) |
|
||||
| Port 53 fails in browser | Filter server-side | [See Port 53](#using-port-53-in-browsers) |
|
||||
| High packet loss | Check rate limits | [See Rate Limits](#limits-per-turn-allocation) |
|
||||
| Connection fails after maintenance | Implement ICE restart | [See ICE Restart](#ice-restart-required-scenarios) |
|
||||
|
||||
## Critical Constraints
|
||||
|
||||
| Constraint | Value | Consequence if Violated |
|
||||
|------------|-------|-------------------------|
|
||||
| Max credential TTL | 48 hours (172800s) | API rejects request |
|
||||
| Credential revocation delay | ~seconds | Billing stops immediately, connection drops shortly |
|
||||
| IP allowlist update window | 14 days (if IPs change) | Connection fails if IPs change |
|
||||
| Packet rate | 5-10k pps per allocation | Packet drops |
|
||||
| Data rate | 50-100 Mbps per allocation | Packet drops |
|
||||
| Unique IP rate | >5 new IPs/sec | Packet drops |
|
||||
|
||||
## Limits Per TURN Allocation
|
||||
|
||||
**Per user** (not account-wide):
|
||||
|
||||
- **IP addresses**: >5 new unique IPs per second
|
||||
- **Packet rate**: 5-10k packets per second (inbound/outbound)
|
||||
- **Data rate**: 50-100 Mbps (inbound/outbound)
|
||||
- **MTU**: No specific limit
|
||||
- **Burst rates**: Higher than documented
|
||||
|
||||
Exceeding limits results in **packet drops**.
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### Setting TTL > 48 hours
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: API will reject
|
||||
const creds = await generate({ ttl: 604800 }); // 7 days
|
||||
|
||||
// ✅ GOOD:
|
||||
const creds = await generate({ ttl: 86400 }); // 24 hours
|
||||
```
|
||||
|
||||
### Hardcoding IPs without monitoring
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: IPs can change with 14-day notice
|
||||
const iceServers = [{ urls: 'turn:141.101.90.1:3478' }];
|
||||
|
||||
// ✅ GOOD: Use DNS
|
||||
const iceServers = [{ urls: 'turn:turn.cloudflare.com:3478' }];
|
||||
```
|
||||
|
||||
### Using port 53 in browsers
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Blocked by Chrome/Firefox
|
||||
urls: ['turn:turn.cloudflare.com:53']
|
||||
|
||||
// ✅ GOOD: Filter port 53
|
||||
urls: urls.filter(url => !url.includes(':53'))
|
||||
```
|
||||
|
||||
### Not handling credential expiry
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Credentials expire but call continues → connection drops
|
||||
const creds = await fetchCreds();
|
||||
const pc = new RTCPeerConnection({ iceServers: creds });
|
||||
|
||||
// ✅ GOOD: Refresh before expiry
|
||||
setInterval(() => refreshCredentials(pc), 3000000); // 50 min
|
||||
```
|
||||
|
||||
### Missing ICE restart support
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: No recovery from TURN maintenance
|
||||
pc.addEventListener('iceconnectionstatechange', () => {
|
||||
console.log('State changed:', pc.iceConnectionState);
|
||||
});
|
||||
|
||||
// ✅ GOOD: Implement ICE restart
|
||||
pc.addEventListener('iceconnectionstatechange', async () => {
|
||||
if (pc.iceConnectionState === 'failed') {
|
||||
await refreshCredentials(pc);
|
||||
pc.restartIce();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Exposing TURN key secret client-side
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: Secret exposed to client
|
||||
const secret = 'your-turn-key-secret';
|
||||
const response = await fetch(`https://rtc.live.cloudflare.com/v1/turn/...`, {
|
||||
headers: { 'Authorization': `Bearer ${secret}` }
|
||||
});
|
||||
|
||||
// ✅ GOOD: Generate credentials server-side
|
||||
const response = await fetch('/api/turn-credentials');
|
||||
```
|
||||
|
||||
## ICE Restart Required Scenarios
|
||||
|
||||
These events require ICE restart (see [patterns.md](./patterns.md#ice-restart-pattern)):
|
||||
|
||||
1. **TURN server maintenance** (occasional on Cloudflare's network)
|
||||
2. **Network topology changes** (anycast routing changes)
|
||||
3. **Credential refresh** during long sessions (>1 hour)
|
||||
4. **Connection failure** (iceConnectionState === 'failed')
|
||||
|
||||
Implement in all production apps:
|
||||
|
||||
```typescript
|
||||
pc.addEventListener('iceconnectionstatechange', async () => {
|
||||
if (pc.iceConnectionState === 'failed' ||
|
||||
pc.iceConnectionState === 'disconnected') {
|
||||
await refreshTURNCredentials(pc);
|
||||
pc.restartIce();
|
||||
const offer = await pc.createOffer({ iceRestart: true });
|
||||
await pc.setLocalDescription(offer);
|
||||
// Send offer to peer via signaling...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Reference: [RFC 8445 Section 2.4](https://datatracker.ietf.org/doc/html/rfc8445#section-2.4)
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] Credentials generated server-side only (never client-side)
|
||||
- [ ] TURN_KEY_SECRET in wrangler secrets, not vars
|
||||
- [ ] TTL ≤ expected session duration (and ≤ 48 hours)
|
||||
- [ ] Rate limiting on credential generation endpoint
|
||||
- [ ] Client authentication before issuing credentials
|
||||
- [ ] Credential revocation API for compromised sessions
|
||||
- [ ] No hardcoded IPs (or DNS monitoring in place)
|
||||
- [ ] Port 53 filtered for browser clients
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: TURN credentials not working
|
||||
|
||||
**Check:**
|
||||
- Key ID and secret are correct
|
||||
- Credentials haven't expired (check TTL)
|
||||
- TTL doesn't exceed 172800 seconds (48 hours)
|
||||
- Server can reach rtc.live.cloudflare.com
|
||||
- Network allows outbound HTTPS
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
// Validate before using
|
||||
if (ttl > 172800) {
|
||||
throw new Error('TTL cannot exceed 48 hours');
|
||||
}
|
||||
```
|
||||
|
||||
### Issue: Slow connection establishment
|
||||
|
||||
**Solutions:**
|
||||
- Ensure proper ICE candidate gathering
|
||||
- Check network latency to Cloudflare edge
|
||||
- Verify firewall allows WebRTC ports (3478, 5349, 443)
|
||||
- Consider using TURN over TLS (port 443) for corporate networks
|
||||
|
||||
### Issue: High packet loss
|
||||
|
||||
**Check:**
|
||||
- Not exceeding rate limits (5-10k pps)
|
||||
- Not exceeding bandwidth limits (50-100 Mbps)
|
||||
- Not connecting to too many unique IPs (>5/sec)
|
||||
- Client network quality
|
||||
|
||||
### Issue: Connection drops after ~48 hours
|
||||
|
||||
**Cause**: Credentials expired (48hr max)
|
||||
|
||||
**Solution**:
|
||||
- Set TTL to expected session duration
|
||||
- Implement credential refresh with setConfiguration()
|
||||
- Use ICE restart if connection fails
|
||||
|
||||
```typescript
|
||||
// Refresh credentials before expiry
|
||||
const refreshInterval = ttl * 1000 - 60000; // 1 min early
|
||||
setInterval(async () => {
|
||||
await refreshTURNCredentials(pc);
|
||||
}, refreshInterval);
|
||||
```
|
||||
|
||||
### Issue: Port 53 URLs in browser fail silently
|
||||
|
||||
**Cause**: Chrome/Firefox block port 53
|
||||
|
||||
**Solution**: Filter port 53 URLs server-side:
|
||||
|
||||
```typescript
|
||||
const filtered = urls.filter(url => !url.includes(':53'));
|
||||
```
|
||||
|
||||
### Issue: Hardcoded IPs stop working
|
||||
|
||||
**Cause**: Cloudflare changed IP addresses (14-day notice)
|
||||
|
||||
**Solution**:
|
||||
- Use DNS hostnames (`turn.cloudflare.com`)
|
||||
- Monitor DNS changes with automated alerts
|
||||
- Update allowlists within 14 days if using IP allowlisting
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
1. Use appropriate TTLs (don't over-provision)
|
||||
2. Implement credential caching
|
||||
3. Set `iceTransportPolicy: 'all'` to try direct first (use `'relay'` only when necessary)
|
||||
4. Monitor bandwidth usage
|
||||
5. Free when used with Cloudflare Calls SFU
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - Credential generation API, revocation
|
||||
- [configuration.md](./configuration.md) - IP allowlisting, monitoring
|
||||
- [patterns.md](./patterns.md) - ICE restart, credential refresh patterns
|
||||
213
.agents/skills/cloudflare-deploy/references/turn/patterns.md
Normal file
213
.agents/skills/cloudflare-deploy/references/turn/patterns.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# TURN Implementation Patterns
|
||||
|
||||
Production-ready patterns for implementing Cloudflare TURN in WebRTC applications.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before implementing these patterns, ensure you have:
|
||||
- TURN key created: see [api.md#create-turn-key](./api.md#create-turn-key)
|
||||
- Worker configured: see [configuration.md#cloudflare-worker-integration](./configuration.md#cloudflare-worker-integration)
|
||||
|
||||
## Basic TURN Configuration (Browser)
|
||||
|
||||
```typescript
|
||||
interface RTCIceServer {
|
||||
urls: string | string[];
|
||||
username?: string;
|
||||
credential?: string;
|
||||
credentialType?: "password" | "oauth";
|
||||
}
|
||||
|
||||
async function getTURNConfig(): Promise<RTCIceServer[]> {
|
||||
const response = await fetch('/api/turn-credentials');
|
||||
const data = await response.json();
|
||||
|
||||
return [
|
||||
{
|
||||
urls: 'stun:stun.cloudflare.com:3478'
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
'turn:turn.cloudflare.com:3478?transport=udp',
|
||||
'turn:turn.cloudflare.com:3478?transport=tcp',
|
||||
'turns:turn.cloudflare.com:5349?transport=tcp',
|
||||
'turns:turn.cloudflare.com:443?transport=tcp'
|
||||
],
|
||||
username: data.username,
|
||||
credential: data.credential,
|
||||
credentialType: 'password'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Use in RTCPeerConnection
|
||||
const iceServers = await getTURNConfig();
|
||||
const peerConnection = new RTCPeerConnection({ iceServers });
|
||||
```
|
||||
|
||||
## Port Selection Strategy
|
||||
|
||||
Recommended order for browser clients:
|
||||
|
||||
1. **3478/udp** (primary, lowest latency)
|
||||
2. **3478/tcp** (fallback for UDP-blocked networks)
|
||||
3. **5349/tls** (corporate firewalls, most reliable)
|
||||
4. **443/tls** (alternate TLS port, firewall-friendly)
|
||||
|
||||
**Avoid port 53**—blocked by Chrome and Firefox.
|
||||
|
||||
```typescript
|
||||
function filterICEServersForBrowser(urls: string[]): string[] {
|
||||
return urls
|
||||
.filter(url => !url.includes(':53')) // Remove port 53
|
||||
.sort((a, b) => {
|
||||
// Prioritize UDP over TCP over TLS
|
||||
if (a.includes('transport=udp')) return -1;
|
||||
if (b.includes('transport=udp')) return 1;
|
||||
if (a.includes('transport=tcp') && !a.startsWith('turns:')) return -1;
|
||||
if (b.includes('transport=tcp') && !b.startsWith('turns:')) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Credential Refresh (Mid-Session)
|
||||
|
||||
When credentials expire during long calls:
|
||||
|
||||
```typescript
|
||||
async function refreshTURNCredentials(pc: RTCPeerConnection): Promise<void> {
|
||||
const newCreds = await fetch('/turn-credentials').then(r => r.json());
|
||||
const config = pc.getConfiguration();
|
||||
config.iceServers = newCreds.iceServers;
|
||||
pc.setConfiguration(config);
|
||||
// Note: setConfiguration() does NOT trigger ICE restart
|
||||
// Combine with restartIce() if connection fails
|
||||
}
|
||||
|
||||
// Auto-refresh before expiry
|
||||
setInterval(async () => {
|
||||
await refreshTURNCredentials(peerConnection);
|
||||
}, 3000000); // 50 minutes if TTL is 1 hour
|
||||
```
|
||||
|
||||
## ICE Restart Pattern
|
||||
|
||||
After network change, TURN server maintenance, or credential expiry:
|
||||
|
||||
```typescript
|
||||
pc.addEventListener('iceconnectionstatechange', async () => {
|
||||
if (pc.iceConnectionState === 'failed') {
|
||||
console.warn('ICE connection failed, restarting...');
|
||||
|
||||
// Refresh credentials
|
||||
await refreshTURNCredentials(pc);
|
||||
|
||||
// Trigger ICE restart
|
||||
pc.restartIce();
|
||||
const offer = await pc.createOffer({ iceRestart: true });
|
||||
await pc.setLocalDescription(offer);
|
||||
|
||||
// Send offer to peer via signaling channel...
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Credentials Caching Pattern
|
||||
|
||||
```typescript
|
||||
class TURNCredentialsManager {
|
||||
private creds: { username: string; credential: string; urls: string[]; expiresAt: number; } | null = null;
|
||||
|
||||
async getCredentials(keyId: string, keySecret: string): Promise<RTCIceServer[]> {
|
||||
const now = Date.now();
|
||||
|
||||
if (this.creds && this.creds.expiresAt > now) {
|
||||
return this.buildIceServers(this.creds);
|
||||
}
|
||||
|
||||
const ttl = 3600;
|
||||
if (ttl > 172800) throw new Error('TTL max 48hrs');
|
||||
|
||||
const res = await fetch(
|
||||
`https://rtc.live.cloudflare.com/v1/turn/keys/${keyId}/credentials/generate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${keySecret}`, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ttl })
|
||||
}
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
const filteredUrls = data.iceServers.urls.filter((url: string) => !url.includes(':53'));
|
||||
|
||||
this.creds = {
|
||||
username: data.iceServers.username,
|
||||
credential: data.iceServers.credential,
|
||||
urls: filteredUrls,
|
||||
expiresAt: now + (ttl * 1000) - 60000
|
||||
};
|
||||
|
||||
return this.buildIceServers(this.creds);
|
||||
}
|
||||
|
||||
private buildIceServers(c: { username: string; credential: string; urls: string[] }): RTCIceServer[] {
|
||||
return [
|
||||
{ urls: 'stun:stun.cloudflare.com:3478' },
|
||||
{ urls: c.urls, username: c.username, credential: c.credential, credentialType: 'password' as const }
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
```typescript
|
||||
// Video conferencing: TURN as fallback
|
||||
const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'all' };
|
||||
|
||||
// IoT/predictable connectivity: force TURN
|
||||
const config = { iceServers: await getTURNConfig(), iceTransportPolicy: 'relay' };
|
||||
|
||||
// Screen sharing: reduce overhead
|
||||
const pc = new RTCPeerConnection({ iceServers: await getTURNConfig(), bundlePolicy: 'max-bundle' });
|
||||
```
|
||||
|
||||
## Integration with Cloudflare Calls SFU
|
||||
|
||||
```typescript
|
||||
// TURN is automatically used when needed
|
||||
// Cloudflare Calls handles TURN + SFU coordination
|
||||
const session = await callsClient.createSession({
|
||||
appId: 'your-app-id',
|
||||
sessionId: 'meeting-123'
|
||||
});
|
||||
```
|
||||
|
||||
## Debugging ICE Connectivity
|
||||
|
||||
```typescript
|
||||
pc.addEventListener('icecandidate', (event) => {
|
||||
if (event.candidate) {
|
||||
console.log('ICE candidate:', event.candidate.type, event.candidate.protocol);
|
||||
}
|
||||
});
|
||||
|
||||
pc.addEventListener('iceconnectionstatechange', () => {
|
||||
console.log('ICE state:', pc.iceConnectionState);
|
||||
});
|
||||
|
||||
// Check selected candidate pair
|
||||
const stats = await pc.getStats();
|
||||
stats.forEach(report => {
|
||||
if (report.type === 'candidate-pair' && report.selected) {
|
||||
console.log('Selected:', report);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [api.md](./api.md) - Credential generation API, types
|
||||
- [configuration.md](./configuration.md) - Worker setup, environment variables
|
||||
- [gotchas.md](./gotchas.md) - Common mistakes, troubleshooting
|
||||
Reference in New Issue
Block a user