sueta / docs / SIGNAL-SERVER.md
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# sueta-signal — reusable blind signaling relay

One small Go binary on the coturn box (78.47.93.79) serves **every** p2p app we
build on this stack. It does exactly three things:

1. `wss://<host>/ws` — room-scoped blind relay of opaque encrypted blobs
   (it cannot read SDP, names, or messages; see `PROTOCOL.md` §8).
2. `GET /turn-credentials` — mints ephemeral coturn credentials (REST scheme).
3. `GET /healthz` — room/peer counts.

TLS is built in: Let's Encrypt via autocert, HTTP-01 on :80, certs cached in
`/var/lib/sueta-signal/certs`. No nginx, no caddy, no certbot.

## Self-host in one command

On any Ubuntu 20.04+ / Debian 11+ box (fresh or not), with DNS already
pointing a hostname at it:

```sh
git clone https://git-chat.ardegazu.ro/sueta.git
sudo sueta/deploy/install-server.sh signal.example.com
```

That installs coturn + a pinned Go toolchain, builds the relay from source,
generates the shared TURN secret, renders both configs, sets up systemd and
ufw (if active), and prints the client build line. One hostname serves both
STUN/TURN (:3478) and the relay (:443). Idempotent — re-runs keep your secret
and env; `--force` re-renders configs. Verified end-to-end against a live box.

## Current production setup

| What | Where |
|---|---|
| binary | `/usr/local/bin/sueta-signal` |
| unit | `/etc/systemd/system/sueta-signal.service` |
| env | `/etc/sueta-signal.env` (chmod 600) |
| shared TURN secret | `/etc/sueta-signal.secret` (created once, reused by redeploys) |
| coturn | `/etc/turnserver.conf` — TURN with `use-auth-secret`, SSRF-hardened |
| DNS | `signal.ardegazu.ro` A → 78.47.93.79, `stun.ardegazu.ro` A → 78.47.93.79 |

Deploy/update: `deploy/deploy-server.sh` (cross-compiles, uploads, restarts).
Logs: `ssh root@78.47.93.79 journalctl -u sueta-signal -f` (never contains payloads).

## Env vars

| Var | Meaning | Example |
|---|---|---|
| `ALLOWED_HOSTS` | hostnames autocert may issue certs for (comma) | `signal.ardegazu.ro,signal.example.com` |
| `ORIGINS` | allowed WS `Origin`s: exact, `*.domain.tld`, or `*`; empty = any | `https://chat.ardegazu.ro,*.ardegazu.ro` |
| `STATIC_AUTH_SECRET` | must equal coturn's `static-auth-secret` | (hex64) |
| `TURN_URLS` | ICE urls handed to clients (comma) | `stun:stun.ardegazu.ro:3478,...` |

Limits (compiled defaults): 500 rooms, 16 peers/room, 64 KiB frames,
30 msg/s per conn, 20 conns per IP, 10 turn-credential requests/min per IP.

## Building a NEW app on this stack

The relay is app-agnostic — apps are isolated by their **app salt** (HKDF salt,
see `client/src/lib/README.md`). To reuse it from a new app:

1. Pick an app salt, e.g. `"drawings.ardegazu.ro/v1"`. Same room secret +
   different salt ⇒ different roomId, so apps can never collide.
2. Copy or import `client/src/lib/` (crypto, signaling, mesh, turn) — it has
   zero app-specific code.
3. Point the app at `wss://signal.ardegazu.ro/ws` — reusing the existing
   hostname is fine (rooms are already namespaced). Add the new app's origin to
   `ORIGINS` in `/etc/sueta-signal.env` and `systemctl restart sueta-signal`.

### Giving an app its own signal hostname (optional, keeps domain identity)

1. DNS: A record `signal.<newdomain>` → 78.47.93.79 (any domain works).
2. Box: append the hostname to `ALLOWED_HOSTS` in `/etc/sueta-signal.env`,
   add the app origin to `ORIGINS`, then `systemctl restart sueta-signal`.
   The cert is issued automatically on first request.
3. Client: set `VITE_SIGNAL_URL=wss://signal.<newdomain>/ws` at build time.

## TURN notes

- Credentials: `username = unix-expiry`, `credential = b64(HMAC-SHA1(secret, username))`, TTL 1 h.
- coturn quotas: 16 sessions/user, 200 total, 1 Mbps/session cap.
- `denied-peer-ip` blocks relaying into private/loopback ranges and the box
  itself (SSRF hardening) — a relay allocation can only reach the public internet.
- Plain `turn:` on 3478/udp+tcp. WebRTC media is DTLS-encrypted regardless;
  `turns:5349` would only help behind TLS-only firewalls and needs a cert
  reachable by coturn — documented future option, not enabled.

## Hosting pattern (no origin server)

The app itself is served from IPFS through an ird tunnel that is *never
connected*: the tunnel falls back to its "offline page", which is the pinned
build. Deploy = `npm run build` + `tunnel_set_offline path=client/dist`
(re-running with unchanged content reuses the pin at no extra cost).