Rene Fichtmueller f2470f3e56 feat: initial release — AI-powered network intelligence platform
PeerCortex unifies PeeringDB, RIPE Stat, bgproutes.io, RIPE Atlas,
Route Views, IRR, RPKI, and CAIDA into a single AI-queryable MCP Server
for network engineers. Powered by local Ollama.

Core capabilities:
- 34 MCP tools for network intelligence
- 11 data sources unified
- ASPA validation engine (RFC 9582) with leak detection
- Peering partner discovery with AI-ranked matches
- BGP analysis and anomaly detection
- RPKI monitoring and compliance reports
- Latency/traceroute via RIPE Atlas
- Transit analysis and cost comparison
- IX traffic statistics
- AS topology mapping
- ASPA object generator and simulator
- 100% local AI — no cloud dependencies
2026-03-26 07:26:14 +13:00

336 lines
9.8 KiB
TypeScript

/**
* @module sources/caida
* CAIDA AS-Relationships dataset client.
*
* CAIDA publishes inferred AS-level relationships (provider-customer,
* peer-to-peer, sibling) derived from BGP data. This client fetches and
* parses the serial-2 relationship format.
*
* @see https://www.caida.org/catalog/datasets/as-relationships/
* @see https://api.asrank.caida.org/v2/docs
*/
import type { ASN } from "../types/common.js";
import { PeerCortexError } from "../types/common.js";
// ── Configuration ────────────────────────────────────────
const CAIDA_ASRANK_BASE_URL = "https://api.asrank.caida.org/v2";
interface CAIDAClientConfig {
/** Base URL override */
readonly baseUrl?: string;
/** Request timeout in milliseconds */
readonly timeoutMs?: number;
}
// ── Types ────────────────────────────────────────────────
/** Relationship type between two ASes */
export type ASRelationshipType =
| "provider-customer" // left provides transit to right
| "customer-provider" // left is customer of right
| "peer-to-peer" // settlement-free peering
| "sibling"; // same organization
/** A single AS-level relationship */
export interface ASRelationship {
readonly asnLeft: ASN;
readonly asnRight: ASN;
readonly relationship: ASRelationshipType;
readonly source: string;
}
/** CAIDA AS Rank entry */
export interface ASRankEntry {
readonly asn: ASN;
readonly asnName: string;
readonly rank: number;
readonly organization: string;
readonly country: string;
readonly cone: {
readonly numberAsns: number;
readonly numberPrefixes: number;
readonly numberAddresses: number;
};
readonly asnDegree: {
readonly provider: number;
readonly peer: number;
readonly customer: number;
readonly total: number;
};
}
/** Cone (customer cone) of an AS */
export interface ASCone {
readonly asn: ASN;
readonly asns: ReadonlyArray<ASN>;
readonly prefixes: ReadonlyArray<string>;
readonly totalAddresses: number;
}
// ── Client Interface ─────────────────────────────────────
/**
* CAIDA AS-Relationships and AS Rank client.
*
* @example
* ```typescript
* const caida = createCAIDAClient();
* const rank = await caida.getASRank(13335);
* console.log(`Cloudflare is ranked #${rank.rank}`);
*
* const rels = await caida.getRelationships(13335);
* const providers = rels.filter(r => r.relationship === "customer-provider");
* ```
*/
export interface CAIDAClient {
/** Get AS Rank entry for an ASN */
getASRank(asn: ASN): Promise<ASRankEntry>;
/** Get top N ASes by rank */
getTopASes(limit?: number): Promise<ReadonlyArray<ASRankEntry>>;
/** Get all known relationships for an ASN */
getRelationships(asn: ASN): Promise<ReadonlyArray<ASRelationship>>;
/** Get the customer cone for an ASN */
getCustomerCone(asn: ASN): Promise<ASCone>;
/** Check if the CAIDA API is reachable */
healthCheck(): Promise<boolean>;
}
// ── Client Factory ───────────────────────────────────────
/**
* Create a new CAIDA AS-Relationships client.
*
* Uses the CAIDA AS Rank GraphQL/REST API for relationship and ranking data.
*
* @param config - Client configuration
* @returns A configured CAIDA client instance
*/
export function createCAIDAClient(
config: CAIDAClientConfig = {}
): CAIDAClient {
const baseUrl = config.baseUrl ?? CAIDA_ASRANK_BASE_URL;
const timeoutMs = config.timeoutMs ?? 30000;
/**
* Execute a GraphQL query against the CAIDA AS Rank API.
*/
async function graphql<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`${baseUrl}/graphql`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"User-Agent": "PeerCortex/0.1.0",
},
body: JSON.stringify({ query, variables }),
signal: controller.signal,
});
if (!response.ok) {
throw new PeerCortexError(
`CAIDA API error: ${response.status} ${response.statusText}`,
"SOURCE_UNAVAILABLE",
"ripe_stat" // TODO: Add caida as DataSourceName
);
}
const body = (await response.json()) as { data: T; errors?: ReadonlyArray<{ message: string }> };
if (body.errors && body.errors.length > 0) {
throw new PeerCortexError(
`CAIDA GraphQL error: ${body.errors[0].message}`,
"SOURCE_UNAVAILABLE",
"ripe_stat"
);
}
return body.data;
} catch (error) {
if (error instanceof PeerCortexError) throw error;
throw new PeerCortexError(
`CAIDA request failed: ${error instanceof Error ? error.message : "Unknown error"}`,
"SOURCE_UNAVAILABLE",
"ripe_stat",
error instanceof Error ? error : undefined
);
} finally {
clearTimeout(timeout);
}
}
/**
* Map CAIDA relationship code to typed enum.
*/
function mapRelationship(code: number): ASRelationshipType {
switch (code) {
case -1: return "provider-customer";
case 0: return "peer-to-peer";
case 1: return "customer-provider";
case 2: return "sibling";
default: return "peer-to-peer";
}
}
return {
async getASRank(asn: ASN): Promise<ASRankEntry> {
const query = `
query GetASRank($asn: String!) {
asn(asn: $asn) {
asn
asnName
rank
organization { orgName }
country { iso }
cone { numberAsns numberPrefixes numberAddresses }
asnDegree { provider peer customer total }
}
}
`;
const result = await graphql<{
asn: {
asn: number;
asnName: string;
rank: number;
organization: { orgName: string };
country: { iso: string };
cone: { numberAsns: number; numberPrefixes: number; numberAddresses: number };
asnDegree: { provider: number; peer: number; customer: number; total: number };
};
}>(query, { asn: String(asn) });
return {
asn: result.asn.asn,
asnName: result.asn.asnName,
rank: result.asn.rank,
organization: result.asn.organization.orgName,
country: result.asn.country.iso,
cone: result.asn.cone,
asnDegree: result.asn.asnDegree,
};
},
async getTopASes(limit: number = 20): Promise<ReadonlyArray<ASRankEntry>> {
// TODO: Implement via CAIDA AS Rank API with pagination
const query = `
query GetTopASes($first: Int!) {
asns(first: $first, sort: "rank") {
edges {
node {
asn
asnName
rank
organization { orgName }
country { iso }
cone { numberAsns numberPrefixes numberAddresses }
asnDegree { provider peer customer total }
}
}
}
}
`;
const result = await graphql<{
asns: {
edges: ReadonlyArray<{
node: {
asn: number;
asnName: string;
rank: number;
organization: { orgName: string };
country: { iso: string };
cone: { numberAsns: number; numberPrefixes: number; numberAddresses: number };
asnDegree: { provider: number; peer: number; customer: number; total: number };
};
}>;
};
}>(query, { first: limit });
return result.asns.edges.map((edge) => ({
asn: edge.node.asn,
asnName: edge.node.asnName,
rank: edge.node.rank,
organization: edge.node.organization.orgName,
country: edge.node.country.iso,
cone: edge.node.cone,
asnDegree: edge.node.asnDegree,
}));
},
async getRelationships(
asn: ASN
): Promise<ReadonlyArray<ASRelationship>> {
// TODO: Implement via AS Rank API asnLinks query
// TODO: Parse provider/customer/peer relationships
const query = `
query GetRelationships($asn: String!) {
asn(asn: $asn) {
asnLinks(first: 500) {
edges {
node {
asn0 { asn }
asn1 { asn }
relationship
}
}
}
}
}
`;
const result = await graphql<{
asn: {
asnLinks: {
edges: ReadonlyArray<{
node: {
asn0: { asn: number };
asn1: { asn: number };
relationship: number;
};
}>;
};
};
}>(query, { asn: String(asn) });
return result.asn.asnLinks.edges.map((edge) => ({
asnLeft: edge.node.asn0.asn,
asnRight: edge.node.asn1.asn,
relationship: mapRelationship(edge.node.relationship),
source: "caida",
}));
},
async getCustomerCone(asn: ASN): Promise<ASCone> {
// TODO: Implement via CAIDA AS Rank cone query
// TODO: This may require downloading the cone dataset for full data
const _asn = asn;
return {
asn,
asns: [],
prefixes: [],
totalAddresses: 0,
};
},
async healthCheck(): Promise<boolean> {
try {
// Use Cloudflare (AS13335) as a known-good test
await this.getASRank(13335);
return true;
} catch {
return false;
}
},
};
}