karvan: quitar el STUN de Google y servir la configuracion ICE del propio nodo
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.
This commit is contained in:
parent
c3a1c841e6
commit
841acf2a7a
4 changed files with 92 additions and 6 deletions
|
|
@ -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; }
|
||||
|
|
|
|||
50
src/backend/turnCredentials.js
Normal file
50
src/backend/turnCredentials.js
Normal file
|
|
@ -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 = <caduca en unix> ":" <identificador>
|
||||
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 };
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -81,5 +81,14 @@
|
|||
"inbox",
|
||||
"publish",
|
||||
"activity"
|
||||
]
|
||||
],
|
||||
"rtc": {
|
||||
"stun": [],
|
||||
"turn": {
|
||||
"urls": [],
|
||||
"secret": "",
|
||||
"ttlSec": 3600
|
||||
},
|
||||
"relayOnly": false
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue