mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
MaxMind and CAIDA each had their own updater with its own lock, state file, publish step, boot download and timer. One engine (common/dataset-updater.js) now owns all of that, and each dataset is a row in common/datasets.js that holds only its own logic: find the newest remote version, fetch or distill it, validate, reload. - Locks: proper-lockfile, a heartbeat lock shared by every process, so a crashed holder's lock goes stale on its own; a booting server waits for a live holder and loads what it published. - State: one file per dataset, written with write-file-atomic; the old .maxmind-update-state.json / .caida-update-state.json are read once, so an upgrade doesn't re-download. - Schedule: croner at a fixed time (DATASET_UPDATE_CRON, default 04:30 local) instead of 24 h from process start, plus a catch-up run at boot for a check a downtime skipped. DATASET_AUTO_UPDATE (default on) replaces MAXMIND_AUTO_UPDATE / CAIDA_AUTO_UPDATE, which still apply when it is unset. - Reload: the engine watches every dataset's files, so a publish by another process or a hand-placed .mmdb is picked up; MaxMind's own watcher goes. MaxMind's City and ASN editions now publish together under one version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
162 lines
5.8 KiB
JavaScript
162 lines
5.8 KiB
JavaScript
// Local CAIDA AS Relationships lookup. Snapshot is downloaded
|
|
// by the dataset updater (common/datasets.js); this module parses and serves.
|
|
//
|
|
// CAIDA as-rel2 row format: `<a>|<b>|<rel>|<source>`
|
|
// rel = -1 → p2c (a is provider of b) ← providers + customers indexes
|
|
// rel = 0 → p2p (peering) ← peers index
|
|
// rel = 1 → s2s (sibling) ← skipped
|
|
//
|
|
// Source: https://publicdata.caida.org/datasets/as-relationships/serial-2/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
import logger from './logger.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
export const AS_REL_DB_DIR = path.join(__dirname, 'as-rel-db');
|
|
export const AS_REL_FILE = 'as-rel2.txt';
|
|
|
|
// customer ASN → Set<provider ASN>. Hot path of asn-connectivity.
|
|
const providersIndex = new Map();
|
|
|
|
// ASN → Set<peer ASN>, symmetric p2p. Feeds asn-connectivity's peering edges.
|
|
const peersIndex = new Map();
|
|
|
|
// provider ASN → Set<customer ASN>. Feeds asn-connectivity's neighbour lists.
|
|
const customersIndex = new Map();
|
|
|
|
// ASN → number of distinct customers it provides transit for. Ranking
|
|
// signal when an intermediate has more providers than MAX_INTERMEDIATE_BRANCH
|
|
// can show; roughly correlates with customer-cone size.
|
|
const customerCount = new Map();
|
|
|
|
// Tier 1 set, derived from the snapshot rather than hardcoded:
|
|
// (a) the AS has no providers in CAIDA's p2c topology (nobody sells
|
|
// transit to it ≈ CAIDA's clique notion), AND
|
|
// (b) it provides transit to at least TIER1_MIN_CUSTOMERS ASes — filters
|
|
// out defunct ASNs that look settlement-free only because they're
|
|
// inactive (e.g. AS1239 Sprint after the T-Mobile merger).
|
|
// 100 is the empirical sweet spot: captures Telxius/Orange/Liberty
|
|
// (customer cones 110-340) without admitting clearly defunct ASNs.
|
|
const TIER1_MIN_CUSTOMERS = 100;
|
|
const tier1Set = new Set();
|
|
|
|
let loadedFrom = null;
|
|
|
|
function findSnapshot() {
|
|
if (!fs.existsSync(AS_REL_DB_DIR)) return null;
|
|
const txts = fs.readdirSync(AS_REL_DB_DIR).filter(f => f.endsWith('.txt'));
|
|
if (txts.length === 0) return null;
|
|
const withMtime = txts.map(f => {
|
|
const full = path.join(AS_REL_DB_DIR, f);
|
|
return { full, mtimeMs: fs.statSync(full).mtimeMs };
|
|
});
|
|
withMtime.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
return withMtime[0].full;
|
|
}
|
|
|
|
function addToIndex(index, key, value) {
|
|
let set = index.get(key);
|
|
if (!set) {
|
|
set = new Set();
|
|
index.set(key, set);
|
|
}
|
|
set.add(value);
|
|
}
|
|
|
|
function parsePipeText(text) {
|
|
for (const rawLine of text.split('\n')) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) continue;
|
|
const parts = line.split('|');
|
|
if (parts.length < 3) continue;
|
|
const a = Number(parts[0]);
|
|
const b = Number(parts[1]);
|
|
if (!a || !b) continue;
|
|
if (parts[2] === '-1') {
|
|
addToIndex(providersIndex, b, a);
|
|
addToIndex(customersIndex, a, b);
|
|
customerCount.set(a, (customerCount.get(a) || 0) + 1);
|
|
} else if (parts[2] === '0') {
|
|
addToIndex(peersIndex, a, b);
|
|
addToIndex(peersIndex, b, a);
|
|
}
|
|
}
|
|
}
|
|
|
|
function rebuildTier1Set() {
|
|
tier1Set.clear();
|
|
for (const [asn, count] of customerCount) {
|
|
if (providersIndex.has(asn)) continue; // has upstream → not Tier 1
|
|
if (count >= TIER1_MIN_CUSTOMERS) tier1Set.add(asn);
|
|
}
|
|
}
|
|
|
|
function loadDatabase() {
|
|
const filePath = findSnapshot();
|
|
if (!filePath) {
|
|
logger.warn({ dir: AS_REL_DB_DIR }, '⚠️ CAIDA as-rel snapshot not found; asn-connectivity will return empty graphs until the updater downloads one');
|
|
providersIndex.clear();
|
|
peersIndex.clear();
|
|
customersIndex.clear();
|
|
customerCount.clear();
|
|
tier1Set.clear();
|
|
loadedFrom = null;
|
|
return;
|
|
}
|
|
const start = Date.now();
|
|
try {
|
|
const text = fs.readFileSync(filePath, 'utf8');
|
|
providersIndex.clear();
|
|
peersIndex.clear();
|
|
customersIndex.clear();
|
|
customerCount.clear();
|
|
parsePipeText(text);
|
|
rebuildTier1Set();
|
|
loadedFrom = path.basename(filePath);
|
|
logger.info(`📦 CAIDA as-rel loaded (${loadedFrom}) — ${providersIndex.size} customers, ${customerCount.size} providers, ${peersIndex.size} peering ASes, ${tier1Set.size} Tier 1s in ${Date.now() - start}ms`);
|
|
} catch (error) {
|
|
logger.warn({ err: error, path: filePath }, '⚠️ Failed to parse CAIDA as-rel snapshot');
|
|
}
|
|
}
|
|
|
|
loadDatabase();
|
|
|
|
/** Reload the snapshot after the dataset updater publishes a fresh file. */
|
|
export function reloadAsRelDatabase(reason = 'reload') {
|
|
logger.info(`🔄 Reloading CAIDA as-rel snapshot (${reason})`);
|
|
loadDatabase();
|
|
}
|
|
|
|
/** Providers (transit upstreams) of an ASN. Empty array when not in dataset. */
|
|
export function providersOf(asn) {
|
|
const set = providersIndex.get(Number(asn));
|
|
return set ? [...set] : [];
|
|
}
|
|
|
|
/** Settlement-free peers of an ASN. Empty array when none observed. */
|
|
export function peersOf(asn) {
|
|
const set = peersIndex.get(Number(asn));
|
|
return set ? [...set] : [];
|
|
}
|
|
|
|
/** Customers (transit downstreams) of an ASN. Empty array when it has none. */
|
|
export const customersOf = (asn) => {
|
|
const set = customersIndex.get(Number(asn));
|
|
return set ? [...set] : [];
|
|
};
|
|
|
|
/** How many distinct ASes this one provides transit for. 0 when never a provider. */
|
|
export function customerCountOf(asn) {
|
|
return customerCount.get(Number(asn)) || 0;
|
|
}
|
|
|
|
/** Whether a snapshot is loaded (false until the updater has fetched one). */
|
|
export const isAsRelLoaded = () => loadedFrom !== null;
|
|
|
|
/** Whether this ASN is in the CAIDA-derived Tier 1 set. */
|
|
export function isTier1(asn) {
|
|
return tier1Set.has(Number(asn));
|
|
}
|