diff --git a/CLAUDE.md b/CLAUDE.md index e16683e..cf4997e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -122,10 +122,10 @@ Handshake frame: `[0x5A 0x42 0x01 baud_lo baud_hi xor_cksum]` — 6 bytes at 50 ### Web page integrity stamping -Each deployed page (`web/chat.html`, `web/zebra-audio.html`, `web/how-it-works.html`) -carries a footer with the build date + its own MD5 + SHA-256. **Run `make stamp` -before deploying any page change** — it sets today's date and recomputes the -hashes (`web/stamp.js`). +Each deployed page (`web/chat.html`, `web/zebra-audio.html`, `web/how-it-works.html`, +`web/host-your-own.html`) carries a footer with the build date + its own MD5 + SHA-256. +**Run `make stamp` before deploying any page change** — it sets today's date and +recomputes the hashes (`web/stamp.js`). - A file can't hold its own hash, so the hashes are computed with the two hash fields **zeroed**, then written back (same length). Self-consistent and @@ -135,6 +135,12 @@ hashes (`web/stamp.js`). injects it in the browser. `stamp.js` is build tooling, never loaded by a page. - **Verify a served page**: blank the md5 field to 32 zeros and the sha256 field to 64 zeros, then re-hash with `sha256sum`/`md5sum` — must match the footer. -- **Deploy flow**: edit `web/*.html` → `make stamp` → commit here → copy the - page(s) into `~/git/www.unturf.com/zebra-report/` (`chat.html` → `index.html`, - others same name) → commit + push there. +- **Deploy = push to BOTH repos.** A page change is not live until it lands in + both. Pushing only the source changes nothing served; pushing only the deploy + repo orphans the source of truth. Both, every time: + 1. **source** — this repo (`zebra-report`): edit `web/*.html` → `make stamp` → + commit → **push to origin**. + 2. **served** — `www.unturf.com`: copy the page(s) into + `~/git/www.unturf.com/zebra-report/` (`chat.html` → `index.html`; + `zebra-audio.html`, `how-it-works.html`, `host-your-own.html` keep their + names) → commit → **push to origin**. diff --git a/Makefile b/Makefile index 70ff986..61c1e2b 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PULSE = src/pulse.c # stamp each web page with today's date + its own md5/sha256 (run before deploy) stamp: - @node web/stamp.js web/chat.html web/zebra-audio.html web/how-it-works.html + @node web/stamp.js web/chat.html web/zebra-audio.html web/how-it-works.html web/host-your-own.html all: tx rx chat bt carrier zebrad diff --git a/web/chat.html b/web/chat.html index c37ec31..7579faf 100644 --- a/web/chat.html +++ b/web/chat.html @@ -169,6 +169,7 @@

zebra report

volume modem chatroom  ·  e2e encrypted  ·  webrtc carrier  ·  how it works  ·  + host your own  ·  unturf

