import net from 'node:net'; import { lookup as dnsLookup } from 'node:dns'; function ipv4ToInt(ip) { const parts = ip.split('.').map(n => Number(n)); if (parts.length !== 4 || parts.some(n => !Number.isInteger(n) || n < 0 || n > 255)) return null; return (((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3]) >>> 0; } function ipv4InCidr(ip, base, bits) { const n = ipv4ToInt(ip); const b = ipv4ToInt(base); if (n == null || b == null) return false; const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0; return (n & mask) === (b & mask); } function expandIpv6(ip) { let input = ip.toLowerCase(); const zone = input.indexOf('%'); if (zone !== -1) input = input.slice(0, zone); if (input === '::') return Array(8).fill(0); const [leftRaw, rightRaw] = input.split('::'); const left = leftRaw ? leftRaw.split(':').filter(Boolean) : []; const right = rightRaw ? rightRaw.split(':').filter(Boolean) : []; const parsePart = (part) => { if (part.includes('.')) { const n = ipv4ToInt(part); if (n == null) return []; return [(n >>> 16) & 0xffff, n & 0xffff]; } return [parseInt(part || '0', 16)]; }; const leftNums = left.flatMap(parsePart); const rightNums = right.flatMap(parsePart); const missing = 8 - leftNums.length - rightNums.length; if (missing < 0) return null; return [...leftNums, ...Array(missing).fill(0), ...rightNums].map(n => Number.isFinite(n) ? n : 0); } function ipv6StartsWith(ip, prefix, bits) { const a = expandIpv6(ip); const p = expandIpv6(prefix); if (!a || !p) return false; let remaining = bits; for (let i = 0; i < 8 && remaining > 0; i++) { const take = Math.min(16, remaining); const mask = (0xffff << (16 - take)) & 0xffff; if ((a[i] & mask) !== (p[i] & mask)) return false; remaining -= take; } return true; } function mappedIpv4(ip) { const m = ip.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/); if (m) return m[1]; const parts = expandIpv6(ip); if (!parts) return null; if (parts.slice(0, 5).every(n => n === 0) && parts[5] === 0xffff) { return `${parts[6] >>> 8}.${parts[6] & 255}.${parts[7] >>> 8}.${parts[7] & 255}`; } return null; } export function isPrivateIp(address) { if (!address) return false; const ip = String(address).replace(/^\[|\]$/g, '').toLowerCase(); const mapped = mappedIpv4(ip); if (mapped) return isPrivateIp(mapped); const family = net.isIP(ip); if (family === 4) { return ipv4InCidr(ip, '0.0.0.0', 8) || ipv4InCidr(ip, '10.0.0.0', 8) || ipv4InCidr(ip, '100.64.0.0', 10) || ipv4InCidr(ip, '127.0.0.0', 8) || ipv4InCidr(ip, '169.254.0.0', 16) || ipv4InCidr(ip, '172.16.0.0', 12) || ipv4InCidr(ip, '192.168.0.0', 16); } if (family === 6) { return ip === '::' || ip === '::1' || ipv6StartsWith(ip, 'fc00::', 7) || ipv6StartsWith(ip, 'fe80::', 10); } return false; } export async function resolvePublicAddresses(hostname, lookupFn = dnsLookup) { const host = String(hostname || '').replace(/^\[|\]$/g, ''); if (!host || host.toLowerCase() === 'localhost') throw new Error('ERR_PROXY_PRIVATE_HOST'); if (net.isIP(host)) { if (isPrivateIp(host)) throw new Error('ERR_PROXY_PRIVATE_IP'); return [{ address: host, family: net.isIP(host) }]; } const result = await new Promise((resolve, reject) => { lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs)); }); const addrs = Array.isArray(result) ? result : [result]; for (const a of addrs) { if (isPrivateIp(a.address)) throw new Error('ERR_PROXY_PRIVATE_IP'); } return addrs; } export async function validateHostFormat(hostname, lookupFn = dnsLookup) { const host = String(hostname || '').replace(/^\[|\]$/g, ''); if (!host) throw new Error('ERR_INVALID_HOST'); if (net.isIP(host)) { return [{ address: host, family: net.isIP(host) }]; } const result = await new Promise((resolve, reject) => { lookupFn(host, { all: true }, (err, addrs) => err ? reject(err) : resolve(addrs)); }); return Array.isArray(result) ? result : [result]; }