v6.8.4: the relay password now changes every day
This commit is contained in:
@@ -4,6 +4,14 @@
|
||||
# long-immutable cache for hashed/static assets, security headers, honest 404s.
|
||||
|
||||
worker_processes auto;
|
||||
|
||||
# njs runs the TURN credential endpoint (deploy/turn-credentials.js). The official
|
||||
# nginx image ships the module; it only has to be loaded.
|
||||
load_module modules/ngx_http_js_module.so;
|
||||
# nginx clears the environment of its workers; this keeps the coturn REST-API
|
||||
# secret (a Fly secret) visible to that script and to nothing else.
|
||||
env TURN_SECRET;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
@@ -69,6 +77,8 @@ http {
|
||||
# one-year immutable cache, which would freeze a sitemap for a year.
|
||||
~^/robots\.txt$ "public, max-age=3600";
|
||||
~^/sitemap\.xml$ "public, max-age=3600";
|
||||
# Relay credentials are minted per request and must never be stored.
|
||||
~^/api/ "no-store";
|
||||
}
|
||||
|
||||
# CDN-Cache-Control is read by Cloudflare (and other CDNs) *independently* of the
|
||||
@@ -88,8 +98,28 @@ http {
|
||||
~^/config/ice-servers\.js$ "no-store";
|
||||
~^/dist/ "no-store";
|
||||
~^/src/i18n/ "no-store";
|
||||
~^/api/ "no-store";
|
||||
}
|
||||
|
||||
# ---- TURN credential endpoint ----
|
||||
js_path /etc/nginx/njs/;
|
||||
js_import turncreds from turn-credentials.js;
|
||||
|
||||
# Rate-limit by the real client. Behind Cloudflare every request arrives from
|
||||
# an edge address, so keying on the connection would throttle unrelated users
|
||||
# together; CF-Connecting-IP is the visitor. Requests that reach Fly directly
|
||||
# carry no such header and fall back to Fly's own client address. A caller who
|
||||
# forges the header can dodge the per-client limit, which is why there is
|
||||
# also a global ceiling below — and coturn has its own quotas behind both.
|
||||
map $http_cf_connecting_ip $turn_client {
|
||||
"" $http_fly_client_ip;
|
||||
default $http_cf_connecting_ip;
|
||||
}
|
||||
# A client needs one credential per day plus a few for reloads and new tabs.
|
||||
limit_req_zone $turn_client zone=turn_per_client:2m rate=10r/m;
|
||||
limit_req_zone $server_name zone=turn_global:1m rate=20r/s;
|
||||
limit_req_status 429;
|
||||
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
listen [::]:8080 default_server;
|
||||
@@ -113,6 +143,13 @@ http {
|
||||
add_header CDN-Cache-Control $sb_cdn_cache always;
|
||||
add_header Service-Worker-Allowed "/" always;
|
||||
|
||||
# Short-lived TURN relay credentials. See deploy/turn-credentials.js.
|
||||
location = /api/turn-credentials {
|
||||
limit_req zone=turn_per_client burst=10 nodelay;
|
||||
limit_req zone=turn_global burst=100 nodelay;
|
||||
js_content turncreds.handle;
|
||||
}
|
||||
|
||||
# Real asset files must return 404 when missing — never fall back to the
|
||||
# HTML shell, which would be served with the wrong content type and break
|
||||
# module/script loading (e.g. a missing config/ice-servers.js).
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// Short-lived TURN credentials for SecureBit clients, served by nginx (njs) at
|
||||
// POST /api/turn-credentials.
|
||||
//
|
||||
// WHY THIS EXISTS
|
||||
// ---------------
|
||||
// The relay used to be reached with one credential that was valid until 2038 and
|
||||
// shipped inside every client. Anything shipped inside a client is public, so
|
||||
// anyone could lift it and relay their own traffic through our server for years.
|
||||
// Here the coturn REST-API secret stays on the server and each client asks for a
|
||||
// credential that expires in a day. A lifted credential stops working on its own,
|
||||
// and getting a new one means coming back here, where requests are rate-limited.
|
||||
//
|
||||
// WHAT IT DOES NOT DO
|
||||
// -------------------
|
||||
// It cannot prove the caller is our app. A browser cannot lie about Origin, so
|
||||
// other WEBSITES cannot use our relay for their visitors; a script outside a
|
||||
// browser can send any Origin it likes, or none (native apps send none). Those
|
||||
// callers are bounded by the rate limit in nginx.conf and by coturn's own quotas.
|
||||
//
|
||||
// It is not a signalling service: it never sees a message, an SDP or who talks to
|
||||
// whom. It hands out a relay credential and forgets the request.
|
||||
//
|
||||
// Plain ES module on purpose: njs runs it in nginx, and the test suite imports the
|
||||
// same file under Node (both provide crypto.createHmac).
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
// Long enough to outlast a call: coturn re-checks the expiry on every refresh of
|
||||
// an allocation, so a credential that expires mid-call drops the relayed leg.
|
||||
// Clients fetch a fresh one well before this runs out.
|
||||
const TTL_SECONDS = 24 * 60 * 60;
|
||||
|
||||
const ALLOWED_ORIGINS = [
|
||||
'https://securebit.chat',
|
||||
'https://securebit-chat.fly.dev',
|
||||
// Desktop (Tauri) webviews: macOS/Linux, then Windows.
|
||||
'tauri://localhost',
|
||||
'http://tauri.localhost',
|
||||
'https://tauri.localhost',
|
||||
];
|
||||
|
||||
const TURN_URLS = [
|
||||
'turn:turn.securebit.chat:3478?transport=udp',
|
||||
'turn:turn.securebit.chat:3478?transport=tcp',
|
||||
// Raw-IP fallback for clients whose WebRTC stack cannot resolve the name.
|
||||
'turn:144.172.96.126:3478?transport=udp',
|
||||
'turn:144.172.96.126:3478?transport=tcp',
|
||||
'turns:turn.securebit.chat:443?transport=tcp',
|
||||
];
|
||||
|
||||
/** coturn REST-API credential: username "<expiry>:<label>", password HMAC-SHA1. */
|
||||
function makeCredential(secret, nowSeconds) {
|
||||
const username = (Math.floor(nowSeconds) + TTL_SECONDS) + ':securebit';
|
||||
const credential = crypto.createHmac('sha1', secret).update(username).digest('base64');
|
||||
return { username: username, credential: credential };
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide a request. Pure, so it can be tested without nginx.
|
||||
* @returns {{status:number, body?:object, allowOrigin?:string}}
|
||||
*/
|
||||
// Written without destructuring or default parameters: njs does not parse them.
|
||||
function decide(req) {
|
||||
const method = req.method;
|
||||
const origin = req.origin;
|
||||
const secret = req.secret;
|
||||
// A browser always sends Origin on a POST. A missing one is a native app or a
|
||||
// script — let it through; the rate limit is what bounds it.
|
||||
const hasOrigin = typeof origin === 'string' && origin.length > 0;
|
||||
if (hasOrigin && ALLOWED_ORIGINS.indexOf(origin) === -1) return { status: 403 };
|
||||
|
||||
const allowOrigin = hasOrigin ? origin : undefined;
|
||||
if (method === 'OPTIONS') return { status: 204, allowOrigin: allowOrigin };
|
||||
if (method !== 'POST') return { status: 405 };
|
||||
if (!secret) return { status: 503 };
|
||||
|
||||
const cred = makeCredential(secret, req.nowSeconds);
|
||||
return {
|
||||
status: 200,
|
||||
allowOrigin: allowOrigin,
|
||||
body: {
|
||||
ttl: TTL_SECONDS,
|
||||
iceServers: [{ urls: TURN_URLS, username: cred.username, credential: cred.credential }],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function handle(r) {
|
||||
const result = decide({
|
||||
method: r.method,
|
||||
origin: r.headersIn['Origin'],
|
||||
secret: process.env.TURN_SECRET,
|
||||
nowSeconds: Date.now() / 1000,
|
||||
});
|
||||
|
||||
if (result.allowOrigin) {
|
||||
r.headersOut['Access-Control-Allow-Origin'] = result.allowOrigin;
|
||||
r.headersOut['Access-Control-Allow-Methods'] = 'POST, OPTIONS';
|
||||
r.headersOut['Access-Control-Max-Age'] = '600';
|
||||
r.headersOut['Vary'] = 'Origin';
|
||||
}
|
||||
if (!result.body) {
|
||||
r.return(result.status);
|
||||
return;
|
||||
}
|
||||
r.headersOut['Content-Type'] = 'application/json';
|
||||
r.return(result.status, JSON.stringify(result.body));
|
||||
}
|
||||
|
||||
// njs accepts only a default export; the helpers ride along for the tests.
|
||||
export default {
|
||||
handle: handle, decide: decide, makeCredential: makeCredential,
|
||||
TTL_SECONDS: TTL_SECONDS, ALLOWED_ORIGINS: ALLOWED_ORIGINS, TURN_URLS: TURN_URLS
|
||||
};
|
||||
Reference in New Issue
Block a user