import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; interface IrrAuditQuery { asn?: string; } async function fetchWithRetry(url: string, retries = 1, timeout = 20000): Promise { for (let i = 0; i <= retries; i++) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { if (i === retries) throw new Error(`HTTP ${response.status}`); continue; } return await response.json(); } catch (e) { clearTimeout(timeoutId); if (i === retries) throw e; await new Promise(r => setTimeout(r, 1500)); } } } export async function irrAuditRoutes(fastify: FastifyInstance): Promise { fastify.get<{ Querystring: IrrAuditQuery }>( '/irr-audit', async (request: FastifyRequest<{ Querystring: IrrAuditQuery }>, reply: FastifyReply) => { let asnStr = request.query.asn || ''; const asn = asnStr.replace(/[^0-9]/g, ''); reply.header('Access-Control-Allow-Origin', '*'); reply.header('Cache-Control', 'public, max-age=1800'); if (!asn) { return reply.status(400).send({ error: 'asn required' }); } try { const nlnogData = await fetchWithRetry(`https://irrexplorer.nlnog.net/api/prefixes/asn/AS${asn}`); const prefixes = (nlnogData && nlnogData.directOrigin) || []; let irrRoutes: string[] = []; let irrDetails: any[] = []; let goodCount = 0; let warnCount = 0; let errorCount = 0; for (const pfx of prefixes) { const hasIrr = pfx.irrRoutes && Object.keys(pfx.irrRoutes).length > 0; const sources = hasIrr ? Object.keys(pfx.irrRoutes) : []; const cat = pfx.categoryOverall || 'unknown'; if (hasIrr) irrRoutes.push(pfx.prefix); irrDetails.push({ prefix: pfx.prefix, irr_sources: sources, rpki_status: pfx.rpkiRoutes && pfx.rpkiRoutes.length ? pfx.rpkiRoutes[0].rpkiStatus : 'not-found', category: cat, messages: (pfx.messages || []).map((m: any) => m.text) }); if (cat === 'success') goodCount++; else if (cat === 'warning') warnCount++; else errorCount++; } const actualPfx = prefixes.map((p: any) => p.prefix); const inBgpNotIrr = actualPfx.filter((p: string) => !irrRoutes.includes(p)); const score = actualPfx.length ? Math.round((irrRoutes.length / actualPfx.length) * 100) : 0; return reply.status(200).send({ asn: asn, irr_routes: irrRoutes, actual_prefixes: actualPfx, in_irr_not_bgp: [], in_bgp_not_irr: inBgpNotIrr, score: score, details: irrDetails, summary: { good: goodCount, warning: warnCount, error: errorCount, total: prefixes.length }, source: 'NLNOG IRR Explorer' }); } catch (e: any) { return reply.status(500).send({ error: e.message }); } } ); }