@@ -1649,9 +1650,10 @@ logLine('sys', 'chat content lives in encrypted SRTP audio. no IP packets carry diff --git a/web/host-your-own.html b/web/host-your-own.html new file mode 100644 index 0000000..88d6185 --- /dev/null +++ b/web/host-your-own.html @@ -0,0 +1,393 @@ + + + + + +zebra report — host your own community + + + + +

host your own

+

run your own zebra community  ·  self-hosted TURN + rendezvous on one edge box  ·  + how it works  ·  + open the chat  ·  + unturf

+ +

+ The zebra pages (the chat and the voice + call) need exactly two things from a server: a rendezvous relay + so two browsers can find each other, and a TURN server so they + can still connect when both sit behind NAT. Everything else — the crypto, + the modem, the audio — runs in the browser. This page hands you the whole + back end so you can run it for your own community on a single small box. +

+ +

+ This is the exact infrastructure behind www.unturf.com/zebra-report, + written out so you can stand up your own. It is a gift — reproduce it, + fork it, harden it. Your members then point the existing pages at your servers + with two URL parameters; nothing about the client needs to change. +

+ +▶ jump to "point your members at it" + +

1 · what you are building

+

+ One internet-facing box (a $5–$6/mo VPS is plenty for a small community) + running three daemons behind a TLS reverse proxy: +

+ +

+ The mint and the relay are the same small program here, but they are independent + — split them if you like. coturn is off-the-shelf. +

+ +

2 · the shape of it

+
browser A rendezvous (wss, encrypted SDP) browser B + ┌──────────┐ ◄──────────────────────────────────────────────────────► ┌──────────┐ + │ zebra │ │ zebra │ + │ page │ ──┐ ┌── │ page │ + └──────────┘ │ GET /turn-cred (https) → short-lived HMAC cred │ └──────────┘ + ▲ │ │ ▲ + │ └────────────────────────┐ ┌────────────────┘ │ + │ media (DTLS-SRTP, encrypted) ▼ ▼ media (DTLS-SRTP) │ + │ ┌─────────────────────────────┐ │ + └─────────────────────────►│ coturn TURN/STUN :3478 │◄──────────────┘ + │ relay UDP 49152-50151 │ + ┌──────────────────────────────┴─────────────────────────────┴───────────────┐ + │ your edge box Caddy (auto-TLS) │ + │ wss://you/zebra-signal ─► relay :8090 │ + │ https://you/turn-cred ─► mint :8090 │ + └──────────────────────────────────────────────────────────────────────────────┘
+

+ When the network allows it, the two browsers talk directly and + coturn never touches the media. coturn is the fallback that guarantees a + connection; the rendezvous relay is only used for the few hundred bytes of + setup, then sits idle. +

+ +

3 · coturn — the TURN relay

+

+ Install it (apt install coturn on Debian/Ubuntu) and replace + /etc/turnserver.conf with this. Swap in your box's public IP and a + DNS name you control: +

+
# /etc/turnserver.conf +external-ip=YOUR.PUBLIC.IP +relay-ip=YOUR.PUBLIC.IP +listening-port=3478 +realm=turn.example.com + +# relay allocation range — open these UDP ports in your firewall too +min-port=49152 +max-port=50151 + +# time-limited credentials: the mint computes HMAC-SHA1(secret, expiry). +# the secret is appended below at deploy and never committed. +use-auth-secret +# static-auth-secret=<injected at deploy, see step 4> + +# abuse quotas — per ephemeral user, so they stay tight as you scale +total-quota=2000 +user-quota=6 +bps-capacity=400000000 +max-bps=2000000 +stale-nonce=600 + +fingerprint +no-cli +no-loopback-peers +no-multicast-peers +log-file=/var/log/coturn/coturn.log +simple-log
+

+ Run it under systemd as an unprivileged user (all ports are above 1024, so no + special capabilities are needed): +

+
# /etc/systemd/system/coturn.service +[Unit] +Description=coturn TURN/STUN relay for WebRTC NAT traversal +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=turnserver +Group=turnserver +ExecStart=/usr/bin/turnserver -c /etc/turnserver.conf +Restart=on-failure +RestartSec=5 +LogsDirectory=coturn +LogsDirectoryMode=0750 +PrivateTmp=true +ProtectSystem=full +ProtectHome=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target
+

+ Firewall: allow inbound UDP 3478 (and TCP 3478 if you + offer TCP relay) plus the whole UDP 49152-50151 range. On a cloud + provider, that means a firewall rule, not just ufw. +

+ +

4 · the shared secret

+

+ coturn and the mint share one secret. The mint signs each ephemeral credential + with it; coturn validates against the same value. Generate your own + — never reuse anyone else's, never print it, never commit it: +

+
# run once, as root, on the box +umask 077 +openssl rand -hex 32 > /etc/zebra-turn-secret +chmod 600 /etc/zebra-turn-secret + +SECRET=$(cat /etc/zebra-turn-secret) + +# wire it into coturn +sed -i '/^static-auth-secret=/d' /etc/turnserver.conf +printf 'static-auth-secret=%s\n' "$SECRET" >> /etc/turnserver.conf + +# and into the mint's environment +printf 'ZEBRA_TURN_SECRET=%s\n' "$SECRET" > /etc/zebra-signal.env +chmod 640 /etc/zebra-signal.env
+

+ Generate once and persist it: rotating the secret invalidates every credential + already handed out, dropping live calls. Keep the file 600, owned + by root. +

+ +

5 · minting credentials — /turn-cred

+

+ This is the only non-obvious piece, and it is tiny. coturn's + use-auth-secret mode accepts any username whose value is a future + unix timestamp, with the password being + base64(HMAC‑SHA1(secret, username)). So the endpoint just + stamps an expiry and signs it. In Go: +

+
const turnTTL = 12 * 3600 // seconds a credential stays valid + +func turnCred(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") // browsers (and file:// copies) can fetch + w.Header().Set("Content-Type", "application/json") + secret := os.Getenv("ZEBRA_TURN_SECRET") + if secret == "" { http.Error(w, `{"error":"turn unavailable"}`, 503); return } + + username := strconv.FormatInt(time.Now().Unix()+turnTTL, 10) + mac := hmac.New(sha1.New, []byte(secret)) + mac.Write([]byte(username)) + json.NewEncoder(w).Encode(map[string]any{ + "username": username, + "credential": base64.StdEncoding.EncodeToString(mac.Sum(nil)), + "ttl": turnTTL, + "stun": []string{"stun:turn.example.com:3478"}, + "uris": []string{ + "turn:turn.example.com:3478?transport=udp", + "turn:turn.example.com:3478?transport=tcp", + }, + }) +}
+

+ That Access-Control-Allow-Origin: * matters: it is what lets a + browser on any page — including a copy of the zebra page saved to disk and + opened from file:// — fetch a credential. The credential is + short-lived and per-user, so handing it out openly is by design. +

+ +

6 · the rendezvous relay

+

+ The relay is a stateless WebSocket server, a few hundred lines of standard + library, no database. Its whole job: +

+ +

+ Run it under systemd as an unprivileged user, reading the secret from the env + file written in step 4: +

+
# /etc/systemd/system/zebra-signal.service +[Unit] +Description=zebra-signal — WebRTC rendezvous relay + TURN credential mint +After=network.target + +[Service] +Type=simple +User=www-data +Group=www-data +Environment=ZEBRA_SIGNAL_ADDR=:8090 +EnvironmentFile=-/etc/zebra-signal.env +ExecStart=/usr/local/bin/zebra-signal +Restart=on-failure +RestartSec=5 +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=full +ProtectHome=true + +[Install] +WantedBy=multi-user.target
+ +

7 · TLS + reverse proxy

+

+ Browsers require wss:// (TLS) for WebSockets and a secure context + for the crypto, so put a reverse proxy in front that terminates TLS. With + Caddy you get automatic certificates and + the config is four lines: +

+
# Caddyfile +turn.example.com { + handle /zebra-signal* { reverse_proxy localhost:8090 } + handle /turn-cred { reverse_proxy localhost:8090 } +}
+

+ Caddy fetches a Let's Encrypt certificate on first request. The WebSocket + upgrade is proxied transparently; the * CORS header set by the mint + passes straight through. That is the entire edge. +

+ +

8 · point your members at it

+

+ Now the payoff: nobody needs a modified page. The published + zebra pages read two URL parameters and fall back to the unturf servers only if + they are absent. Send your community a link with your own endpoints: +

+
https://www.unturf.com/zebra-report/zebra-audio.html?signal=wss://turn.example.com/zebra-signal&turncred=https://turn.example.com/turn-cred
+

+ Or host the page yourself (it is a single self-contained HTML file) and serve it + from the same box. Either way, the call is established through your + relay and, when needed, relayed through your coturn. The same two + parameters work on the text chat (index.html) and the voice call + (zebra-audio.html). +

+

+ Saved a copy to disk? It still works from file:// — the crypto + runs in a secure context and the * CORS header lets the saved file + fetch credentials — as long as your relay and TURN server are reachable. +

+ +

9 · how many users can it carry

+

+ The rendezvous relay is nearly free: it moves a few hundred bytes per call setup + and then idles, so a tiny box pairs thousands of rooms. coturn is the + ceiling, and only for relayed calls (direct peer-to-peer calls cost it + nothing). Each relayed voice call is bidirectional audio — tens of kbit/s + per leg. With bps-capacity=400000000 (400 Mbit/s) the limit is + whatever your VPS's actual uplink and monthly transfer allow, long before coturn + itself strains. +

+

+ The user-quota and total-quota lines cap concurrent + allocations to blunt abuse. Raise total-quota as you grow; keep + user-quota small (a handful of allocations per credential is plenty + for one call). Because credentials are per-user and expire, a leaked one is + worthless within hours. +

+ +
+ +

10 · it's a gift

+

+ This stack is open intellectual capital — take it and run a community the + unturf servers will never see or meter. Patch it, harden it, pass it on. Every + box that runs its own relay makes the whole mesh more resilient and less + centralised, which is the entire point. +

+▶ open the chat +  +▶ open the voice call + +

+ zebra report · host your own community · + unturf +

+ + + + diff --git a/web/how-it-works.html b/web/how-it-works.html index e3f055c..163fcb6 100644 --- a/web/how-it-works.html +++ b/web/how-it-works.html @@ -60,7 +60,8 @@

zebra report

how it works  ·  a volume-modem chatroom over webrtc  ·  - open the chat  ·  unturf

+ open the chat  ·  + host your own  ·  unturf

Zebra report is a two-person chatroom where your words never travel as network @@ -267,9 +268,10 @@