/* ===================================================================== FUNKTIONIERT DIE VERMITTLUNG VON AUSSEN? -- 18.09.2026 Aufrufen (von einem Rechner ausserhalb des Servers): TURN_USER=… TURN_PASS=… node tools/turn-von-aussen.mjs 159.195.212.167 3478 Die Zugangsdaten kommen ueber die Umgebung, nicht als Argument: Argumente stehen in der Prozessliste, die jeder auf dem Rechner lesen kann. --------------------------------------------------------------------- WARUM DAS NICHT SCHON DURCH DIE STUN-ANTWORT BEWIESEN IST Dass der Server auf 3478 antwortet, heisst nur: Der Dienst laeuft und dieser eine Port ist offen. Ein Anruf, der wirklich vermittelt wird, laeuft aber ueber einen ZWEITEN Port, den coturn erst bei der Zuteilung vergibt (49160-49260). Ist der Bereich in der Firewall nicht offen, dann - antwortet STUN weiterhin brav, - gelingt die Zuteilung sogar, - und trotzdem kommt kein einziger Ton an. Das faellt beim Einrichten nicht auf und beim ersten echten Anruf auch nicht sofort -- es scheitern ja nur die Verbindungen, die die Vermittlung ueberhaupt brauchen. Deshalb wird hier bis zum Schluss gemessen: zuteilen lassen, sich selbst als Gegenstelle eintragen, ein Paket ueber die zugeteilte Adresse schicken und nachsehen, ob es ankommt. ===================================================================== */ import { createSocket } from "node:dgram"; import { createHash, createHmac, randomBytes } from "node:crypto"; const HOST = process.argv[2] || "159.195.212.167"; const PORT = Number(process.argv[3] || 3478); const USER = process.env.TURN_USER || ""; const PASS = process.env.TURN_PASS || ""; if (!USER || !PASS) { console.error("KANN NICHT NACHSEHEN: TURN_USER und TURN_PASS fehlen."); console.error("Sie werden vom Server aus dem Geheimnis gerechnet -- siehe"); console.error("den Aufrufblock in der Anleitung."); process.exit(3); } const KOCHTOPF = 0x2112a442; // der feste Erkennungswert aus RFC 5389 const M = { bindung: 0x0001, zuteilung: 0x0003, erlaubnis: 0x0008, }; const A = { ABBILD: 0x0001, BENUTZER: 0x0006, NACHRICHT_ECHT: 0x0008, FEHLER: 0x0009, GEGENSTELLE: 0x0012, BEREICH: 0x0014, EINMALWERT: 0x0015, WEITERLEITUNG: 0x0016, DATEN: 0x0013, TRANSPORT: 0x0019, XOR_ABBILD: 0x0020, }; let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; /* ---------- Nachrichten bauen und lesen ------------------------------- */ function attribut(art, inhalt) { const fuell = (4 - (inhalt.length % 4)) % 4; const b = Buffer.alloc(4 + inhalt.length + fuell); b.writeUInt16BE(art, 0); b.writeUInt16BE(inhalt.length, 2); // Laenge OHNE Fuellbytes -- das ist die Falle inhalt.copy(b, 4); return b; } function nachricht(art, kennung, attribute, zugang) { let koerper = Buffer.concat(attribute); const kopf = Buffer.alloc(20); kopf.writeUInt16BE(art, 0); kopf.writeUInt32BE(KOCHTOPF, 4); kennung.copy(kopf, 8); if (zugang) { /* Der Pruefwert wird ueber die Nachricht gerechnet, deren Laenge ihn schon MITZAEHLT -- sonst rechnet die Gegenseite etwas anderes aus als wir. Das ist der Punkt, an dem eigene STUN-Versuche typischerweise scheitern. */ kopf.writeUInt16BE(koerper.length + 24, 2); const schluessel = createHash("md5") .update(`${zugang.user}:${zugang.bereich}:${zugang.pass}`, "utf8").digest(); const wert = createHmac("sha1", schluessel) .update(Buffer.concat([kopf, koerper])).digest(); koerper = Buffer.concat([koerper, attribut(A.NACHRICHT_ECHT, wert)]); } kopf.writeUInt16BE(koerper.length, 2); return Buffer.concat([kopf, koerper]); } function lies(b) { if (b.length < 20) return null; const art = b.readUInt16BE(0); const attribute = {}; let p = 20; while (p + 4 <= b.length) { const a = b.readUInt16BE(p), l = b.readUInt16BE(p + 2); attribute[a] = b.subarray(p + 4, p + 4 + l); p += 4 + l + ((4 - (l % 4)) % 4); } return { art, kennung: b.subarray(8, 20), attribute }; } /** Adressen stehen verodert in der Nachricht -- sonst wuerden Geraete * unterwegs daran herumbessern, weil sie meinen, sie muessten. */ function adresse(b, kennung) { if (!b || b.length < 8) return null; const hafen = b.readUInt16BE(2) ^ (KOCHTOPF >>> 16); const schluessel = Buffer.concat([ Buffer.from([(KOCHTOPF >>> 24) & 255, (KOCHTOPF >>> 16) & 255, (KOCHTOPF >>> 8) & 255, KOCHTOPF & 255]), kennung]); const ip = []; for (let i = 0; i < b.length - 4; i++) ip.push(b[4 + i] ^ schluessel[i]); return { ip: ip.join("."), hafen }; } function schicke(s, daten, frist = 4000) { return new Promise((fertig) => { const uhr = setTimeout(() => { s.removeListener("message", zuhoerer); fertig(null); }, frist); const zuhoerer = (a) => { clearTimeout(uhr); s.removeListener("message", zuhoerer); fertig(a); }; s.on("message", zuhoerer); s.send(daten, PORT, HOST); }); } /* ---------- Der Ablauf ------------------------------------------------ */ console.log(`=== Vermittlung von aussen: ${HOST}:${PORT} ===`); console.log(""); const s = createSocket("udp4"); await new Promise((f) => s.bind(f)); /* 1. Zuteilung ohne Anmeldung -- muss abgelehnt werden, und die Ablehnung liefert Bereich und Einmalwert fuer den zweiten Versuch. */ const k1 = randomBytes(12); const a1 = lies(await schicke(s, nachricht(M.zuteilung, k1, [attribut(A.TRANSPORT, Buffer.from([17, 0, 0, 0]))]))); ok(!!a1, "der Server antwortet ueberhaupt"); if (!a1) { console.log("\nKeine Antwort -- Dienst aus oder Port 3478 zu."); process.exit(1); } const bereich = a1.attribute[A.BEREICH]?.toString("utf8") || ""; const einmal = a1.attribute[A.EINMALWERT]; ok(a1.art === 0x0113 && !!bereich && !!einmal, `ohne Anmeldung wird abgewiesen, wie es sein muss (Bereich „${bereich}")`); /* 2. Mit Anmeldung -- das ist die Zuteilung, die ein Anruf braucht. */ const zugang = { user: USER, pass: PASS, bereich }; const k2 = randomBytes(12); const a2 = lies(await schicke(s, nachricht(M.zuteilung, k2, [ attribut(A.TRANSPORT, Buffer.from([17, 0, 0, 0])), attribut(A.BENUTZER, Buffer.from(USER, "utf8")), attribut(A.BEREICH, Buffer.from(bereich, "utf8")), attribut(A.EINMALWERT, einmal), ], zugang))); if (!a2 || a2.art !== 0x0103) { const c = a2?.attribute[A.FEHLER]; const code = c ? c[2] * 100 + c[3] : "?"; ok(false, `die Zuteilung MISSLINGT (Fehler ${code}) -- so kaeme kein Anruf zustande`); console.log("\n Bei 401: Die Zugangsdaten passen nicht zum Geheimnis auf dem Server."); console.log(" Bei 486/508: Die Grenzen in turnserver.conf sind erreicht."); process.exit(1); } const weiter = adresse(a2.attribute[A.WEITERLEITUNG], k2); ok(!!weiter, `die Zuteilung gelingt -- wir bekommen ${weiter?.ip}:${weiter?.hafen}`); ok(weiter.hafen >= 49160 && weiter.hafen <= 49260, `und der Port liegt im vorgesehenen Bereich (${weiter.hafen}, erwartet 49160-49260)`); const meins = adresse(a2.attribute[A.XOR_ABBILD], k2); ok(!!meins, `von dort aus sehen wir aus wie ${meins?.ip}:${meins?.hafen}`); /* 3. Uns selbst als Gegenstelle eintragen. */ const k3 = randomBytes(12); const eigeneIp = meins.ip.split(".").map(Number); const gegen = Buffer.alloc(8); gegen[1] = 1; // IPv4 gegen.writeUInt16BE(1 ^ (KOCHTOPF >>> 16), 2); for (let i = 0; i < 4; i++) { gegen[4 + i] = eigeneIp[i] ^ [(KOCHTOPF >>> 24) & 255, (KOCHTOPF >>> 16) & 255, (KOCHTOPF >>> 8) & 255, KOCHTOPF & 255][i]; } const a3 = lies(await schicke(s, nachricht(M.erlaubnis, k3, [ attribut(A.GEGENSTELLE, gegen), attribut(A.BENUTZER, Buffer.from(USER, "utf8")), attribut(A.BEREICH, Buffer.from(bereich, "utf8")), attribut(A.EINMALWERT, einmal), ], zugang))); ok(a3?.art === 0x0108, "wir duerfen uns selbst als Gegenstelle eintragen"); /* 4. DIE EIGENTLICHE FRAGE: Kommt ein Paket ueber die zugeteilte Adresse an? Das beantwortet, ob der Portbereich offen ist -- alles davor beantwortet es NICHT. */ const peer = createSocket("udp4"); /* Irgendein freier Port -- fuer die Erlaubnis zaehlt allein die IP-Adresse, und ein fest verdrahteter Port waere genau dann belegt, wenn man ihn braucht. */ await new Promise((f) => peer.bind(0, f)); const angekommen = new Promise((fertig) => { const uhr = setTimeout(() => fertig(false), 5000); s.on("message", (b) => { const n = lies(b); if (n?.art === 0x0017 && n.attribute[A.DATEN]) { // Data Indication clearTimeout(uhr); fertig(n.attribute[A.DATEN].toString("utf8") === "hallo-vermittlung"); } }); }); peer.send(Buffer.from("hallo-vermittlung"), weiter.hafen, weiter.ip); const kam = await angekommen; ok(kam, kam ? "ein Paket ueber die zugeteilte Adresse kommt an -- die Vermittlung traegt wirklich" : `NICHTS kommt an Port ${weiter.hafen} an -- der Bereich 49160-49260/udp ist zu`); peer.close(); s.close(); console.log(""); console.log(`${geprueft} Pruefungen, ${fehler} Fehler`); console.log(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG -- Anrufe werden auch dann vermittelt," + "\n wenn keine direkte Verbindung zustande kommt."); process.exit(fehler ? 1 : 0);