diff --git a/continuwuity/README.md b/continuwuity/README.md new file mode 100644 index 0000000..5394dc6 --- /dev/null +++ b/continuwuity/README.md @@ -0,0 +1,238 @@ +# Matrix Self-Hosted Stack — Setup Guide + +## File Overview + +``` +matrix-stack/ +├── docker-compose.yml # All services: Continuwuity, Traefik, Coturn, LiveKit, lk-jwt-service +├── continuwuity.toml # Homeserver config: TURN URIs, LiveKit foci +├── livekit.yaml # LiveKit media server config +├── coturn.conf # TURN/STUN server config +└── README.md # This file +``` + +--- + +## Prerequisites + +1. Two DNS A records pointing to your server's public IP: + - `matrix.example.com` + - `livekit.example.com` + (If you want your Matrix IDs as `@user:example.com` rather than + `@user:matrix.example.com`, also point `example.com` to your server.) + +2. Docker + Docker Compose installed. + +3. Create the external proxy network Traefik uses: + ```bash + docker network create proxy + ``` + +--- + +## One-Time Secret Generation + +### Coturn secret +```bash +# Install pwgen if needed: apt install pwgen +pwgen -s 64 1 +``` +Paste the output into both `coturn.conf` (static-auth-secret) +and `continuwuity.toml` (turn_secret). They MUST match. + +### LiveKit keys +```bash +docker run --rm livekit/livekit-server:latest generate-keys +``` +This outputs a key (~20 chars) and secret (~64 chars). +Paste them into: +- `livekit.yaml` → keys section +- `docker-compose.yml` → lk-jwt-service LIVEKIT_KEY / LIVEKIT_SECRET + +### Registration token (for invite-only signup) +```bash +pwgen -s 32 1 +``` +Paste into docker-compose.yml → CONTINUWUITY_REGISTRATION_TOKEN. + +--- + +## Starting Up + +```bash +# First boot — creates admin user automatically +docker compose up -d + +# Get your admin password +docker compose logs homeserver | grep "Created user" + +# IMPORTANT: Edit docker-compose.yml and remove the --execute flag +# from the homeserver command, then restart: +docker compose up -d homeserver +``` + +Log in with any Matrix client (Element, Cinny, etc.) using +`@admin:example.com` and the generated password. + +--- + +## Firewall Rules + +Open these ports on your server's firewall (ufw examples shown). + +### HTTP/HTTPS — handled by Traefik +```bash +ufw allow 80/tcp # HTTP (redirected to HTTPS by Traefik) +ufw allow 443/tcp # HTTPS for matrix.example.com and livekit.example.com +``` + +### Coturn — TURN/STUN (raw UDP/TCP, NOT proxied by Traefik) +```bash +ufw allow 3478/tcp # STUN + TURN +ufw allow 3478/udp +ufw allow 5349/tcp # TURN over TLS (if you configured TLS in coturn.conf) +ufw allow 5349/udp +ufw allow 50201:65535/udp # Coturn media relay port range +``` + +### LiveKit — RTC media (raw UDP/TCP, NOT proxied by Traefik) +```bash +ufw allow 7881/tcp # LiveKit direct TCP (for clients that can't UDP) +ufw allow 50100:50200/udp # LiveKit media relay port range +``` + +### Summary table + +| Port(s) | Protocol | Service | Via Traefik? | +|----------------|-----------|--------------------|--------------| +| 80 | TCP | HTTP (→ HTTPS) | Yes | +| 443 | TCP | HTTPS | Yes | +| 3478 | TCP + UDP | Coturn TURN/STUN | No — direct | +| 5349 | TCP + UDP | Coturn TURN TLS | No — direct | +| 7881 | TCP | LiveKit direct TCP | No — direct | +| 50100–50200 | UDP | LiveKit media | No — direct | +| 50201–65535 | UDP | Coturn media relay | No — direct | + +**Key point**: Traefik only handles ports 80 and 443. The Matrix +homeserver (6167) and LiveKit HTTP (7880) are never exposed directly +— Traefik proxies them internally. Coturn and LiveKit's RTC ports +bypass Traefik entirely and are opened directly to the internet. + +--- + +## Can I use an External NGINX Proxy Manager instead of Traefik? + +**Short answer: yes, but with important caveats.** + +### What NPM can handle +- `matrix.example.com` → proxy to `http://YOUR_SERVER_IP:6167` + - Enable WebSockets + - Set `X-Forwarded-For`, `Host`, `X-Real-IP` headers +- `livekit.example.com` with path splitting: + - `/sfu/get`, `/healthz`, `/get_token` → `http://YOUR_SERVER_IP:8081` + - Everything else → `http://YOUR_SERVER_IP:7880` + - Enable WebSockets on both +- NPM handles Let's Encrypt TLS automatically + +### What NPM cannot handle +- **Coturn** — raw UDP/TCP, not HTTP. NPM (like Traefik) can't proxy it. + You just open ports 3478/5349 directly and point DNS at your server IP. +- **LiveKit RTC ports** (7881/tcp, 50100-50200/udp) — same story. + These bypass any reverse proxy entirely. + +### Changes needed to docker-compose.yml for NPM + +1. **Remove the entire `traefik:` service block.** + +2. **Remove all `labels:` blocks** from homeserver and lk-jwt-service. + +3. **Expose the ports NPM needs to reach:** + ```yaml + homeserver: + ports: + - "127.0.0.1:6167:6167" # NPM proxies this + + lk-jwt-service: + ports: + - "127.0.0.1:8081:8081" # NPM proxies this + + # livekit already uses network_mode: host, so 7880 is available + # on the host at 127.0.0.1:7880 automatically + ``` + Using `127.0.0.1:` prefix means only NPM (on the same host or + your network) can reach them — not the open internet. + +4. **Remove the `acme:` volume** (NPM handles TLS). + +5. In NPM, configure two proxy hosts: + + **matrix.example.com** + - Forward to: `http://YOUR_SERVER_IP:6167` + - Websockets: ON + - Custom nginx config: + ```nginx + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + ``` + + **livekit.example.com** + - Forward to: `http://YOUR_SERVER_IP:7880` (default) + - Websockets: ON + - Advanced tab — add this custom config for path splitting: + ```nginx + location ~ ^/(sfu/get|healthz|get_token) { + proxy_pass http://YOUR_SERVER_IP:8081$request_uri; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_buffering off; + } + ``` + +### NPM on a separate machine? +If NPM runs on a different server, replace `127.0.0.1` with your +Matrix server's internal IP, and make sure port 6167 is firewalled +to only allow connections from NPM's IP. + +--- + +## Verification + +### Test TURN credentials +```bash +curl "https://matrix.example.com/_matrix/client/r0/voip/turnServer" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" | jq +``` +Should return a JSON object with `uris`, `username`, `password`. + +### Test TURN connectivity +1. Go to https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ +2. Paste the credentials from above +3. Click "Gather candidates" — look for `relay` type candidates + +### Test LiveKit +1. GET `https://livekit.example.com/healthz` — should return 200 +2. Use https://livekit.io/connection-test with a token from `/get_token` + +### Test federation +https://federationtester.matrix.org — enter your domain + +--- + +## Troubleshooting + +**Federation not working** +- Check `.well-known/matrix/server` is reachable: + `curl https://example.com/.well-known/matrix/server` +- Should return: `{"m.server":"matrix.example.com:443"}` + +**TURN not working** +- Verify firewall allows 3478/udp from the internet +- Check coturn logs: `docker compose logs coturn` +- Confirm `turn_secret` in continuwuity.toml matches `static-auth-secret` in coturn.conf + +**Element Call / group calls failing** +- Check lk-jwt-service is reachable: `curl https://livekit.example.com/healthz` +- Confirm LiveKit UDP ports (50100-50200) are open +- Check `foci` URL in continuwuity.toml matches where lk-jwt-service is deployed diff --git a/continuwuity/continuwuity.toml b/continuwuity/continuwuity.toml new file mode 100644 index 0000000..97284fa --- /dev/null +++ b/continuwuity/continuwuity.toml @@ -0,0 +1,45 @@ +# ============================================================ +# continuwuity.toml — Continuwuity Homeserver Configuration +# Mount this at /etc/continuwuity.toml in the container. +# +# Most settings are handled via environment variables in +# docker-compose.yml. This file handles things that are +# easier to express in TOML: TURN and MatrixRTC config. +# ============================================================ + +[global] + +# ------------------------------------------------------------ +# TURN / STUN — for legacy 1:1 voice and video calls +# Uses shared secret auth (time-limited credentials, more secure +# than static username/password). +# The turn_secret MUST match static-auth-secret in coturn.conf. +# ------------------------------------------------------------ +turn_uris = [ + "turn:matrix.example.com?transport=udp", + "turn:matrix.example.com?transport=tcp", + "turns:matrix.example.com?transport=udp", # TURN over TLS (port 5349) + "turns:matrix.example.com?transport=tcp" +] + +# Must match `static-auth-secret` in coturn.conf +turn_secret = "YOUR_COTURN_SECRET" # EDIT THIS + +# How long TURN credentials are valid (seconds). 24h default. +turn_ttl = 86400 + +# Set to true only if you want unauthenticated/guest users to +# be able to use TURN. Not recommended — leaves TURN open to abuse. +turn_allow_guests = false + + +# ------------------------------------------------------------ +# MatrixRTC / Element Call — group calls via LiveKit +# Clients discover the LiveKit endpoint via the +# /_matrix/client/v1/rtc/transports API (MSC4143). +# The URL here points to your lk-jwt-service, NOT LiveKit directly. +# ------------------------------------------------------------ +[global.matrix_rtc] +foci = [ + { type = "livekit", livekit_service_url = "https://livekit.example.com" }, +] diff --git a/continuwuity/coturn.conf b/continuwuity/coturn.conf new file mode 100644 index 0000000..7134283 --- /dev/null +++ b/continuwuity/coturn.conf @@ -0,0 +1,51 @@ +# ============================================================ +# coturn.conf — Coturn TURN/STUN Server Configuration +# Mount this at /etc/coturn/turnserver.conf in the container. +# +# Generate a secret with: pwgen -s 64 1 +# The secret here MUST match turn_secret in continuwuity.toml. +# ============================================================ + +# Use time-limited shared-secret auth (more secure than static credentials) +use-auth-secret +static-auth-secret=YOUR_COTURN_SECRET # EDIT THIS — must match continuwuity.toml + +# realm should match your Matrix domain +realm=matrix.example.com # EDIT THIS + +# ------------------------------------------------------------ +# Port ranges +# Default coturn range is 49152-65535. +# We start at 50201 so it doesn't overlap with LiveKit (50100-50200). +# ------------------------------------------------------------ +min-port=50201 +max-port=65535 + +# ------------------------------------------------------------ +# Optional: TLS support (recommended for production) +# You'll need to provide certificates. One approach is to copy +# them from your Let's Encrypt store (requires a cron/hook). +# Comment these out if you're not setting up TLS on coturn. +# ------------------------------------------------------------ +# tls-listening-port=5349 +# cert=/etc/coturn/certs/fullchain.pem +# pkey=/etc/coturn/certs/privkey.pem + +# ------------------------------------------------------------ +# Security hardening +# Prevents coturn from being used as an open relay/proxy. +# ------------------------------------------------------------ +# Deny connections to private/loopback IP ranges (prevents SSRF) +denied-peer-ip=10.0.0.0-10.255.255.255 +denied-peer-ip=192.168.0.0-192.168.255.255 +denied-peer-ip=172.16.0.0-172.31.255.255 +denied-peer-ip=127.0.0.0-127.255.255.255 + +# Only allow relay to public IPs +no-multicast-peers + +# Disable the web admin interface (not needed, reduces attack surface) +no-cli + +# Log to stdout so Docker captures it +log-file=stdout diff --git a/continuwuity/docker-compose.yml b/continuwuity/docker-compose.yml new file mode 100644 index 0000000..78b2943 --- /dev/null +++ b/continuwuity/docker-compose.yml @@ -0,0 +1,180 @@ +# ============================================================ +# Matrix Self-Hosted Stack +# Services: Continuwuity, Coturn, LiveKit, lk-jwt-service +# +# Domains (replace throughout): +# matrix.example.com — homeserver +# livekit.example.com — LiveKit + JWT service +# +# Before starting: +# 1. Fill in all YOUR_* placeholders +# 2. Set your email for Let's Encrypt +# 3. Run: docker network create proxy +# 4. Then: docker compose up -d +# 5. Grab admin password: docker compose logs homeserver | grep "Created user" +# 6. Remove the --execute flag from homeserver command after first boot +# ============================================================ + +services: + + # ---------------------------------------------------------- + # Traefik — Reverse Proxy & TLS termination + # Handles HTTPS for matrix.example.com and livekit.example.com + # Does NOT handle coturn (raw UDP/TCP, not HTTP) + # ---------------------------------------------------------- + traefik: + image: traefik:latest + container_name: traefik + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - acme:/etc/traefik/acme + environment: + TRAEFIK_LOG_LEVEL: INFO + TRAEFIK_ENTRYPOINTS_WEB_ADDRESS: ":80" + TRAEFIK_ENTRYPOINTS_WEB_HTTP_REDIRECTIONS_ENTRYPOINT_TO: websecure + TRAEFIK_ENTRYPOINTS_WEBSECURE_ADDRESS: ":443" + TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_TLS_CERTRESOLVER: letsencrypt + # Allow encoded characters needed by Matrix + TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDSLASH: "true" + TRAEFIK_ENTRYPOINTS_WEBSECURE_HTTP_ENCODEDCHARACTERS_ALLOWENCODEDHASH: "true" + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT: "true" + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_EMAIL: "you@example.com" # EDIT THIS + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_KEYTYPE: EC384 + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE: "true" + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_HTTPCHALLENGE_ENTRYPOINT: web + TRAEFIK_CERTIFICATESRESOLVERS_LETSENCRYPT_ACME_STORAGE: /etc/traefik/acme/acme.json + TRAEFIK_PROVIDERS_DOCKER: "true" + TRAEFIK_PROVIDERS_DOCKER_EXPOSEDBYDEFAULT: "false" + networks: + - proxy + + # ---------------------------------------------------------- + # Continuwuity — Matrix Homeserver + # ---------------------------------------------------------- + homeserver: + image: forgejo.ellis.link/continuwuation/continuwuity:latest + container_name: continuwuity + restart: unless-stopped + # Remove the --execute flag after first boot once you have your admin password + command: /sbin/conduwuit --execute "users create-user admin" + depends_on: + - traefik + volumes: + - db:/var/lib/continuwuity + - ./continuwuity.toml:/etc/continuwuity.toml:ro + # Use host DNS to avoid Docker's resolver causing federation issues + - /etc/resolv.conf:/etc/resolv.conf:ro + networks: + - proxy + labels: + - "traefik.enable=true" + # Serve matrix.example.com AND well-known paths on example.com for delegation + - "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))" + - "traefik.http.routers.continuwuity.entrypoints=websecure" + - "traefik.http.routers.continuwuity.tls.certresolver=letsencrypt" + - "traefik.http.services.continuwuity.loadbalancer.server.port=6167" + environment: + CONTINUWUITY_SERVER_NAME: "example.com" # Your Matrix ID domain (@user:example.com) + CONTINUWUITY_DATABASE_PATH: /var/lib/continuwuity + CONTINUWUITY_ADDRESS: 0.0.0.0 + CONTINUWUITY_PORT: 6167 + CONTINUWUITY_MAX_REQUEST_SIZE: "20000000" # ~20 MB + CONTINUWUITY_ALLOW_REGISTRATION: "false" # Enable with a token for invite-only + CONTINUWUITY_REGISTRATION_TOKEN: "YOUR_INVITE_TOKEN" # EDIT THIS + CONTINUWUITY_ALLOW_FEDERATION: "true" + CONTINUWUITY_ALLOW_CHECK_FOR_UPDATES: "true" + CONTINUWUITY_TRUSTED_SERVERS: '["matrix.org"]' + CONTINUWUITY_CONFIG: /etc/continuwuity.toml + # Well-known delegation so clients and servers find your homeserver + CONTINUWUITY_WELL_KNOWN: | + { + client=https://matrix.example.com, + server=matrix.example.com:443 + } + ulimits: + nofile: + soft: 1048567 + hard: 1048567 + + # ---------------------------------------------------------- + # Coturn — TURN/STUN server for legacy 1:1 calls + # Uses host networking — Traefik does NOT proxy this. + # Reachable directly on the host IP via UDP/TCP ports 3478/5349 + # and the relay port range 50201-65535. + # ---------------------------------------------------------- + coturn: + image: coturn/coturn:latest + container_name: coturn + restart: unless-stopped + network_mode: host + volumes: + - ./coturn.conf:/etc/coturn/turnserver.conf:ro + + # ---------------------------------------------------------- + # LiveKit — Media server for MatrixRTC / Element Call + # Also uses host networking for RTC performance. + # Traefik proxies HTTP/WS on port 7880 via livekit.example.com. + # ---------------------------------------------------------- + livekit: + image: livekit/livekit-server:latest + container_name: livekit + restart: unless-stopped + command: --config /etc/livekit.yaml + network_mode: host + volumes: + - ./livekit.yaml:/etc/livekit.yaml:ro + # Note: with network_mode: host, Traefik cannot directly label this container. + # The livekit.example.com routing is handled via the lk-jwt-service container + # labels, with LiveKit itself accessed at 127.0.0.1:7880 from that container's + # perspective. See lk-jwt-service labels below. + + # ---------------------------------------------------------- + # lk-jwt-service — Issues JWT tokens for LiveKit + # Matrix users authenticate here before joining calls. + # Traefik routes livekit.example.com here, with path-based + # splitting: /sfu/get, /healthz, /get_token go to this service; + # everything else is reverse-proxied onward to LiveKit on :7880. + # ---------------------------------------------------------- + lk-jwt-service: + image: ghcr.io/element-hq/lk-jwt-service:latest + container_name: lk-jwt-service + restart: unless-stopped + environment: + - LIVEKIT_JWT_BIND=:8081 + - LIVEKIT_URL=wss://livekit.example.com # EDIT: your LiveKit domain + - LIVEKIT_KEY=YOUR_LK_KEY # EDIT: from generate-keys + - LIVEKIT_SECRET=YOUR_LK_SECRET # EDIT: from generate-keys + - LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com # EDIT: your Matrix domain + ports: + - "127.0.0.1:8081:8081" # Only bind to localhost; Traefik reaches it here + networks: + - proxy + labels: + - "traefik.enable=true" + # High-priority rule: JWT paths go to lk-jwt-service + - "traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))" + - "traefik.http.routers.livekit-jwt.entrypoints=websecure" + - "traefik.http.routers.livekit-jwt.tls.certresolver=letsencrypt" + - "traefik.http.routers.livekit-jwt.priority=10" + - "traefik.http.services.livekit-jwt.loadbalancer.server.port=8081" + # Low-priority rule: everything else on livekit.example.com goes to LiveKit itself + - "traefik.http.routers.livekit.rule=Host(`livekit.example.com`)" + - "traefik.http.routers.livekit.entrypoints=websecure" + - "traefik.http.routers.livekit.tls.certresolver=letsencrypt" + - "traefik.http.routers.livekit.priority=1" + - "traefik.http.services.livekit.loadbalancer.server.url=http://127.0.0.1:7880" + +# ---------------------------------------------------------- +# Volumes & Networks +# ---------------------------------------------------------- +volumes: + db: + acme: + +networks: + proxy: + external: true diff --git a/continuwuity/livekit.yaml b/continuwuity/livekit.yaml new file mode 100644 index 0000000..e424c5b --- /dev/null +++ b/continuwuity/livekit.yaml @@ -0,0 +1,68 @@ +# ============================================================ +# livekit.yaml — LiveKit Media Server Configuration +# Mount this at /etc/livekit.yaml in the livekit container. +# +# Replace LK_MATRIX_KEY and LK_MATRIX_SECRET with the values +# generated by: +# docker run --rm livekit/livekit-server:latest generate-keys +# ============================================================ + +# HTTP/WebSocket port (proxied by Traefik) +port: 7880 + +# Bind to all interfaces (Traefik reaches it on 127.0.0.1:7880 +# since both livekit and the host share the network stack) +bind_addresses: + - "" + +rtc: + # Direct TCP port for clients that can't use UDP + tcp_port: 7881 + + # UDP port range for media relay + # Starts at 50100 to leave room below for coturn if needed. + # Coturn is configured with min-port=50201 to avoid overlap. + port_range_start: 50100 + port_range_end: 50200 + + # Required when running behind NAT — LiveKit advertises your + # public IP to clients rather than the internal Docker IP + use_external_ip: true + + # Disable loopback candidates — not useful in production + enable_loopback_candidate: false + + # Optional: Use your coturn server as a TURN relay for LiveKit. + # This improves connectivity for clients behind strict firewalls. + # Uncomment and fill in if you want LiveKit to use coturn: + # + # turn_servers: + # - host: matrix.example.com + # port: 3478 + # protocol: udp + # secret: "YOUR_COTURN_SECRET" # Same secret as coturn.conf + # - host: matrix.example.com + # port: 3478 + # protocol: tcp + # secret: "YOUR_COTURN_SECRET" + # - host: matrix.example.com + # port: 5349 + # protocol: tls + # secret: "YOUR_COTURN_SECRET" + +# API keys — must match LIVEKIT_KEY and LIVEKIT_SECRET +# in the lk-jwt-service environment variables +keys: + YOUR_LK_KEY: YOUR_LK_SECRET # EDIT BOTH OF THESE + +# Optional: enable LiveKit's built-in TURN server. +# Only useful for LiveKit connections — cannot be used by coturn +# or legacy Matrix calls. Use this if you don't want a separate +# coturn deployment and only care about group calls. +# +# turn: +# enabled: true +# udp_port: 3478 +# relay_range_start: 50300 +# relay_range_end: 50400 +# domain: livekit.example.com