diff --git a/README.md b/README.md index 2c113ab..175514f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,50 @@ If NPM doesn't support static file serving directly, run a small static server o - `/ws` → anchor process (WebSocket, keep-alive) - `/*` → static file server (SPA fallback: return `index.html` for unknown paths) +### 4. TURN relay (optional, fixes mobile / CGNAT) + +WebRTC hole-punching fails when both peers are behind symmetric NAT — common on mobile data and some ISPs. A TURN relay fixes this. It runs directly on the VPS, not through Nginx Proxy Manager. + +**Firewall:** open UDP 3478 (and optionally TCP 3478) on the Hetzner firewall. No NPM config needed — coturn speaks its own protocol. + +**Install coturn:** + +```bash +apt install coturn +``` + +**`/etc/turnserver.conf`:** + +``` +listening-port=3478 +fingerprint +use-auth-secret +static-auth-secret=YOUR_RANDOM_SECRET +realm=your-domain.com +no-tcp-relay +``` + +Replace `YOUR_RANDOM_SECRET` with any random string (e.g. `openssl rand -hex 32`). Enable and start: + +```bash +systemctl enable coturn +systemctl start coturn +``` + +**Update `config.js`** to tell browsers about the TURN server: + +```js +window.WASTE_CONFIG = { + signalURL: 'wss://your-domain.com/ws', + turnURL: 'turn:your-domain.com:3478', + turnSecret: 'YOUR_RANDOM_SECRET', +} +``` + +The `use-auth-secret` mode generates short-lived TURN credentials from the shared secret — no user database required. The relay only sees opaque DTLS-encrypted blobs. + +> The browser adapter reads `turnURL` and `turnSecret` from `WASTE_CONFIG` and adds the TURN server to the WebRTC `ICEServers` list automatically. If not configured, STUN-only is used (works for most desktop/home NAT situations). + --- ## How it works: daemon vs browser mode diff --git a/web/src/adapter/browser.ts b/web/src/adapter/browser.ts index f29852b..b14e404 100644 --- a/web/src/adapter/browser.ts +++ b/web/src/adapter/browser.ts @@ -16,6 +16,17 @@ const EKEY_PREFIX = 'yaw/2.1 ekey' const FS_TIMEOUT = 2000 const STUN = 'stun:stun.l.google.com:19302' +function iceServers(): RTCIceServer[] { + const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnSecret?: string } }).WASTE_CONFIG + const servers: RTCIceServer[] = [{ urls: STUN }] + if (cfg?.turnURL && cfg?.turnSecret) { + // TURN with time-limited credentials derived from static-auth-secret + const user = Math.floor(Date.now() / 1000) + 3600 + ':waste' + servers.push({ urls: cfg.turnURL, username: user, credential: cfg.turnSecret }) + } + return servers +} + const enc = (s: string) => new TextEncoder().encode(s) function concat(...arrs: Uint8Array[]): Uint8Array { @@ -263,7 +274,7 @@ class PeerConn { this.on = on this.nick = nick this.share = share - this.pc = new RTCPeerConnection({ iceServers: [{ urls: STUN }] }) + this.pc = new RTCPeerConnection({ iceServers: iceServers() }) const kp = sodium.crypto_box_keypair() this._esk = kp.privateKey this._epk = kp.publicKey