RPKI ROA Doğrulayıcı (Eğitim Amaçlı)

// ── ROA Database ───────────────────────────────────────────────────────── const ROAS = [ { afi: 4, prefix: '1.1.1.0', prefixLen: 24, maxLength: 24, asn: 13335, note: 'Cloudflare 1.1.1.1' }, { afi: 4, prefix: '1.0.0.0', prefixLen: 24, maxLength: 24, asn: 13335, note: 'Cloudflare 1.0.0.1' }, { afi: 4, prefix: '8.8.8.0', prefixLen: 24, maxLength: 24, asn: 15169, note: 'Google DNS 8.8.8.8' }, { afi: 4, prefix: '8.8.4.0', prefixLen: 24, maxLength: 24, asn: 15169, note: 'Google DNS 8.8.4.4' }, { afi: 4, prefix: '9.9.9.0', prefixLen: 24, maxLength: 24, asn: 19281, note: 'Quad9 9.9.9.9' }, { afi: 4, prefix: '149.112.112.0', prefixLen: 24, maxLength: 24, asn: 19281, note: 'Quad9 149.112.112.112' }, { afi: 4, prefix: '208.67.222.0', prefixLen: 24, maxLength: 24, asn: 36692, note: 'OpenDNS 208.67.222.222' }, { afi: 4, prefix: '208.67.220.0', prefixLen: 24, maxLength: 24, asn: 36692, note: 'OpenDNS 208.67.220.220' }, { afi: 4, prefix: '74.82.42.0', prefixLen: 24, maxLength: 24, asn: 6939, note: 'Hurricane Electric DNS' }, { afi: 4, prefix: '199.43.132.0', prefixLen: 24, maxLength: 24, asn: 3130, note: 'ARIN noc.arin.net' }, ]; // ── IPv4 helpers ───────────────────────────────────────────────────────── function ipToInt(ip) { return ip.split('.').reduce((acc, oct) => (acc << 8) | parseInt(oct, 10), 0) >>> 0; } function prefixMask(len) { return len === 0 ? 0 : ((~0 << (32 - len)) >>> 0); } function roaCoversPrefix(roaNet, roaLen, roaMaxLen, testNet, testLen) { if (testLen < roaLen) return false; if (testLen > roaMaxLen) return false; const mask = prefixMask(roaLen); return (ipToInt(roaNet) & mask) === (ipToInt(testNet) & mask); } function parseCIDR(cidr) { const [net, lenStr] = cidr.trim().split('/'); if (!lenStr) return null; const len = parseInt(lenStr, 10); if (isNaN(len) || len < 0 || len > 32) return null; const parts = net.split('.'); if (parts.length !== 4) return null; return { net, len }; } function validateIPv4(prefix, prefixLen, originAsn) { const covering = ROAS.filter(r => r.afi === 4 && roaCoversPrefix(r.prefix, r.prefixLen, r.maxLength, prefix, prefixLen) ); if (covering.length === 0) return { state: 'NOTFOUND', matching: [], covering: [] }; const matching = covering.filter(r => r.asn === originAsn); return { state: matching.length > 0 ? 'VALID' : 'INVALID', matching, covering }; } // ── UI ─────────────────────────────────────────────────────────────────── function setExample(prefix, asn) { document.getElementById('prefixInput').value = prefix; document.getElementById('asnInput').value = asn; validate(); } window.setExample = setExample; function validate() { const cidrRaw = document.getElementById('prefixInput').value.trim(); const asnRaw = parseInt(document.getElementById('asnInput').value.trim(), 10); const parsed = parseCIDR(cidrRaw); if (!parsed) { alert('Invalid CIDR notation — expected format: 1.2.3.0/24'); return; } if (isNaN(asnRaw) || asnRaw < 1) { alert('Invalid ASN — must be a positive integer'); return; } const result = validateIPv4(parsed.net, parsed.len, asnRaw); const box = document.getElementById('resultBox'); const stateEl = document.getElementById('resultState'); const descEl = document.getElementById('resultDesc'); const detailEl = document.getElementById('roaDetails'); const explEl = document.getElementById('explanationDiv'); const stateKey = result.state.toLowerCase().replace('notfound', 'notfound'); box.className = 'result-box result-' + stateKey; stateEl.className = 'result-state state-' + stateKey; stateEl.textContent = result.state === 'NOTFOUND' ? 'Not Found' : result.state; const STATE_DESCS = { VALID: `A ROA exists that authorizes AS${asnRaw} to announce ${cidrRaw}.`, INVALID: `ROA(s) cover ${cidrRaw} but none authorize AS${asnRaw} as the origin.`, NOTFOUND: `No ROA in this database covers ${cidrRaw}. The prefix owner has not created a ROA (or this sample database doesn't include it).`, }; descEl.textContent = STATE_DESCS[result.state]; if (result.covering.length > 0) { let html = `

Covering ROAs (${result.covering.length})

`; for (const r of result.covering) { const isMatch = r.asn === asnRaw; html += ``; } html += '
Prefix maxLength ASN Note Match?
${r.prefix}/${r.prefixLen} /${r.maxLength} AS${r.asn} ${r.note} ${isMatch ? '✓ ASN matches' : '✗ ASN mismatch'}
'; detailEl.innerHTML = html; } else { detailEl.innerHTML = ''; } const EXPLS = { VALID: `
Algorithm (RFC 6811 §5): At least one ROA covers the prefix (prefix length ≤ ROA maxLength AND the announced prefix is within the ROA prefix network), AND that ROA's origin ASN matches the BGP UPDATE's origin AS. All conditions satisfied → Valid.
`, INVALID: `
Algorithm (RFC 6811 §5): At least one ROA covers the prefix (test passes the subnet and maxLength checks), but no covering ROA has an origin ASN equal to AS${asnRaw}. → Invalid. This is strong evidence of a BGP hijack or misconfigured ROA.
`, NOTFOUND: `
Algorithm (RFC 6811 §5): No ROA in the Validated ROA Payload (VRP) table covers this prefix (either the prefix is not within any ROA's network, or the announced length exceeds every covering ROA's maxLength). → Not Found. The prefix is treated as it was before RPKI existed — no assertion either way.
`, }; explEl.innerHTML = EXPLS[result.state]; document.getElementById('resultSection').style.display = 'block'; } window.validate = validate; // ── Populate ROA table ──────────────────────────────────────────────────── function populateRoaTable() { const tbody = document.getElementById('roaTableBody'); for (const r of ROAS) { const tr = document.createElement('tr'); tr.innerHTML = `${r.prefix}/${r.prefixLen} /${r.maxLength} AS${r.asn} ${r.note}`; tbody.appendChild(tr); } } populateRoaTable(); })();