diff --git a/public/index.html b/public/index.html index f3fe104..21791bc 100644 --- a/public/index.html +++ b/public/index.html @@ -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 += '
\u2705 Peering possible at shared IXPs (' + withCommon.length + ')
'; + var h = ''; + + // NEW PEERING OPPORTUNITIES (not yet peering, shared IXPs exist) + if (potential.length > 0) { + h += '
\uD83D\uDE80 New Peering Opportunities (' + potential.length + ')
'; h += '
'; - withCommon.forEach(function(r) { + potential.forEach(function(r) { h += '
'; h += '
AS' + r.asn + '' + r.common_ixps.length + ' shared IXPs
'; h += '
' + escHtml(r.name) + '
'; @@ -2726,16 +2737,32 @@ function loadPeeringRecommendations(asn, ixConnections) { h += '
'; } - if (without.length > 0) { - h += '
\u26a0\ufe0f No shared IXP (' + without.length + ')
'; - h += '
'; - without.forEach(function(r) { - h += '' + escHtml(r.name) + ' (AS' + r.asn + ')'; + // ALREADY ESTABLISHED (peering exists + shared IXPs) + if (established.length > 0) { + h += '
\u2705 Already Peering (' + established.length + ')
'; + h += '
'; + established.forEach(function(r) { + h += '' + escHtml(r.name) + ' (AS' + r.asn + ') — ' + r.common_ixps.length + ' IXPs'; }); h += '
'; } - h += '
Compared with top 20 global networks by traffic volume
'; + // NO SHARED IXP + if (without.length > 0) { + h += '
\u26a0\ufe0f No Shared IXP (' + without.length + ')
'; + h += '
'; + without.forEach(function(r) { + var alreadyPeer = existingPeers.has(r.asn) ? ' \u2714 peered via transit' : ''; + h += '' + escHtml(r.name) + ' (AS' + r.asn + ')' + alreadyPeer + ''; + }); + h += '
'; + } + + if (potential.length === 0 && established.length > 0) { + h += '
\u2705 Already peering with all top networks at shared IXPs
'; + } + + h += '
Compared with top 20 global networks — existing peering detected via BGP neighbour data
'; $('peeringRecContent').innerHTML = h; }); } diff --git a/server.js b/server.js index 4bb60ca..12460e5 100644 --- a/server.js +++ b/server.js @@ -726,8 +726,8 @@ async function fetchBgpHeNet(asn) { const result = {}; const titleMatch = html.match(/([^<]+)<\/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") {