// ═══════════════════════════════════════════════════════════════════════════════ // V2 — валидация CIDR по ТЗ // // ЭТО: чистые функции. НЕ: БД, Express, сеть. // // ЗАЧЕМ: // 1. Единственное место где определены правила ТЗ (диапазоны, маски). // 2. Вызывается в crud/ перед каждым добавлением/изменением. // 3. Если ТЗ поменяется — править только здесь. // // ПРОВЕРКИ (по порядку): // 1. Пустое значение // 2. IPv6 (не поддерживается) // 3. Двойной слеш (некорректный формат) // 4. Маска /22–/32 (ТЗ: нельзя шире /22) // 5. Валидный IPv4-адрес // 6. Нормализация хостовой части (13.0.0.5/24 → 13.0.0.0/24) // 7. Пересечение с BLOCKED_RANGES (14 диапазонов из Приложения А ТЗ) // ═══════════════════════════════════════════════════════════════════════════════ const net = require('net'); // ── BLOCKED_RANGES — Приложение А ТЗ (14 диапазонов) ──────────────────────── // Это ЕДИНСТВЕННОЕ место где хранятся запрещённые диапазоны. // При изменении ТЗ — править ТОЛЬКО этот массив. const BLOCKED_RANGES = [ '10.0.0.0/8', // Private RFC1918 '172.16.0.0/12', // Private RFC1918 '192.168.0.0/16', // Private RFC1918 '100.64.0.0/10', // CGNAT RFC6598 '127.0.0.0/8', // Loopback '169.254.0.0/16', // Link-local '192.0.0.0/24', // IANA special '192.0.2.0/24', // TEST-NET-1 '198.51.100.0/24', // TEST-NET-2 '203.0.113.0/24', // TEST-NET-3 '198.18.0.0/15', // Benchmarking '224.0.0.0/4', // Multicast '240.0.0.0/4', // Reserved Class E '0.0.0.0/8', // "This" network '255.255.255.255/32',// Broadcast ]; // ── validate(input) → { cidr, wasNormalized } ─────────────────────────────── // ПОЛНАЯ проверка по ТЗ. // wasNormalized=true — если хостовая часть была обнулена (13.0.0.5/24 → 13.0.0.0/24). // Бросает Error с русским текстом — пользователь видит в UI. function validate(input) { const raw = (input || '').trim(); if (!raw) throw new Error('Пустое значение'); if (raw.includes(':')) throw new Error('IPv6 не поддерживается'); if ((raw.match(/\//g) || []).length > 1) throw new Error('Некорректный формат'); // Если маска не указана — считаем /32 (одиночный хост) let cidr = raw.includes('/') ? raw : raw + '/32'; const [addr, maskStr] = cidr.split('/'); // Маска: только цифры, одна или две if (!/^\d{1,2}$/.test(maskStr)) throw new Error('Некорректная маска'); const mask = parseInt(maskStr, 10); if (mask < 22 || mask > 32) throw new Error('Маска должна быть от /22 до /32'); if (!net.isIPv4(addr)) throw new Error('Некорректный IPv4 адрес'); // Нормализация: обнуляем хостовую часть битовой маской const ipNum = addr.split('.').reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0; const netMask = ~((1 << (32 - mask)) - 1) >>> 0; const network = (ipNum & netMask) >>> 0; const networkAddr = [ (network >>> 24) & 0xff, (network >>> 16) & 0xff, (network >>> 8) & 0xff, network & 0xff, ].join('.'); const normalized = networkAddr + '/' + mask; // Проверка против ВСЕХ запрещённых диапазонов for (const blocked of BLOCKED_RANGES) if (overlaps(normalized, blocked)) throw new Error(`Диапазон ${normalized} пересекается с запрещённым (${blocked})`); return { cidr: normalized, wasNormalized: addr !== networkAddr }; } // ── overlaps(cidr1, cidr2) → boolean ─────────────────────────────────────── // Проверяет пересечение двух CIDR-диапазонов. // Используется: validate() для BLOCKED_RANGES, db/queries для проверки дубликатов. function overlaps(cidr1, cidr2) { const a = cidrToRange(cidr1), b = cidrToRange(cidr2); return a.start <= b.end && b.start <= a.end; } // ── cidrToRange(cidr) → { start, end } ────────────────────────────────────── // CIDR → числовой диапазон (start/end — uint32 IP-адреса). function cidrToRange(cidr) { const [addr, maskStr] = cidr.split('/'); const ip = addr.split('.').reduce((acc, o) => (acc << 8) + parseInt(o, 10), 0) >>> 0; const mask = parseInt(maskStr, 10); return { start: ip, end: (ip | ((1 << (32 - mask)) - 1)) >>> 0 }; } // ── aggregateCIDRs(cidrs) → string[] ─────────────────────────────────────── // Схлопывание списка CIDR в минимальный набор (для экспорта). // Пример: 10.0.0.0/24 + 10.0.1.0/24 → 10.0.0.0/23 function aggregateCIDRs(cidrs) { const ranges = cidrs.map(c => cidrToRange(c)).sort((a, b) => a.start - b.start); const merged = []; for (const r of ranges) { const last = merged[merged.length - 1]; if (last && r.start <= last.end + 1) last.end = Math.max(last.end, r.end); else merged.push({ ...r }); } return merged.map(r => { let start = r.start, end = r.end, cidrs = []; while (start <= end) { let maxMask = 32; while (maxMask > 0 && ((start & ((1 << (32 - maxMask)) - 1)) !== 0 || (start + (1 << (32 - maxMask)) - 1) > end)) maxMask--; cidrs.push(numToIP(start) + '/' + maxMask); start += (1 << (32 - maxMask)); } return cidrs; }).flat(); } function numToIP(n) { return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.'); } module.exports = { validate, overlaps, cidrToRange, aggregateCIDRs, numToIP, BLOCKED_RANGES };