From 37b88f3ff2744a0a86c668d3618f009f50023881 Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Sat, 10 Oct 2026 04:01:25 -0300 Subject: [PATCH] =?UTF-8?q?refactor(server):=20reduz=20a=20complexidade=20?= =?UTF-8?q?das=20fun=C3=A7=C3=B5es=20de=20netGuard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/lib/netGuard.ts | 77 ++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/server/src/lib/netGuard.ts b/server/src/lib/netGuard.ts index c451ef5..7ba5e70 100644 --- a/server/src/lib/netGuard.ts +++ b/server/src/lib/netGuard.ts @@ -27,59 +27,70 @@ function parseIPv4(ip: string): number[] | null { : null; } +// Faixas IPv4 não públicas: [primeiro octeto, 2º octeto mín., 2º octeto máx.], +// e o terceiro octeto só importa em 192.0.0.0/24. +const BLOCKED_IPV4_RANGES: Array<[number, number, number]> = [ + [0, 0, 255], // "esta rede" + [10, 0, 255], // privada + [127, 0, 255], // loopback + [100, 64, 127], // CGNAT + [169, 254, 254], // link-local / metadados de nuvem + [172, 16, 31], // privada + [192, 168, 168], // privada + [198, 18, 19], // benchmarking +]; + function isBlockedIPv4([a, b, c]: number[]): boolean { - return ( - a === 0 || // "esta rede" - a === 10 || // privada - a === 127 || // loopback - (a === 100 && b >= 64 && b <= 127) || // CGNAT - (a === 169 && b === 254) || // link-local / metadados de nuvem - (a === 172 && b >= 16 && b <= 31) || // privada - (a === 192 && b === 0 && c === 0) || // IETF protocol assignments - (a === 192 && b === 168) || // privada - (a === 198 && (b === 18 || b === 19)) || // benchmarking - a >= 224 // multicast, reservado e broadcast - ); + if (a >= 224) return true; // multicast, reservado e broadcast + if (a === 192 && b === 0 && c === 0) return true; // IETF protocol assignments + return BLOCKED_IPV4_RANGES.some(([first, min, max]) => a === first && b >= min && b <= max); +} + +/** Troca um IPv4 embutido no fim ("::ffff:1.2.3.4") por dois grupos hexa. */ +function expandEmbeddedIPv4(text: string): string | null { + const embedded = text.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/); + if (!embedded) return text; + const v4 = parseIPv4(embedded[2]); + if (!v4) return null; + return `${embedded[1]}${((v4[0] << 8) | v4[1]).toString(16)}:${((v4[2] << 8) | v4[3]).toString(16)}`; } /** Expande um IPv6 para 8 grupos de 16 bits (ou null se for inválido). */ function parseIPv6(ip: string): number[] | null { - let text = ip.split("%")[0].toLowerCase(); // remove zone id - const embedded = text.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/); - if (embedded) { - const v4 = parseIPv4(embedded[2]); - if (!v4) return null; - text = `${embedded[1]}${((v4[0] << 8) | v4[1]).toString(16)}:${((v4[2] << 8) | v4[3]).toString(16)}`; - } + const text = expandEmbeddedIPv4(ip.split("%")[0].toLowerCase()); // remove zone id + if (text === null) return null; const halves = text.split("::"); if (halves.length > 2) return null; const head = halves[0] ? halves[0].split(":") : []; const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : []; const missing = 8 - head.length - tail.length; - if (missing < (halves.length === 2 ? 1 : 0) || (halves.length === 1 && missing !== 0)) return null; + const compressed = halves.length === 2; + if (compressed ? missing < 1 : missing !== 0) return null; const groups = [...head, ...Array(missing).fill("0"), ...tail].map((g) => parseInt(g, 16)); - return groups.length === 8 && groups.every((g) => g >= 0 && g <= 0xffff) ? groups : null; + return groups.every((g) => g >= 0 && g <= 0xffff) ? groups : null; +} + +function isReservedIPv6Prefix(first: number): boolean { + return ( + (first & 0xfe00) === 0xfc00 || // fc00::/7 (unique local) + (first & 0xffc0) === 0xfe80 || // fe80::/10 (link-local) + (first & 0xff00) === 0xff00 // multicast + ); } function isBlockedIPv6(ip: string): boolean { const g = parseIPv6(ip); if (!g) return true; // não conseguiu interpretar: recusa por segurança - const allZeroUntil = (n: number) => g.slice(0, n).every((x) => x === 0); - const v4FromTail = () => [g[6] >> 8, g[6] & 0xff, g[7] >> 8, g[7] & 0xff]; + const zeros = (from: number, to: number) => g.slice(from, to).every((x) => x === 0); + const embeddedV4 = [g[6] >> 8, g[6] & 0xff, g[7] >> 8, g[7] & 0xff]; - if (allZeroUntil(7) && g[7] <= 1) return true; // :: e ::1 - if (allZeroUntil(5) && g[5] === 0xffff) return isBlockedIPv4(v4FromTail()); // ::ffff:a.b.c.d - if (g[0] === 0x64 && g[1] === 0xff9b && g.slice(2, 6).every((x) => x === 0)) { - return isBlockedIPv4(v4FromTail()); // NAT64 64:ff9b::/96 - } - return ( - (g[0] & 0xfe00) === 0xfc00 || // fc00::/7 (unique local) - (g[0] & 0xffc0) === 0xfe80 || // fe80::/10 (link-local) - (g[0] & 0xff00) === 0xff00 // multicast - ); + if (zeros(0, 7) && g[7] <= 1) return true; // :: e ::1 + if (zeros(0, 5) && g[5] === 0xffff) return isBlockedIPv4(embeddedV4); // ::ffff:a.b.c.d + if (g[0] === 0x64 && g[1] === 0xff9b && zeros(2, 6)) return isBlockedIPv4(embeddedV4); // NAT64 + return isReservedIPv6Prefix(g[0]); } /** true se o endereço IP (v4 ou v6) pertence a uma faixa não pública. */