From 841acf2a7aa625ff0a86bfe55fae2f9ced99c79f Mon Sep 17 00:00:00 2001 From: SITO Date: Wed, 19 Aug 2026 00:16:50 +0200 Subject: [PATCH] karvan: quitar el STUN de Google y servir la configuracion ICE del propio nodo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El cliente tenia stun.l.google.com fijo en el codigo. Un servidor STUN aprende la IP publica y el momento de cada consulta, asi que tal cual estaba cada llamada le avisaba a Google de que ese usuario esta llamando. En un proyecto con esta postura sobre privacidad no se sostiene. Por defecto ya no hay ningun servidor ICE: solo candidatos host, que funcionan en la misma red y con NAT amable. Es mas honesto degradar a nada que degradar a un tercero. Se anade turnCredentials.js, siguiendo el estilo de los helpers de src/backend/: genera credenciales efimeras con el mecanismo REST de coturn (use-auth-secret), username = caducidad:identificador y credential = HMAC-SHA1 en base64. El feed id hace de identificador, que es lo que distingue a quien llama en Oasis. Como coturn valida el HMAC, las credenciales se emiten sin hablar con el servidor y caducan solas. La ruta GET /karvan/ice las sirve solo a loopback —la credencial va firmada con el secreto del nodo y no debe salir de la maquina— y el cliente la pide ANTES de crear ninguna RTCPeerConnection, porque la configuracion ICE de una conexion ya creada no se puede cambiar de forma fiable. Con dos segundos de espera maxima y respaldo a candidatos host. La configuracion vive en oasis-config.json bajo rtc, vacia de fabrica, y admite tres formas: STUN propio, TURN con secreto compartido, o credenciales estaticas. Con relayOnly se fuerza iceTransportPolicy relay para que ningun par vea la IP del otro. Verificado: sin configuracion devuelve iceServers vacio; con secreto emite credencial reproducible y con caducidad; cero referencias a Google en el codigo. --- src/backend/fork_routes.js | 10 +++++++ src/backend/turnCredentials.js | 50 ++++++++++++++++++++++++++++++++++ src/client/public/js/karvan.js | 27 ++++++++++++++---- src/configs/oasis-config.json | 11 +++++++- 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 src/backend/turnCredentials.js diff --git a/src/backend/fork_routes.js b/src/backend/fork_routes.js index e2cd175..0f60566 100644 --- a/src/backend/fork_routes.js +++ b/src/backend/fork_routes.js @@ -14,6 +14,7 @@ const koaRouter = require("../server/node_modules/@koa/router"); const { koaBody } = require("../server/node_modules/koa-body"); const { stripDangerousTags } = require("./sanitizeHtml"); +const { buildIceServers } = require("./turnCredentials"); const karvanModel = require('../models/karvan_model')({}); // FORK_IA_UX: salas efímeras en RAM (mensajes temporales + señalización WebRTC) const { karvanView, karvanRoomView } = require("../views/karvan_view"); @@ -75,6 +76,15 @@ async function karvanInviteAuthor(roomId) { const forkRouter = new koaRouter(); forkRouter + // Configuracion ICE para las llamadas. Solo loopback: la credencial TURN va + // firmada con el secreto del nodo y no debe salir de la maquina. + .get('/karvan/ice', async (ctx) => { + if (!checkMod(ctx, 'karvanMod')) { ctx.status = 403; ctx.body = { error: 'off' }; return; } + if (!isLoopbackRequest(ctx)) { ctx.status = 403; ctx.body = { error: 'forbidden' }; return; } + let who = ''; try { who = getViewerId() || ''; } catch (e) {} + let rtc = null; try { rtc = (getConfig() || {}).rtc || null; } catch (e) {} + ctx.body = buildIceServers(rtc, who); + }) // ===== FORK_IA_UX: módulo KARVAN (mensajes temporales RAM + WebRTC data-channel) ===== .get('/karvan', async (ctx) => { if (!checkMod(ctx, 'karvanMod')) { ctx.redirect('/modules'); return; } diff --git a/src/backend/turnCredentials.js b/src/backend/turnCredentials.js new file mode 100644 index 0000000..d1b6ea1 --- /dev/null +++ b/src/backend/turnCredentials.js @@ -0,0 +1,50 @@ +"use strict"; +// Credenciales efimeras para un TURN propio (coturn con use-auth-secret). +// +// Es el mecanismo REST de coturn: el servidor no guarda usuarios, valida que +// credential == base64(HMAC-SHA1(static-auth-secret, username)) +// y que el timestamp del username no haya caducado. Asi las credenciales se +// generan sin hablar con coturn y caducan solas. +// +// Portado del turn.js de karvan-web, cambiando sessionId por el feed id: lo que +// identifica a quien llama en Oasis es su feed, no una sesion anonima. + +const crypto = require("crypto"); + +const DEFAULT_TTL = 3600; + +// username = ":" +function mintTurnCredentials({ urls, secret, ttlSec = DEFAULT_TTL }, who, now = Date.now()) { + if (!secret || !Array.isArray(urls) || !urls.length) return null; + const expiry = Math.floor(now / 1000) + ttlSec; + const username = expiry + ":" + String(who || "anon").slice(0, 64); + const credential = crypto.createHmac("sha1", secret).update(username).digest("base64"); + return { urls, username, credential, ttl: ttlSec, expiry }; +} + +// Lee la configuracion de rtc y devuelve lo que el navegador espera en iceServers. +// Formas admitidas en oasis-config.json: +// "rtc": { "stun": ["stun:mi.pub:3478"] } +// "rtc": { "turn": { "urls": [...], "secret": "...", "ttlSec": 3600 }, "relayOnly": true } +// "rtc": { "turn": { "urls": [...], "username": "...", "credential": "..." } } (estatico) +function buildIceServers(rtc, who, now = Date.now()) { + const out = []; + if (!rtc || typeof rtc !== "object") return { iceServers: out, relayOnly: false }; + + if (Array.isArray(rtc.stun)) { + for (const u of rtc.stun) if (typeof u === "string" && u) out.push({ urls: u }); + } + + const t = rtc.turn; + if (t && Array.isArray(t.urls) && t.urls.length) { + if (t.secret) { + const c = mintTurnCredentials(t, who, now); + if (c) out.push({ urls: c.urls, username: c.username, credential: c.credential }); + } else if (t.username && t.credential) { + out.push({ urls: t.urls, username: t.username, credential: t.credential }); + } + } + return { iceServers: out, relayOnly: !!rtc.relayOnly && out.some(s => s.credential) }; +} + +module.exports = { mintTurnCredentials, buildIceServers }; diff --git a/src/client/public/js/karvan.js b/src/client/public/js/karvan.js index c2c900d..04a4797 100644 --- a/src/client/public/js/karvan.js +++ b/src/client/public/js/karvan.js @@ -110,7 +110,10 @@ // --- WebRTC (data-channel, perfect negotiation) --- var RTC = window.RTCPeerConnection || window.webkitRTCPeerConnection; - var rtcConfig = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }] }; + // Sin servidores ICE por defecto: solo candidatos host (LAN y NAT amable). + // Un STUN de terceros aprende la IP publica y el momento de cada llamada, asi que + // no se pone ninguno de fabrica. Si el nodo ofrece TURN propio, /karvan/ice lo da. + var rtcConfig = { iceServers: [] }; function setLive(on) { if (!liveTxt) return; @@ -291,9 +294,23 @@ setInterval(pollMsgs, 2000); if (RTC) { setLive(false); - announce(); - setInterval(announce, 8000); - pollSignals(); - setInterval(pollSignals, 2000); + // La configuracion ICE tiene que estar resuelta antes de la primera + // RTCPeerConnection: no se puede cambiar de forma fiable una ya creada. + var iceReady = fetch("/karvan/ice", { headers: { accept: "application/json" } }) + .then(function (r) { return r.ok ? r.json() : null; }) + .then(function (d) { + if (d && Array.isArray(d.iceServers) && d.iceServers.length) { + rtcConfig = { iceServers: d.iceServers }; + if (d.relayOnly) rtcConfig.iceTransportPolicy = "relay"; + } + }) + .catch(function () {}); + var iceTimeout = new Promise(function (res) { setTimeout(res, 2000); }); + Promise.race([iceReady, iceTimeout]).then(function () { + announce(); + setInterval(announce, 8000); + pollSignals(); + setInterval(pollSignals, 2000); + }); } })(); diff --git a/src/configs/oasis-config.json b/src/configs/oasis-config.json index f5b6c5a..ad8f220 100644 --- a/src/configs/oasis-config.json +++ b/src/configs/oasis-config.json @@ -81,5 +81,14 @@ "inbox", "publish", "activity" - ] + ], + "rtc": { + "stun": [], + "turn": { + "urls": [], + "secret": "", + "ttlSec": 3600 + }, + "relayOnly": false + } } \ No newline at end of file