Files
dogfather-universe/tools/turn-einrichten.mjs
T
DogFatherGitandClaude Opus 5 f30227c873 Unsere Seiten, klickbare Video-Karten -- und coturn, das nie vermittelt hat
Drei Sachen von Filipe, und eine davon war ein Fehler von mir.

1. DER VERMITTLUNGSSERVER HAT NIE EINEN ANRUF GETRAGEN.

Am 18.09. habe ich gemeldet, coturn laufe und sei "von aussen
nachgewiesen". Das war falsch. Im Protokoll: null Zuteilungen, jemals.
Beim ersten echten Versuch "check_stun_auth: Cannot find credentials"
-- in /etc/turnserver.conf fehlte `static-auth-secret`, also suchte
coturn die Zugangsdaten in seiner eigenen Datenbank und wies jeden ab.

Und mein eigenes Werkzeug meldete die ganze Zeit gruen:
"Eingetragen, Geheimnis lesbar, Server antwortet." Jedes Wort wahr --
und keins beantwortete die Frage, um die es geht. Eine STUN-Bindung
fragt "bist du da?", eine Zuteilung fragt "traegst du meinen Anruf?".

`tools/turn-sprache.mjs` (neu) versucht jetzt eine ECHTE Zuteilung und
schickt ein Paket darueber. `turn-einrichten.mjs` benutzt sie und hat
drei Ausgaenge statt zwei: traegt / traegt nicht / konnte nicht
nachsehen. Nach Filipes Reparatur auf dem Server gemessen: Zuteilung
auf Port 49199, Paket angekommen -- von Luxemburg durch Deutschland.

2. UNSERE SEITEN -- die erste Kachel, die HINAUSFUEHRT.

Website und VanVans Shop, mit dem Rabattcode DOGI10. Der Code wurde
nachgesehen, nicht abgeschrieben: partnercodes.json, 10 Prozent,
aktiv, "zum weitergeben an Community" -- und giltAufSale: false,
weshalb der Satz zu reduzierten Artikeln danebensteht. Der zweite Code
dort ist als "nur fuer Filipe persoenlich" vermerkt und steht nirgends.

Auch Name und Beschreibung des Shops stammen von der Seite selbst.
Meine erste Fassung hiess "Van's DIY Bastelbedarf" und nannte "Perlen,
Anhaenger, Werkzeug" -- ausgedacht und falsch. Er heisst mit "&" und
verkauft Haekelwerke, Plushies, Schmuck.

Der Ton der Kachel wurde GESUCHT, nicht gewaehlt: sieben geratene
Blautoene schafften den noetigen Abstand nicht, also 372 600
Kombinationen abgesucht. #087ce7, Abstand 0.310, Kontrast 4.51:1.

3. DIE GANZE VIDEO-KARTE KLICKT -- "egal wo man drauf drückt".

Und hier steckte der lehrreiche Fehler: Zuerst stand der Klick-Block
unten vor `return k`. Diese Funktion hat DREI Rueckgabepunkte, und der
erste lautet `if (!darfEintragen()) { ...; return k; }` -- also genau
fuer die Mitglieder, fuer die Filipe es wollte. Beim Team ging es, bei
der Community nicht. Jetzt haengt er dort, wo `videoWeg` entsteht.

Nicht klickbar bleiben Karten ohne Video und solche, deren Video bei
TikTok geloescht wurde -- sonst fuehrt der Klick auf eine Fehlerseite,
und wir sehen kaputt aus.

GEMESSEN: pruef-wege-nach-draussen (neu) 63 Pruefungen, 0 Fehler, auf
1400 und 412 px. Mit Gegenproben: der Kopierknopf oeffnet nichts, der
eigene Knopf wirkt genau einmal, auf einer Karte ohne Video passiert
nichts. pruef-community-sicht 10/0 auf jetzt 11 Seiten, 0 tote Wege.

Nebenbei: Der Pfeil klebte bei langen Untertiteln am Text (auf dem
Bildschirmfoto gesehen). 0.45rem Abstand.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-19 01:17:09 +02:00

281 lines
11 KiB
JavaScript

