Ein turn: ohne Zugangsdaten haette JEDEN Anruf verhindert, nicht nur die vermittelten

Gefunden am 25.09.2026 beim Bau des Podcast-Studios, das dieselbe
Rechnung benutzt, und dort im echten Browser gemessen:

  Failed to construct 'RTCPeerConnection': Both username and credential
  are required when the URL scheme is "turn" or "turns".

`new RTCPeerConnection(...)` WIRFT. Die Folge ist nicht "kein
Vermittlungsserver", sondern KEIN ANRUF -- auch nicht der, der direkt
zustande gekommen waere. Aus einem Randproblem (etwa jeder Fuenfte
braucht Vermittlung) waere ein Totalausfall fuer alle geworden,
ausgeloest davon, dass /etc/coturn-workspace.geheimnis einmal nicht
lesbar ist: Rechte verstellt, beim Neuaufbau vergessen, Datei leer.

server/workspace-turn.js laesst solche Eintraege bewusst stehen, damit
der Grund nicht verschwindet ("KEIN STILLES WEGLASSEN"). Das ist dort
richtig -- es heisst aber, dass im Browser aussortiert werden muss,
bevor die Verbindung gebaut wird. Genau das fehlte. Der Kommentar
daneben behauptete "ein turn: ohne Passwort schadet nicht"; das war
nie gemessen und ist falsch.

Zwei Aenderungen in workspace/assets/js/anruf.js:

1. Beim Uebernehmen der Adressen werden turn:/turns:-Eintraege ohne
   Benutzer UND Passwort aussortiert. STUN bleibt -- es kennt gar keine
   Anmeldung und ist der Weg, der in den meisten Faellen reicht. Der
   Grund geht nicht verloren, er steht weiter in der Konsole (jetzt mit
   dem Dateinamen, in dem man nachsehen muss).

2. `adressenFrisch()` sagt bei gesetztem Grund immer "nicht frisch".
   Sonst waere die Liste fuer immer frisch: Ohne Zugangsdaten bleibt nur
   STUN uebrig, und ein STUN-Eintrag hat keinen Ablaufzeitpunkt --
   `every()` sagt dann "alles gueltig" und es wird nie wieder gefragt.
   Waere die Datei auf dem Server repariert, telefonierte trotzdem
   niemand mehr mit Vermittlung, bis er die Seite neu laedt. (Nebenbei
   die zweite Falle derselben Zeile: `every()` auf einer LEEREN Liste
   ist `true`.)

