aere-quantum/readiness/adrese-private.mjs

63 lines
4.3 KiB
JavaScript

// O adresa IP la care un serviciu PUBLIC al nostru nu are voie sa se conecteze in numele unui strain: loopback, retele private,
// link-local, CGNAT, adrese de documentatie si de test, multicast, rezervate, si formele IPv6 ale acelorasi (inclusiv IPv4 mapat
// si NAT64). UN singur loc, folosit de gateway (tintele webhook-urilor) si de scanerul de pregatire post-cuantica: doua copii ale
// aceleiasi liste diverg, iar cea din gateway nu stia de CGNAT (100.64/10), de IPv4 mapat in IPv6 privat si de `::`.
//
// Scris 2026-09-18, dupa ce scanerul PUBLIC, fara cheie, a fost prins (masurat pe serviciul viu) rezolvand o gazda straina la
// 127.0.0.1 si ::1 si INCERCAND conexiunea: oricine isi putea indrepta un nume spre o adresa interna si afla din raspuns daca
// portul 443 e deschis acolo si ce certificat poarta. Regula: orice nu e DOVEDIT public e privat (o adresa pe care nu o inteleg
// nu primeste conexiune).
const v4 = (ip) => { const p = ip.split('.'); if (p.length !== 4) return null; const n = p.map((x) => (/^\d{1,3}$/.test(x) ? Number(x) : NaN)); return n.every((x) => x >= 0 && x <= 255) ? n : null; };
function v4Privat([a, b, c]) {
return a === 0 || a === 10 || a === 127 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) ||
(a === 192 && b === 168) || (a === 192 && b === 0 && c === 0) || (a === 192 && b === 0 && c === 2) || (a === 192 && b === 88 && c === 99) ||
(a === 198 && (b === 18 || b === 19)) || (a === 198 && b === 51 && c === 100) || (a === 203 && b === 0 && c === 113) || a >= 224;
}
// IPv6 desfacut in 8 grupuri de 16 biti; null daca textul nu e o adresa IPv6
function v6Grupuri(ip) {
let t = ip.toLowerCase(); const zona = t.indexOf('%'); if (zona >= 0) t = t.slice(0, zona);
let coada4 = null;
const m = /^(.*:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(t);
if (m) { coada4 = v4(m[2]); if (!coada4) return null; t = m[1] + ((coada4[0] << 8) | coada4[1]).toString(16) + ':' + ((coada4[2] << 8) | coada4[3]).toString(16); }
if ((t.match(/::/g) || []).length > 1) return null;
const [st, dr] = t.includes('::') ? t.split('::') : [t, null];
const a = st ? st.split(':') : [], b = dr === null ? [] : (dr ? dr.split(':') : []);
if (dr === null ? a.length !== 8 : a.length + b.length > 7) return null;
const g = [...a, ...Array(8 - a.length - b.length).fill('0'), ...b];
if (!g.every((x) => /^[0-9a-f]{1,4}$/.test(x))) return null;
return g.map((x) => parseInt(x, 16));
}
export function ipPrivat(ip) {
const text = String(ip || '').trim();
const p4 = v4(text);
if (p4) return v4Privat(p4);
const g = v6Grupuri(text);
if (!g) return true; // nu e o adresa pe care o inteleg: nu primeste conexiune
const ultimele4 = [g[6] >> 8, g[6] & 255, g[7] >> 8, g[7] & 255];
if (g.slice(0, 5).every((x) => x === 0)) {
if (g[5] === 0xffff) return v4Privat(ultimele4); // ::ffff:a.b.c.d, IPv4 mapat: il judeca regula IPv4
if (g[5] === 0) return true; // ::, ::1 si vechiul ::a.b.c.d (retras): nimic public acolo
}
if (g[0] === 0x64 && g[1] === 0xff9b) return v4Privat(ultimele4); // NAT64 64:ff9b::/96 (si 64:ff9b:1::/48)
if (g[0] === 0x2002) return v4Privat([g[1] >> 8, g[1] & 255, g[2] >> 8, g[2] & 255]); // 6to4 poarta un IPv4 in el
if ((g[0] & 0xfe00) === 0xfc00) return true; // fc00::/7 unique local
if ((g[0] & 0xffc0) === 0xfe80) return true; // fe80::/10 link-local
if ((g[0] & 0xff00) === 0xff00) return true; // ff00::/8 multicast
if (g[0] === 0x2001 && g[1] === 0x0db8) return true; // documentatie
if (g[0] === 0x3fff && (g[1] & 0xf000) === 0) return true; // 3fff::/20, documentatie (RFC 9637); prins de proba, nu de mine
if (g[0] === 0x2001 && g[1] === 0) return true; // Teredo
if ((g[0] & 0xe000) !== 0x2000) return true; // in afara 2000::/3 (unicast global) nu e public
return false;
}
// Un `lookup` pentru tls.connect / https.request care intoarce NUMAI adresa deja verificata: intre verificare si conexiune
// numele nu mai e rezolvat a doua oara, deci un DNS care isi schimba raspunsul (rebinding) nu mai are ce sa mute.
export const lookupFixat = (adresa) => (gazda, optiuni, cb) => {
if (typeof optiuni === 'function') { cb = optiuni; optiuni = {}; }
if (optiuni && optiuni.all) cb(null, [{ address: adresa.address, family: adresa.family }]);
else cb(null, adresa.address, adresa.family);
};