Research-Stack/5-Applications/webrtc-bridge/README.md
Brandon Schneider 683bde4af6 feat(infra): WebRTC bridge + Caddy edge config + Tailscale Funnel
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
2026-05-28 13:11:53 -05:00

136 lines
4.6 KiB
Markdown

# 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
1. Client opens WebSocket to `wss://361395-1.tail4e7094.ts.net/webrtc/ws`
2. Client creates `RTCPeerConnection` with public STUN servers
3. Client creates SDP offer, sends via WebSocket
4. Server receives offer, creates answer, sends back
5. ICE candidates exchanged over WebSocket
6. WebRTC data channel opens — signaling WebSocket may close
7. Client sends HTTP requests as JSON over the data channel
8. 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):**
```json
{
"id": "req-uuid",
"method": "GET",
"path": "/api/v1/status",
"headers": {"Accept": "application/json"},
"body": ""
}
```
**Response (server → client):**
```json
{
"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-1` with `hostNetwork: true`, so its
Tailscale IP `100.110.163.82` is 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
```bash
cd 5-Applications/webrtc-bridge
podman build -t localhost/webrtc-bridge:latest .
```
### Import into k3s
```bash
podman save localhost/webrtc-bridge:latest | ssh 361395-1 k3s ctr images import -
```
### Apply manifests
```bash
kubectl apply -f k8s/bridge.yaml
```
### Verify
```bash
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)