- Add 4 missing skills: chromedp-alpine-container, claude-memory-api, openclaw-custom-model-provider, webrtc-turn-shared-secret - Add 9 custom agents: sre, dba, devops-engineer, platform-engineer, security-engineer, network-engineer, observability-engineer, home-automation-engineer, cluster-health-checker - Add openclaw-install.sh: standalone script to clone dotfiles and install skills/agents/hooks/settings to OpenClaw's home directory Replaces the cc-config NFS volume + sync.sh approach
105 lines
3.6 KiB
Markdown
105 lines
3.6 KiB
Markdown
---
|
|
name: webrtc-turn-shared-secret
|
|
description: |
|
|
Generate ephemeral TURN credentials from a shared secret for coturn (--use-auth-secret mode).
|
|
Use when: (1) WebRTC ICE connection state goes to "failed" or stays at "checking",
|
|
(2) STUN-only config can't establish media path through NAT/k8s,
|
|
(3) coturn is configured with --use-auth-secret and you need time-limited credentials,
|
|
(4) need to pass TURN credentials to both server-side (pion/webrtc) and client-side
|
|
(browser RTCPeerConnection). Covers credential generation, Go implementation, and
|
|
client-side WebRTC configuration.
|
|
author: Claude Code
|
|
version: 1.0.0
|
|
date: 2026-02-21
|
|
---
|
|
|
|
# WebRTC TURN Server with Shared Secret Credentials
|
|
|
|
## Problem
|
|
WebRTC connections fail with `ICE connection state: failed` when peers are behind NAT
|
|
(especially in Kubernetes pods). STUN alone can't establish a media path through
|
|
symmetric NAT. A TURN server is needed, and coturn's shared secret mode requires
|
|
generating ephemeral credentials.
|
|
|
|
## Context / Trigger Conditions
|
|
- `webrtc: ICE connection state: failed` in server logs
|
|
- `ICE connection state: failed` in browser console
|
|
- WebRTC signaling (offer/answer) succeeds but no media flows
|
|
- Server is in a k8s pod with private IP, client is behind NAT
|
|
- coturn configured with `--use-auth-secret` or `use-auth-secret` in turnserver.conf
|
|
|
|
## Solution
|
|
|
|
### Credential Generation (TURN REST API)
|
|
|
|
```
|
|
username = Unix timestamp of expiry (e.g., "1740200000")
|
|
password = Base64(HMAC-SHA1(username, shared_secret))
|
|
```
|
|
|
|
### Go Implementation
|
|
|
|
```go
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
func GenerateTURNCredentials(turnURL, sharedSecret string, ttl time.Duration) (urls []string, username, credential string) {
|
|
expiry := time.Now().Add(ttl).Unix()
|
|
username = fmt.Sprintf("%d", expiry)
|
|
mac := hmac.New(sha1.New, []byte(sharedSecret))
|
|
mac.Write([]byte(username))
|
|
credential = base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
return []string{turnURL}, username, credential
|
|
}
|
|
```
|
|
|
|
### Server-side (pion/webrtc)
|
|
|
|
```go
|
|
iceServers := []webrtc.ICEServer{
|
|
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
|
{
|
|
URLs: []string{"turn:your-turn-server:3478"},
|
|
Username: username,
|
|
Credential: credential,
|
|
CredentialType: webrtc.ICECredentialTypePassword,
|
|
},
|
|
}
|
|
pc, _ := webrtc.NewPeerConnection(webrtc.Configuration{ICEServers: iceServers})
|
|
```
|
|
|
|
### Client-side (browser)
|
|
|
|
Send ICE config from server to client via signaling channel (WebSocket),
|
|
then create RTCPeerConnection with it:
|
|
|
|
```javascript
|
|
// Server sends: { type: "iceServers", iceServers: [...] }
|
|
socket.onmessage = (e) => {
|
|
const msg = JSON.parse(e.data);
|
|
if (msg.type === 'iceServers') {
|
|
pc = new RTCPeerConnection({ iceServers: msg.iceServers });
|
|
}
|
|
};
|
|
```
|
|
|
|
## Verification
|
|
|
|
1. Server logs should show `ICE connection state: connected` (not `failed`)
|
|
2. Browser console should show `ICE connection state: connected`
|
|
3. Test TURN connectivity: `turnutils_uclient -u username -w credential turn-server-ip`
|
|
|
|
## Notes
|
|
- Both server and client need the TURN credentials — the server uses them for its
|
|
PeerConnection, and the client needs them for its RTCPeerConnection
|
|
- Credentials are time-limited (TTL); generate fresh ones per session
|
|
- If TURN server hostname doesn't resolve from k8s pods (CoreDNS custom zones),
|
|
use the IP address directly: `turn:1.2.3.4:3478`
|
|
- STUN is still useful as a fallback for direct connections; keep it in the ICE
|
|
servers list alongside TURN
|
|
- The shared secret must match coturn's `static-auth-secret` config
|