Geprueft in server/pruef-anruf.mjs: Die Pruefung liest den Filter aus
der ausgelieferten Datei und FUEHRT IHN AUS -- vier Adressen hinein,
zwei brauchbare heraus, mit Gegenprobe, dass er ueberhaupt etwas
wegnimmt. Eine Pruefung, die nur nachsieht, ob irgendwo "filter" steht,
waere auch dann gruen, wenn der Filter das Falsche tut.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 17:02:23 +02:00
co-authored by Claude Opus 5
parent cdb6c46f8b
commit 29d339b262
2 changed files with 106 additions and 4 deletions
+54 -1
View File
@@ -16,7 +16,7 @@
Aufrufen mit: node server/pruef-anruf.mjs
===================================================================== */
import { mkdtempSync, rmSync, writeFileSync, unlinkSync } from "node:fs";
import { mkdtempSync, rmSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { request as httpAnfrage } from "node:http";
@@ -1224,6 +1224,59 @@ melde("=== 8. Zu dritt ===");
}
}
/* =====================================================================
EIN turn: OHNE ZUGANGSDATEN DARF DEN BROWSER NICHT ERREICHEN
(25.09.2026)
Am selben Tag beim Bau des Podcast-Studios im echten Browser
nachgemessen:
Failed to construct 'RTCPeerConnection': Both username and
credential are required when the URL scheme is "turn" or "turns".
`new RTCPeerConnection(...)` WIRFT. Die Folge ist nicht "kein
Vermittlungsserver", sondern KEIN ANRUF -- auch nicht der, der direkt
zustande gekommen waere. Aus einem Randproblem (etwa jeder Fuenfte
braucht Vermittlung) wuerde ein Totalausfall fuer alle, ausgeloest
davon, dass eine Datei auf dem Server unlesbar ist.
Der Server laesst solche Eintraege bewusst stehen, damit der Grund
nicht verschwindet -- das ist dort richtig. Aussortiert wird im
Browser, in `workspace/assets/js/anruf.js`.
DIESE PRUEFUNG LIEST KEINEN TEXT, SONDERN FUEHRT DEN ECHTEN AUSDRUCK
AUS. Eine Prüfung, die nur nachsieht, ob irgendwo das Wort "filter"
steht, waere auch dann gruen, wenn der Filter das Falsche tut.
===================================================================== */
{
const quelle = readFileSync(new URL("../workspace/assets/js/anruf.js", import.meta.url), "utf8");
const t = /adressen = \(d\.adressen \|\| \[\]\)(\.filter\([\s\S]*?\n {8}\}\);)/.exec(quelle);
ok(!!t, "in anruf.js wird die Adressliste ueberhaupt gefiltert");
if (t) {
/* Genau der Ausdruck aus der ausgelieferten Datei, ausgefuehrt. */
const filtern = new Function("d", "return (d.adressen || [])" + t[1] + ";");
const rein = {
adressen: [
{ urls: "stun:1.2.3.4:3478" },
{ urls: "turn:1.2.3.4:3478?transport=udp", username: "1:a", credential: "x" },
{ urls: "turn:1.2.3.4:3478?transport=tcp" },
{ urls: "turns:1.2.3.4:5349", username: "1:a" },
],
};
const raus = filtern(rein);
ok(raus.length === 2, `es bleiben genau die zwei brauchbaren uebrig (${raus.length})`);
ok(raus.some((a) => a.urls.startsWith("stun:")),
"STUN bleibt drin -- es kennt gar keine Anmeldung und reicht meistens");
ok(raus.every((a) => !/^turns?:/.test(a.urls) || (a.username && a.credential)),
"kein turn: ohne Benutzer UND Passwort geht hinaus");
/* GEGENPROBE: Wuerde der Filter alles durchlassen, waere die Zeile
oben auch gruen. Hier wird bewiesen, dass er wirklich etwas
wegnimmt. */
ok(raus.length < rein.adressen.length,
"Gegenprobe: der Filter nimmt tatsaechlich etwas weg");
}
}
/* ------------------------------------------------------------------- */
melde("");
melde(`${geprueft} Pruefungen, ${fehler} Fehler`
+52 -3
View File
@@ -1259,8 +1259,25 @@
immer kommt. */
let telefonGehtHier = true;
/* Der Grund, den der Server zum Vermittlungsserver mitschickt --
leer heisst "alles in Ordnung". Er steht hier oben, weil ihn
`adressenFrisch()` braucht. */
let turnGrund = '';
function adressenFrisch() {
if (!adressenGeholt) return false;
/* FEHLT DAS GEHEIMNIS, GILT DIE LISTE NIE ALS FRISCH.
Sonst waere sie es fuer immer: Ohne Zugangsdaten bleibt nach dem
Aussortieren nur STUN uebrig, und ein STUN-Eintrag hat keinen
Ablaufzeitpunkt -- `every()` sagt dann "alles noch gueltig" und
es wird nie wieder gefragt. Waere die Datei auf dem Server
zwischenzeitlich repariert, telefonierte trotzdem niemand mehr
mit Vermittlung, bis er die Seite neu laedt.
(Nebenbei die zweite Falle derselben Zeile: `every()` auf einer
LEEREN Liste ist `true`. Auch dagegen hilft diese Abfrage.) */
if (turnGrund) return false;
const jetzt = Math.floor(Date.now() / 1000);
return adressen.every((a) => {
const m = /^(\d{10,}):/.exec(a && a.username || '');
@@ -1277,15 +1294,47 @@
if (a.status === 404) { telefonGehtHier = false; return; }
if (a.ok) {
const d = await a.json();
adressen = d.adressen || [];
/* ═══════════════════════════════════════════════════════════
EIN turn: OHNE ZUGANGSDATEN DARF NICHT IN DIE LISTE.
Am 25.09.2026 im echten Browser nachgemessen (beim Bau des
Podcast-Studios, das dieselbe Rechnung benutzt):
Failed to construct 'RTCPeerConnection': Both username and
credential are required when the URL scheme is "turn" or
"turns".
`new RTCPeerConnection(...)` WIRFT. Die Folge ist nicht etwa
"kein Vermittlungsserver", sondern KEIN ANRUF -- auch nicht
der, der direkt zustande gekommen waere. Aus einem
Randproblem (etwa jeder fuenfte braucht Vermittlung) wuerde
ein Totalausfall fuer alle.
Der Server laesst solche Eintraege bewusst stehen, damit der
Grund nicht verschwindet (server/workspace-turn.js, Absatz
"KEIN STILLES WEGLASSEN"). Das ist dort richtig -- aber es
heisst, dass HIER aussortiert werden muss, bevor gebaut wird.
Der Grund geht nicht verloren: Er steht weiter unten in der
Konsole.
STUN bleibt selbstverstaendlich drin -- STUN kennt gar keine
Anmeldung, und es ist der Weg, der in den meisten Faellen
reicht.
═══════════════════════════════════════════════════════════ */
adressen = (d.adressen || []).filter((e) => {
if (!/^turns?:/.test(String(e && e.urls))) return true;
return !!(e.username && e.credential);
});
nurGleichesNetz = !!d.nur_gleiches_netz;
gruppeMax = d.gruppe_max || 4;
adressenGeholt = true;
/* Ein eingetragener Vermittlungsserver, der niemanden
durchlässt, ist schlimmer als keiner -- er sieht aus, als
wäre alles eingerichtet. Deshalb steht er in der Konsole. */
if (d.turn_grund) {
console.warn('[anruf] Vermittlungsserver ohne Zugangsdaten:', d.turn_grund);
turnGrund = d.turn_grund || '';
if (turnGrund) {
console.warn('[anruf] Vermittlungsserver ohne Zugangsdaten:', turnGrund,
'- Anrufe gehen nur noch direkt. Auf dem Server nachsehen: /etc/coturn-workspace.geheimnis');
}
}
} catch { /* dann eben ohne -- im selben Netz geht es trotzdem */ }