/* =====================================================================
DEN VERMITTLUNGSSERVER EINTRAGEN UND NACHMESSEN -- 18.09.2026
Aufrufen (auf dem Server, als Benutzer dogiweb):
node tools/turn-einrichten.mjs pruefen
node tools/turn-einrichten.mjs eintragen turn:159.195.212.167:3478
node tools/turn-einrichten.mjs entfernen
---------------------------------------------------------------------
WARUM EIN BEFEHL UND KEIN EINGABEFELD
Die Adresse wird einmal eingetragen und dann jahrelang nicht mehr.
Ein Menuepunkt dafuer waere eine Seite, die niemand findet, wenn er
sie braucht -- und eine, die gepflegt werden muss. Der Befehl steht
dagegen neben den anderen Einrichtungsschritten, in derselben
Anleitung.
WAS DIESER BEFEHL NICHT TUT: Er erzeugt das Geheimnis nicht. Das
gehoert root, weil es auch in /etc/turnserver.conf muss. Hier wird
nur NACHGESEHEN, ob es aus Sicht des Dienstes lesbar ist -- der
haeufigste Fehler beim Einrichten sind Dateirechte, und der faellt
sonst erst beim ersten Anruf auf, der nicht zustande kommt.
DAS GEHEIMNIS WIRD NIE AUSGEGEBEN, auch nicht gekuerzt.
===================================================================== */
import { createSocket } from "node:dgram";
import { randomBytes } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { turnGeheimnisLesen, turnZugang, TURN_GEHEIMNIS_DATEI } from "../server/workspace-turn.js";
import { zuteilungPruefen } from "./turn-sprache.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DB_PFAD = process.env.WORKSPACE_DB
|| join(__dirname, "..", "..", "workspace-daten", "workspace.db");
const [, , befehl, wert] = process.argv;
/* ---------------------------------------------------------------------
EIN ECHTER VERSUCH, KEIN BLICK IN DIE KONFIGURATION
Eine Konfigurationsdatei zu lesen beweist, dass jemand etwas
hingeschrieben hat. Ob der Dienst laeuft, ob die Firewall offen ist
und ob er von aussen antwortet, beweist nur eine Anfrage.
Ein STUN-Binding ist dafuer die kleinste moegliche: 20 Byte hin,
und zurueck kommt die eigene Adresse, wie der Server sie sieht.
--------------------------------------------------------------------- */
function stunFragen(host, port = 3478, frist = 3000) {
return new Promise((fertig) => {
const kennung = randomBytes(12);
const anfrage = Buffer.alloc(20);
anfrage.writeUInt16BE(0x0001, 0); // Binding Request
anfrage.writeUInt16BE(0x0000, 2); // keine Attribute
anfrage.writeUInt32BE(0x2112a442, 4); // der feste Erkennungswert
kennung.copy(anfrage, 8);
const s = createSocket("udp4");
const uhr = setTimeout(() => {
try { s.close(); } catch { /* schon zu */ }
fertig({ ok: false, grund: "keine Antwort innerhalb von 3 Sekunden" });
}, frist);
s.on("error", (f) => {
clearTimeout(uhr);
try { s.close(); } catch { /* schon zu */ }
fertig({ ok: false, grund: f.message });
});
s.on("message", (a) => {
clearTimeout(uhr);
try { s.close(); } catch { /* schon zu */ }
if (a.length < 20 || a.readUInt16BE(0) !== 0x0101) {
return fertig({ ok: false, grund: "Antwort ist kein STUN-Erfolg" });
}
if (!a.subarray(8, 20).equals(kennung)) {
return fertig({ ok: false, grund: "Antwort gehoert zu einer anderen Anfrage" });
}
/* XOR-MAPPED-ADDRESS heraussuchen -- die Adresse ist mit dem
Erkennungswert verodert, damit unterwegs niemand daran
herumbastelt. */
let p = 20;
while (p + 4 <= a.length) {
const art = a.readUInt16BE(p), laenge = a.readUInt16BE(p + 2);
if (art === 0x0020 && laenge >= 8) {
const hafen = a.readUInt16BE(p + 6) ^ 0x2112;
const ip = [0, 1, 2, 3]
.map((i) => a[p + 8 + i] ^ [0x21, 0x12, 0xa4, 0x42][i]).join(".");
return fertig({ ok: true, gesehenAls: `${ip}:${hafen}` });
}
p += 4 + laenge + ((4 - (laenge % 4)) % 4);
}
fertig({ ok: true, gesehenAls: "(ohne Adressangabe)" });
});
s.send(anfrage, port, host, (f) => {
if (f) {
clearTimeout(uhr);
try { s.close(); } catch { /* schon zu */ }
fertig({ ok: false, grund: f.message });
}
});
});
}
function adressenLesen(db) {
try {
const r = db.prepare("SELECT wert FROM einstellungen WHERE schluessel = 'anruf_adressen'").get();
const l = JSON.parse(r?.wert || "[]");
return Array.isArray(l) ? l : [];
} catch { return []; }
}
function adressenSchreiben(db, liste) {
db.prepare(`
INSERT INTO einstellungen (schluessel, wert, geaendert, von)
VALUES ('anruf_adressen', ?, ?, NULL)
ON CONFLICT(schluessel) DO UPDATE SET
wert = excluded.wert, geaendert = excluded.geaendert`)
.run(JSON.stringify(liste), new Date().toISOString());
}
/* --------------------------------------------------------------------- */
if (!["pruefen", "eintragen", "entfernen"].includes(befehl)) {
console.log("Aufruf: node tools/turn-einrichten.mjs pruefen"
+ "\n node tools/turn-einrichten.mjs eintragen turn:159.195.212.167:3478"
+ "\n node tools/turn-einrichten.mjs entfernen");
process.exit(2);
}
const db = new DatabaseSync(DB_PFAD);
if (befehl === "eintragen") {
if (!/^(stun|turn|turns):[^\s]+$/.test(wert || "")) {
console.error("FEHLT: eine Adresse wie turn:159.195.212.167:3478");
process.exit(2);
}
const alt = adressenLesen(db);
/* Doppelte Eintraege vermeiden -- zweimal dieselbe Adresse bedeutet,
dass jeder Browser sie zweimal probiert. */
const neu = alt.filter((e) => e.urls !== wert).concat([{ urls: wert }]);
adressenSchreiben(db, neu);
console.log(`Eingetragen. Vorher ${alt.length}, jetzt ${neu.length} Adresse(n).`);
} else if (befehl === "entfernen") {
adressenSchreiben(db, []);
console.log("Alle Adressen entfernt.");
}
/* ---------- Der Zustand, immer ---------------------------------------- */
console.log("");
console.log("=== Zustand ===");
const adressen = adressenLesen(db);
console.log(`Eingetragene Adressen: ${adressen.length}`);
for (const a of adressen) console.log(` ${a.urls}`);
const g = turnGeheimnisLesen();
console.log("");
if (g.wert) {
console.log(`Geheimnis: lesbar (${g.wert.length} Zeichen) aus ${TURN_GEHEIMNIS_DATEI}`);
} else {
console.log(`Geheimnis: NICHT verwendbar -- ${g.grund}`);
console.log(` erwartet unter ${TURN_GEHEIMNIS_DATEI}`);
if (g.grund.startsWith("unlesbar")) {
console.log(" (Dateirechte: root:dogiweb mit 640)");
}
}
/* Wirklich anfragen -- bei jeder eingetragenen Adresse. */
let erreichbar = 0, versucht = 0;
for (const a of adressen) {
const m = /^(?:stun|turns?):([^:/?]+)(?::(\d+))?/.exec(a.urls || "");
if (!m) continue;
versucht++;
const antwort = await stunFragen(m[1], Number(m[2] || 3478));
if (antwort.ok) {
erreichbar++;
console.log(`\n${a.urls}: antwortet. Von dort aus sehen wir aus wie ${antwort.gesehenAls}`);
} else {
console.log(`\n${a.urls}: KEINE ANTWORT -- ${antwort.grund}`);
console.log(" Meist eins von dreien: Dienst laeuft nicht "
+ "(systemctl status coturn), Firewall zu (ufw), oder falsche Adresse.");
}
}
db.close();
/* ---- TRAEGT DIE VERMITTLUNG AUCH? (19.09.2026) ---------------------
BIS HIERHER HAT DIESES WERKZEUG NUR GEFRAGT: "bist du da?" Und
genau daran ist es gescheitert.
Vom 18. auf den 19.09. stand hier vier Tage lang gruen:
"Eingetragen, Geheimnis lesbar, Server antwortet." Jedes Wort wahr.
Und trotzdem hatte coturn in dieser ganzen Zeit KEINE EINZIGE
Zuteilung geschafft -- in /etc/turnserver.conf fehlte
`static-auth-secret`, also suchte er die Zugangsdaten in seiner
eigenen Datenbank und wies jeden ab. Ich habe Filipe auf Basis
dieser gruenen Zeile gesagt, Anrufe aus fremden Netzen seien
nachgewiesen. Sie waren nie moeglich.
Eine STUN-Bindung beantwortet "laeuft der Dienst". Eine Zuteilung
beantwortet "traegt er einen Anruf". Das sind zwei Fragen, und nur
die zweite ist die, wegen der coturn ueberhaupt aufgesetzt wurde. */
let vermittlung = null;
if (adressen.length && g.wert) {
const erste = adressen.find((a) => /^turns?:/.test(a.urls || ""));
const m = erste && /^turns?:([^:/?]+)(?::(\d+))?/.exec(erste.urls);
if (!m) {
vermittlung = { stand: "weiss_nicht",
grund: "keine turn:-Adresse eingetragen (nur stun: vermittelt nichts)" };
} else {
/* Die Zugangsdaten rechnen wir uns selbst aus -- mit derselben
Funktion, die der Server fuer den Browser benutzt. Waeren es
andere, wuerde die Pruefung etwas messen, das im Betrieb nie
vorkommt. Die Kennung 0 sagt im coturn-Protokoll: niemand
Bestimmtes. */
const z = turnZugang(0, g.wert);
console.log("");
console.log("Zuteilung wird versucht ...");
vermittlung = await zuteilungPruefen({
host: m[1], port: Number(m[2] || 3478),
user: z.username, pass: z.credential,
});
}
if (vermittlung.stand === "traegt") {
const p = vermittlung.zugeteilt;
console.log(` Zuteilung gelingt: ${p.ip}:${p.hafen} (Bereich „${vermittlung.bereich}")`);
if (vermittlung.paket === true) {
console.log(" Ein Testpaket kommt darueber an -- die Vermittlung traegt wirklich.");
} else if (vermittlung.paket === false) {
console.log(` ABER: ${vermittlung.paketGrund}`);
}
} else if (vermittlung.stand === "traegt_nicht") {
console.log(` ZUTEILUNG ABGELEHNT (Fehler ${vermittlung.code})`);
console.log(` ${vermittlung.grund}`);
} else {
console.log(` KONNTE NICHT NACHSEHEN: ${vermittlung.grund}`);
}
}
console.log("");
if (!adressen.length) {
console.log("ERGEBNIS: Keine Adresse eingetragen -- Anrufe klappen nur im selben Netz.");
process.exit(1);
}
if (!g.wert) {
console.log("ERGEBNIS: Adresse da, aber ohne Geheimnis weist coturn jeden ab.");
process.exit(1);
}
if (versucht && erreichbar < versucht) {
console.log(`ERGEBNIS: ${erreichbar} von ${versucht} Adressen antworten.`);
process.exit(1);
}
/* DREI AUSGAENGE, NICHT ZWEI. "Konnte nicht nachsehen" darf nicht wie
"in Ordnung" aussehen -- sonst entsteht wieder eine gruene Zeile,
die nichts weiss. Rueckgabewert 2 sagt das auch einem Skript. */
if (vermittlung?.stand === "traegt_nicht") {
console.log("ERGEBNIS: Der Server antwortet, aber er VERMITTELT NICHT.");
console.log(" Anrufe aus Mobilfunk und strengen WLANs kommen nicht zustande.");
process.exit(1);
}
if (!vermittlung || vermittlung.stand === "weiss_nicht") {
console.log("ERGEBNIS: Ob die Vermittlung traegt, konnte nicht geprueft werden.");
console.log(` ${vermittlung?.grund || "kein Versuch moeglich"}`);
console.log(" Das ist KEIN Befund -- es heisst nur, dass gerade niemand nachsehen konnte.");
process.exit(2);
}
if (vermittlung.paket === false) {
console.log("ERGEBNIS: Die Zuteilung gelingt, aber kein Paket kommt durch.");
console.log(` ${vermittlung.paketGrund}`);
process.exit(1);
}
console.log("ERGEBNIS: Eingetragen, Geheimnis lesbar, Server antwortet");
console.log(" UND die Vermittlung traegt (Zuteilung geprueft, Paket angekommen).");