From 445ea88f2b600a6b5666bebba5cb469d28c7f0c9 Mon Sep 17 00:00:00 2001 From: Dogfather Date: Sun, 20 Sep 2026 23:06:52 +0200 Subject: [PATCH] `fetch` verweigert Port 5060 -- und sagt dasselbe wie ein toter Server Nach der Umstellung auf abgeleitete Nummern bekam pruef-chat-kanaele die 5060 und stuerzte ab: TypeError: fetch failed Der Server lief nachweislich ("Server laeuft auf http://127.0.0.1:5060"), die Anmeldung ueber `http.request` hatte sogar geklappt, und BASIS war sauber ("http://127.0.0.1:5060", 21 Zeichen, PORT eine Zahl). Nur die `fetch`-Aufrufe scheiterten. 5060 ist SIP und steht auf der "bad ports"-Liste der Fetch-Spezifikation. Node hat sie uebernommen: `fetch` baut dorthin keine Verbindung auf, noch bevor ein Paket fliegt. Die Ursache steht nur in `.cause` -- "bad port" statt "ECONNREFUSED". Die Meldung obendrueber ist beide Male dieselbe, und wer sie liest, sucht am Server. Gegengeprueft mit einem echten Zuhoerer auf beiden Nummern: 5060 -> fetch failed (bad port) 5062 -> fetch failed (ECONNREFUSED) <- also normal erreichbar `eigenerPort` ueberspringt die gesperrten Nummern jetzt (portNummer). Uebersprungen wird nach der REGEL, nicht nach einer Tabelle von Sonderfaellen: die n-te brauchbare Nummer ab 5000. Eindeutig bleibt es, weil die Reihenfolge feststeht. pruef-portnummern (11 statt 8) prueft es doppelt: gegen die Liste UND am echten Verhalten -- ein Zuhoerer auf 5060 muss fuer `fetch` tot sein, einer auf einer abgeleiteten Nummer erreichbar. Ohne die zweite Haelfte waere die Liste nur eine Behauptung ueber Node, und Behauptungen altern. Nebenbefund: Auch im alten, von Hand vergebenen Bereich lag eine gesperrte Nummer -- die 4190. Sie war nie vergeben, rein zufaellig. pruef-chat-kanaele: 79 Pruefungen, 0 Fehler -- wieder auf der Grundlinie, die vor der Umstellung gemessen wurde. Co-Authored-By: Claude Opus 5 --- server/helfer-port.mjs | 59 +++++++++++++++++++++++++++++++++++- server/pruef-portnummern.mjs | 52 +++++++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 4 deletions(-) diff --git a/server/helfer-port.mjs b/server/helfer-port.mjs index 0eeae650..4abf8e1e 100644 --- a/server/helfer-port.mjs +++ b/server/helfer-port.mjs @@ -134,6 +134,63 @@ const PRO_DATEI = 2; * etwa weil jemand die Funktion aus einem anderen Ordner aufruft --, * wird NICHT geraten. Eine geratene Nummer waere genau die Doppelung, * die hier abgeschafft wird. */ +/* ===================================================================== + PORTS, DIE `fetch` NICHT ANFASST (20.09.2026, teuer gelernt) + + Die Fetch-Spezifikation fuehrt eine Liste "bad ports" -- Nummern, auf + die ein Browser KEINE Verbindung aufbaut, weil sich ueber sie fremde + Dienste missbrauchen liessen (SMTP, NNTP, SIP, IRC und andere). Node + hat diese Liste uebernommen: `fetch` wirft dort `TypeError: fetch + failed` mit der Ursache `bad port`, noch bevor ein Paket fliegt. + + WIE ES AUFGEFALLEN IST: Nach der Umstellung auf abgeleitete Nummern + bekam pruef-chat-kanaele die 5060 -- und stuerzte ab, obwohl der + Server nachweislich lief ("Server laeuft auf http://127.0.0.1:5060") + und die Anmeldung ueber `http.request` sogar geklappt hatte. Nur die + `fetch`-Aufrufe scheiterten. 5060 ist SIP und steht auf der Liste. + + WARUM DAS SO SCHWER ZU FINDEN WAR: Die Meldung lautet "fetch failed" + -- dieselbe Meldung wie bei einem Server, der nicht laeuft. Wer sie + liest, sucht am Server. Der Unterschied steht erst in `.cause`: + `bad port` statt `ECONNREFUSED`. Gegengeprueft mit einem echten + Zuhoerer auf 5060: Port 5060 -> "bad port", Port 5062 -> sauberes + "ECONNREFUSED". Die Sperre ist also real und trifft genau diese + Nummern. + + NEBENBEFUND: Auch im alten, von Hand vergebenen Bereich lag eine -- + die 4190 (ManageSieve). Sie war nie vergeben, rein zufaellig. + ===================================================================== */ +const GESPERRT = new Set([ + 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, + 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, + 138, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, 526, 530, 531, + 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720, + 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, + 6669, 6679, 6697, 10080, +]); + +/** Die n-te brauchbare Nummer ab BASIS -- gesperrte werden uebersprungen. + * + * Damit bleibt die Ableitung eindeutig (die Reihenfolge steht fest), + * ohne dass eine gesperrte Nummer je herauskommt. Eine Liste von + * Ausnahmen musste dafuer niemand pflegen: Uebersprungen wird nach + * der Regel, nicht nach einer Tabelle von Sonderfaellen. + */ +export function portNummer(nte) { + let p = BASIS; + let uebrig = nte; + while (true) { + if (!GESPERRT.has(p)) { + if (uebrig === 0) return p; + uebrig -= 1; + } + p += 1; + if (p > BASIS + 5000) { + throw new Error("[Port] Kein freier Nummernbereich mehr oberhalb von " + BASIS + "."); + } + } +} + export async function eigenerPort(meta, wer = null, nr = 0) { const pfad = fileURLToPath(meta.url); const mein = basename(pfad); @@ -151,5 +208,5 @@ export async function eigenerPort(meta, wer = null, nr = 0) { if (nr < 0 || nr >= PRO_DATEI) { throw new Error(`[Port] Es gibt ${PRO_DATEI} Nummern je Datei (0 und 1), nicht ${nr}.`); } - return portMussFreiSein(BASIS + stelle * PRO_DATEI + nr, wer || mein.replace(/\.mjs$/, "")); + return portMussFreiSein(portNummer(stelle * PRO_DATEI + nr), wer || mein.replace(/\.mjs$/, "")); } diff --git a/server/pruef-portnummern.mjs b/server/pruef-portnummern.mjs index d45cb997..25fd1e50 100644 --- a/server/pruef-portnummern.mjs +++ b/server/pruef-portnummern.mjs @@ -21,7 +21,7 @@ ===================================================================== */ import { readdirSync, readFileSync } from "node:fs"; -import { eigenerPort } from "./helfer-port.mjs"; +import { eigenerPort, portNummer } from "./helfer-port.mjs"; let ok_ = 0, fehl = 0; const ok = (b, text) => { if (b) { ok_++; console.log(" ok " + text); } @@ -54,7 +54,7 @@ const belegt = new Map(); let doppelt = 0; dateien.forEach((f, i) => { for (let n = 0; n < 2; n++) { - const p = 5000 + i * 2 + n; + const p = portNummer(i * 2 + n); if (belegt.has(p)) { doppelt++; console.log(` ${p}: ${belegt.get(p)} und ${f}`); } belegt.set(p, f); } @@ -62,6 +62,52 @@ dateien.forEach((f, i) => { ok(belegt.size === dateien.length * 2 && doppelt === 0, `${belegt.size} Nummern fuer ${dateien.length} Dateien, ${doppelt} doppelt`); +console.log("\n=== 2b. Keine davon ist eine, die `fetch` verweigert ==="); +/* DIE TEUERSTE HALBE STUNDE DIESES ABENDS. Nach der Umstellung bekam + pruef-chat-kanaele die 5060 und stuerzte ab -- obwohl der Server + nachweislich lief und die Anmeldung ueber `http.request` geklappt + hatte. 5060 ist SIP und steht auf der "bad ports"-Liste der + Fetch-Spezifikation, die Node uebernommen hat: `fetch` baut dorthin + keine Verbindung auf. + + Die Meldung lautet "fetch failed" -- genau wie bei einem Server, der + nicht laeuft. Wer sie liest, sucht am Server. Deshalb steht sie + jetzt hier als eigene Pruefung: Die naechste Datei, die in so eine + Nummer rutscht, soll es HIER erfahren und nicht in einer + Fehlersuche. */ +const BOESE = [1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, + 69, 77, 79, 87, 95, 101, 102, 103, 104, 109, 110, 111, 113, 115, 117, 119, + 123, 135, 137, 138, 139, 143, 161, 179, 389, 427, 465, 512, 513, 514, 515, + 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, + 995, 1719, 1720, 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, + 6666, 6667, 6668, 6669, 6679, 6697, 10080]; +const getroffen = [...belegt.keys()].filter((x) => BOESE.includes(x)); +ok(getroffen.length === 0, + `keine der ${belegt.size} Nummern steht auf der Sperrliste (${getroffen.join(", ") || "—"})`); + +/* GEGENPROBE AM ECHTEN VERHALTEN, nicht an der Liste: Ein Zuhoerer auf + einer gesperrten Nummer muss fuer `fetch` unerreichbar sein, einer + daneben erreichbar. Ohne das waere die Liste oben nur eine + Behauptung ueber Node -- und Behauptungen altern. */ +{ + const { createServer } = await import("node:http"); + const probe = async (nummer) => { + const dienst = createServer((q, a) => { a.writeHead(200); a.end("da"); }); + await new Promise((r) => dienst.listen(nummer, "127.0.0.1", r)); + let grund = "erreichbar"; + try { await fetch(`http://127.0.0.1:${nummer}/`); } + catch (f) { grund = f?.cause?.message || f?.message || "?"; } + await new Promise((r) => dienst.close(r)); + return grund; + }; + const gesperrt = await probe(5060); + const erlaubt = await probe(portNummer(0)); + ok(/bad port/i.test(gesperrt), + `eine gesperrte Nummer ist fuer fetch tot, obwohl jemand lauscht (5060: ${gesperrt})`); + ok(erlaubt === "erreichbar", + `eine abgeleitete dagegen nicht (${portNummer(0)}: ${erlaubt})`); +} + console.log("\n=== 3. Die Ableitung stimmt wirklich (gemessen, nicht gerechnet) ==="); /* NICHT die Formel gegen sich selbst pruefen -- das waere eine Bestaetigung ohne Aussage. Gefragt wird die Funktion, die die @@ -70,7 +116,7 @@ console.log("\n=== 3. Die Ableitung stimmt wirklich (gemessen, nicht gerechnet) const ersteDatei = dateien[0]; const meine = dateien.indexOf("pruef-portnummern.mjs"); const p0 = await eigenerPort(import.meta, "pruef-portnummern (Selbsttest)", 0); -ok(p0 === 5000 + meine * 2, +ok(p0 === portNummer(meine * 2), `meine eigene Nummer ist ${p0} — Stelle ${meine} im Alphabet, erste Datei ${ersteDatei}`); const p1 = await eigenerPort(import.meta, "pruef-portnummern (Selbsttest)", 1); ok(p1 === p0 + 1, `und die zweite ist ${p1}, also genau eine daneben`);