fix: bgp.he.net scraper + peering recommendations

bgp.he.net scraper:
- Fixed prefix regex: "Prefixes Originated (v4): 147" format
- Fixed peer regex: "BGP Peers Observed (all): 274" format
- Added prefixes_all field
- AS6830: v4=147, v6=9, peers=274 (was all unavailable)
- Prefix cross-check now works: RIPE 151 vs HE 156 = 97% agreement

Peering Recommendations:
- Now filters out already-established peering sessions
- 3 categories: New Opportunities, Already Peering, No Shared IXP
- Uses BGP neighbour data to detect existing sessions
- Shows "Already peering with all top networks" when applicable
This commit is contained in:
Rene Fichtmueller 2026-03-28 02:32:50 +13:00
parent f21a8bbba6
commit 036ca861ae
2 changed files with 52 additions and 28 deletions

View File

@ -847,7 +847,7 @@ async function doLookup() {
$('sourcesCard').classList.remove('hidden');
// Load peering recommendations
if (d.ix_presence && d.ix_presence.connections) loadPeeringRecommendations(currentAsn, d.ix_presence.connections);
if (d.ix_presence && d.ix_presence.connections) loadPeeringRecommendations(currentAsn, d.ix_presence.connections, d);
// Load ASPA and bgproutes.io data asynchronously
loadHealthReport(raw);
@ -2676,8 +2676,9 @@ function renderHealthReport(d) {
}
function loadPeeringRecommendations(asn, ixConnections) {
function loadPeeringRecommendations(asn, ixConnections, lookupData) {
if (!ixConnections || ixConnections.length === 0) return;
lookupData = lookupData || {};
$('peeringRecCard').classList.remove('hidden');
// Get the IXPs this network is on
@ -2685,6 +2686,13 @@ function loadPeeringRecommendations(asn, ixConnections) {
var myIxNames = {};
ixConnections.forEach(function(ix) { myIxNames[ix.ix_id] = ix.ix_name; });
// Get existing BGP neighbours (to filter out already-established peering)
var existingPeers = new Set();
var nb = lookupData.neighbours || {};
(nb.upstreams || []).forEach(function(n) { existingPeers.add(n.asn); });
(nb.downstreams || []).forEach(function(n) { existingPeers.add(n.asn); });
(nb.peers || []).forEach(function(n) { existingPeers.add(n.asn); });
// Top networks to check peering potential with
var topNets = [13335, 15169, 32934, 16509, 8075, 20940, 6939, 174, 1299, 2914, 3356, 3257, 714, 36459, 13414, 46489, 14618, 54113, 396982, 2906];
@ -2702,17 +2710,20 @@ function loadPeeringRecommendations(asn, ixConnections) {
}).catch(function() { return null; });
})).then(function(results) {
results = results.filter(function(r) { return r && r.asn !== parseInt(asn); });
// Sort by common IXPs descending
results.sort(function(a, b) { return b.common_ixps.length - a.common_ixps.length; });
var h = '';
var withCommon = results.filter(function(r) { return r.common_ixps.length > 0; });
// Split into 3 categories: established, potential new, no shared IXP
var established = results.filter(function(r) { return r.common_ixps.length > 0 && existingPeers.has(r.asn); });
var potential = results.filter(function(r) { return r.common_ixps.length > 0 && !existingPeers.has(r.asn); });
var without = results.filter(function(r) { return r.common_ixps.length === 0; });
if (withCommon.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u2705 Peering possible at shared IXPs (' + withCommon.length + ')</div>';
var h = '';
// NEW PEERING OPPORTUNITIES (not yet peering, shared IXPs exist)
if (potential.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--green);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\uD83D\uDE80 New Peering Opportunities (' + potential.length + ')</div>';
h += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:.5rem;margin-bottom:1rem">';
withCommon.forEach(function(r) {
potential.forEach(function(r) {
h += '<div onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;background:var(--bg);border:1px solid rgba(156,206,106,.2);border-radius:10px;padding:.65rem .85rem;transition:all .15s" onmouseenter="this.style.borderColor=\'var(--green)\'" onmouseleave="this.style.borderColor=\'rgba(156,206,106,.2)\'">';
h += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem"><span style="font-weight:700;font-size:.85rem;color:var(--green)">AS' + r.asn + '</span><span style="font-size:.65rem;color:var(--muted)">' + r.common_ixps.length + ' shared IXPs</span></div>';
h += '<div style="font-size:.75rem;color:var(--text-dim);margin-bottom:.3rem">' + escHtml(r.name) + '</div>';
@ -2726,16 +2737,32 @@ function loadPeeringRecommendations(asn, ixConnections) {
h += '</div>';
}
if (without.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--orange);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u26a0\ufe0f No shared IXP (' + without.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.4rem">';
without.forEach(function(r) {
h += '<span onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;font-size:.7rem;padding:.25rem .5rem;border-radius:6px;background:rgba(255,158,100,.08);border:1px solid rgba(255,158,100,.15);color:var(--orange)">' + escHtml(r.name) + ' (AS' + r.asn + ')</span>';
// ALREADY ESTABLISHED (peering exists + shared IXPs)
if (established.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:#7aa2f7;text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u2705 Already Peering (' + established.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:1rem">';
established.forEach(function(r) {
h += '<span onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;font-size:.7rem;padding:.25rem .5rem;border-radius:6px;background:rgba(122,162,247,.08);border:1px solid rgba(122,162,247,.15);color:#7aa2f7">' + escHtml(r.name) + ' (AS' + r.asn + ') — ' + r.common_ixps.length + ' IXPs</span>';
});
h += '</div>';
}
h += '<div style="font-size:.65rem;color:var(--dim);margin-top:.75rem;text-align:center">Compared with top 20 global networks by traffic volume</div>';
// NO SHARED IXP
if (without.length > 0) {
h += '<div style="font-size:.7rem;font-weight:700;color:var(--orange);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.5rem">\u26a0\ufe0f No Shared IXP (' + without.length + ')</div>';
h += '<div style="display:flex;flex-wrap:wrap;gap:.4rem">';
without.forEach(function(r) {
var alreadyPeer = existingPeers.has(r.asn) ? ' \u2714 peered via transit' : '';
h += '<span onclick="lookupAsn(' + r.asn + ')" style="cursor:pointer;font-size:.7rem;padding:.25rem .5rem;border-radius:6px;background:rgba(255,158,100,.08);border:1px solid rgba(255,158,100,.15);color:var(--orange)">' + escHtml(r.name) + ' (AS' + r.asn + ')' + alreadyPeer + '</span>';
});
h += '</div>';
}
if (potential.length === 0 && established.length > 0) {
h += '<div style="margin-top:.75rem;padding:.5rem;background:rgba(156,206,106,.06);border:1px solid rgba(156,206,106,.1);border-radius:8px;font-size:.75rem;color:var(--green);text-align:center">\u2705 Already peering with all top networks at shared IXPs</div>';
}
h += '<div style="font-size:.65rem;color:var(--dim);margin-top:.75rem;text-align:center">Compared with top 20 global networks — existing peering detected via BGP neighbour data</div>';
$('peeringRecContent').innerHTML = h;
});
}

View File

@ -726,8 +726,8 @@ async function fetchBgpHeNet(asn) {
const result = {};
const titleMatch = html.match(/<title>([^<]+)<\/title>/i);
if (titleMatch) result.title = titleMatch[1].trim();
const peerMatch = html.match(/Observed\s+Peers[^<]*<[^>]*>\s*(\d+)/i) || html.match(/(\d+)\s+Peers/i);
if (peerMatch) result.peer_count = parseInt(peerMatch[1]);
const peerMatch = html.match(/BGP\s+Peers\s+Observed\s*\(all\)\s*:\s*(\d[\d,]*)/i) || html.match(/Observed\s+Peers[^<]*<[^>]*>\s*(\d+)/i);
if (peerMatch) result.peer_count = parseInt(peerMatch[1].replace(/,/g, ''));
const countryMatch = html.match(/Country[^<]*<[^>]*>[^<]*<[^>]*>\s*<[^>]*>([^<]+)/i);
if (countryMatch) result.country = countryMatch[1].trim();
const lgMatch = html.match(/Looking\s+Glass[^<]*<[^>]*href="([^"]+)"/i);
@ -736,10 +736,13 @@ async function fetchBgpHeNet(asn) {
if (descMatch) result.description = descMatch[1].trim();
const irrMatch = html.match(/IRR\s+Record[^<]*<[^>]*>[^<]*<[^>]*>([^<]+)/i);
if (irrMatch) result.irr_record = irrMatch[1].trim();
const v4Match = html.match(/Prefixes\s+v4[^<]*<[^>]*>\s*(\d+)/i) || html.match(/IPv4\s+Prefixes[^<]*<[^>]*>\s*(\d+)/i);
if (v4Match) result.prefixes_v4 = parseInt(v4Match[1]);
const v6Match = html.match(/Prefixes\s+v6[^<]*<[^>]*>\s*(\d+)/i) || html.match(/IPv6\s+Prefixes[^<]*<[^>]*>\s*(\d+)/i);
if (v6Match) result.prefixes_v6 = parseInt(v6Match[1]);
// bgp.he.net format: "Prefixes Originated (v4): 147<br/>" or "Prefixes v4 ... <td>147"
const v4Match = html.match(/Prefixes\s+Originated\s*\(v4\)\s*:\s*(\d[\d,]*)/i) || html.match(/Prefixes\s+v4[^<]*<[^>]*>\s*(\d+)/i);
if (v4Match) result.prefixes_v4 = parseInt(v4Match[1].replace(/,/g, ''));
const v6Match = html.match(/Prefixes\s+Originated\s*\(v6\)\s*:\s*(\d[\d,]*)/i) || html.match(/Prefixes\s+v6[^<]*<[^>]*>\s*(\d+)/i);
if (v6Match) result.prefixes_v6 = parseInt(v6Match[1].replace(/,/g, ''));
const allMatch = html.match(/Prefixes\s+Originated\s*\(all\)\s*:\s*(\d[\d,]*)/i);
if (allMatch) result.prefixes_all = parseInt(allMatch[1].replace(/,/g, ''));
result.source_url = "https://bgp.he.net/AS" + asn;
return result;
} catch (_e) {
@ -940,14 +943,8 @@ const server = http.createServer(async (req, res) => {
return res.end();
}
let url, reqPath;
try {
url = new URL(req.url, "http://localhost");
reqPath = url.pathname;
} catch (_) {
res.writeHead(400);
return res.end("Bad Request");
}
const url = new URL(req.url, "http://localhost");
const reqPath = url.pathname;
// Serve static files
if (reqPath === "/" || reqPath === "/index.html") {