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:
parent
f21a8bbba6
commit
036ca861ae
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
25
server.js
25
server.js
@ -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") {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user