- 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
3.6 KiB
3.6 KiB
| name | description | author | version | date |
|---|---|---|---|---|
| webrtc-turn-shared-secret | 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. | Claude Code | 1.0.0 | 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: failedin server logsICE connection state: failedin 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-secretoruse-auth-secretin 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
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)
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:
// 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
- Server logs should show
ICE connection state: connected(notfailed) - Browser console should show
ICE connection state: connected - 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-secretconfig