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 = `
';
detailEl.innerHTML = html;
} else {
detailEl.innerHTML = '';
}
const EXPLS = {
VALID: `${r.prefix}/${r.prefixLen}
/${r.maxLength}
AS${r.asn}
${r.note} `;
tbody.appendChild(tr);
}
}
populateRoaTable();
})();
Covering ROAs (${result.covering.length})
| Prefix | maxLength | ASN | Note | Match? |
|---|---|---|---|---|
| ${r.prefix}/${r.prefixLen} | /${r.maxLength} | AS${r.asn} | ${r.note} | ${isMatch ? '✓ ASN matches' : '✗ ASN mismatch'} |
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 = `