WebRTC bridge (5-Applications/webrtc-bridge/): - Go signaling server with pion/webrtc v4 + gorilla/websocket - Browser client with dark UI, RTT/ICE metrics, request builder - HTTP proxy over WebRTC data channel to Traefik - k3s deployment on 361395-1 with hostNetwork - Traefik IngressRoute at /webrtc with stripPrefix middleware - Bypasses Tailscale DERP relay latency (~129ms → direct P2P) Caddy edge config (5-Applications/caddy-edge/): - Caddyfile with Porkbun DNS-01 challenge - JSON config with explicit TLS connection policies - k3s deployment on 361395-1 with hostNetwork - Note: TLS handshake fails in Caddy 2.10.2 (internal error) despite certs being loaded. Using Tailscale Funnel instead. Infrastructure fixes: - Tailscale Funnel enabled on 361395-1 → Traefik - Traefik ingress for 361395-1.tail4e7094.ts.net → Homer - Funnel hostname: https://361395-1.tail4e7094.ts.net
4.6 KiB
WebRTC Bridge
Bypass Tailscale DERP relay latency for the Research Stack k3s cluster.
Problem
Tailscale Funnel on node 361395-1 exposes Traefik at
https://361395-1.tail4e7094.ts.net, but Funnel traffic traverses DERP
relays, adding ~129 ms of latency per hop. For interactive workloads
(dashboards, shell, streaming) this is unacceptable.
Solution
A lightweight WebRTC bridge that establishes a direct peer-to-peer data channel between the browser client and the cluster node, bypassing DERP entirely. After a one-time signaling exchange over WebSocket, all HTTP traffic flows over the WebRTC data channel with sub-10 ms latency when both peers have direct connectivity.
Architecture
┌──────────────┐ WebSocket (signaling) ┌───────────────────┐
│ │ ◄──────────────────────── │ │
│ Browser │ SDP offer/answer │ Signaling Server │
│ Client │ ICE candidates │ (Go, port 8080) │
│ │ │ on 361395-1 │
│ │ WebRTC Data Channel │ │
│ │ ◄════════════════════════► │ HTTP proxy │
│ │ (peer-to-peer) │ → Traefik:30080 │
└──────────────┘ └───────────────────┘
│ │
│ Direct P2P (UDP) │
│ STUN for NAT traversal │
│ No DERP relay! │
▼ ▼
Client NAT Tailscale mesh
(STUN resolves) (direct route)
Components
| Component | Language | Port | Description |
|---|---|---|---|
signaling-server.go |
Go | 8080 | WebSocket signaling + static file server |
client/index.html |
HTML/JS | — | Browser-side WebRTC client |
Containerfile |
— | — | Multi-stage Go build |
k8s/bridge.yaml |
YAML | — | k3s deployment manifests |
Signaling Flow
- Client opens WebSocket to
wss://361395-1.tail4e7094.ts.net/webrtc/ws - Client creates
RTCPeerConnectionwith public STUN servers - Client creates SDP offer, sends via WebSocket
- Server receives offer, creates answer, sends back
- ICE candidates exchanged over WebSocket
- WebRTC data channel opens — signaling WebSocket may close
- Client sends HTTP requests as JSON over the data channel
- Server proxies requests to Traefik, returns responses
Data Channel Protocol
Messages on the data channel are JSON-encoded HTTP request/response pairs:
Request (client → server):
{
"id": "req-uuid",
"method": "GET",
"path": "/api/v1/status",
"headers": {"Accept": "application/json"},
"body": ""
}
Response (server → client):
{
"id": "req-uuid",
"status": 200,
"headers": {"Content-Type": "application/json"},
"body": "{\"ok\": true}"
}
NAT Traversal
- Uses public STUN servers (
stun:stun.l.google.com:19302,stun:stun1.l.google.com:19302) - The server runs on
361395-1withhostNetwork: true, so its Tailscale IP100.110.163.82is directly reachable - If STUN fails to resolve a direct path, the connection falls back to Tailscale's own NAT traversal (which is better than DERP for most cases)
- A TURN server can be added later if needed
Deployment
Build the container image
cd 5-Applications/webrtc-bridge
podman build -t localhost/webrtc-bridge:latest .
Import into k3s
podman save localhost/webrtc-bridge:latest | ssh 361395-1 k3s ctr images import -
Apply manifests
kubectl apply -f k8s/bridge.yaml
Verify
kubectl -n edge get pods -l app=webrtc-bridge
curl -s https://361395-1.tail4e7094.ts.net/webrtc/health
Access
Open https://361395-1.tail4e7094.ts.net/webrtc/ in a browser. The client
will automatically connect via the signaling server, establish a WebRTC data
channel, and begin proxying HTTP requests.
Future Work
- Add TURN server for symmetric NAT scenarios
- Multiplex multiple HTTP requests over a single data channel
- Add connection quality metrics (RTT, packet loss)
- Support WebSocket proxying through the data channel
- Add authentication (Tailscale identity headers)