PeerCortex/src/aspa/validator.ts
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

333 lines
10 KiB
TypeScript

/**
* @module aspa/validator
* RFC 9582 Section 6 — ASPA-based AS path validation algorithm.
*
* Implements the Autonomous System Provider Authorization (ASPA) path
* validation procedure as defined in RFC 9582. ASPA enables detection
* of route leaks and unauthorized path segments by verifying that each
* AS in a BGP path has authorized its upstream provider relationship.
*
* @see https://www.rfc-editor.org/rfc/rfc9582#section-6
*
* @example
* ```typescript
* const aspaObjects = new Map<number, ASPAObject>();
* aspaObjects.set(64501, {
* customerAsn: 64501,
* providers: [{ asn: 64500, afi: ["ipv4", "ipv6"] }],
* });
*
* const result = validatePath(
* [13335, 64501, 64500, 174],
* aspaObjects,
* "upstream"
* );
* console.log(result.status); // "valid" | "invalid" | "unknown" | "unverifiable"
* ```
*/
// ── Interfaces ──────────────────────────────────────────
/**
* An ASPA object as registered in the RPKI.
*
* Maps a customer AS to its authorized upstream providers,
* optionally scoped to specific address families.
*
* @see RFC 9582 Section 3 — ASPA Profile
*/
export interface ASPAObject {
/** The customer AS that created this authorization */
readonly customerAsn: number;
/** Authorized upstream provider ASNs with address family scope */
readonly providers: ReadonlyArray<{
readonly asn: number;
readonly afi: ReadonlyArray<"ipv4" | "ipv6">;
}>;
}
/**
* Result of ASPA path validation.
*
* Contains the validation status, the analyzed path, any violations
* found, and whether a route leak was detected.
*/
export interface ASPAValidationResult {
/** Overall validation status per RFC 9582 Section 6 */
readonly status: "valid" | "invalid" | "unknown" | "unverifiable";
/** The AS path that was validated */
readonly path: ReadonlyArray<number>;
/** List of specific violations found in the path */
readonly violations: ReadonlyArray<ASPAViolation>;
/** Whether the path exhibits a route leak pattern */
readonly leakDetected: boolean;
/** The ASN responsible for the leak, if detected */
readonly leakingAsn?: number;
/** Confidence score from 0.0 to 1.0 based on ASPA coverage of the path */
readonly confidence: number;
}
/**
* A specific ASPA violation at a position in the AS path.
*
* Indicates that a hop in the path was not authorized by the
* customer's ASPA object.
*/
export interface ASPAViolation {
/** Zero-based position in the AS path where the violation occurs */
readonly position: number;
/** The ASN at this position */
readonly asn: number;
/** ASNs that are authorized providers for this ASN */
readonly expectedProviders: ReadonlyArray<number>;
/** The actual next-hop ASN in the path */
readonly actualNextHop: number;
/** Human-readable explanation of the violation */
readonly reason: string;
}
// ── Helper Functions ────────────────────────────────────
/**
* Check whether `providerAsn` is an authorized provider of `customerAsn`.
*
* @param customerAsn - The customer ASN to check
* @param providerAsn - The candidate provider ASN
* @param aspaObjects - Map of all known ASPA objects
* @param afi - Address family to check ("ipv4" or "ipv6")
* @returns "provider" if authorized, "not-provider" if explicitly not listed,
* or "no-attestation" if the customer has no ASPA object
*
* @see RFC 9582 Section 6 — Verification of Provider Authorization
*/
function checkProviderAuthorization(
customerAsn: number,
providerAsn: number,
aspaObjects: ReadonlyMap<number, ASPAObject>,
afi: "ipv4" | "ipv6" = "ipv4"
): "provider" | "not-provider" | "no-attestation" {
const aspa = aspaObjects.get(customerAsn);
if (!aspa) {
return "no-attestation";
}
const isAuthorized = aspa.providers.some(
(p) => p.asn === providerAsn && p.afi.includes(afi)
);
return isAuthorized ? "provider" : "not-provider";
}
/**
* Remove consecutive duplicate ASNs from a path (AS path prepending).
*
* BGP speakers may prepend their own ASN multiple times for traffic
* engineering. For ASPA validation, consecutive duplicates are collapsed.
*
* @param path - The raw AS path
* @returns The path with consecutive duplicates removed
*/
function deduplicatePath(path: ReadonlyArray<number>): ReadonlyArray<number> {
return path.filter((asn, index) => index === 0 || asn !== path[index - 1]);
}
// ── Core Validation Functions ───────────────────────────
/**
* Validate an AS path in the upstream direction per RFC 9582 Section 6.
*
* Walks the path from the origin AS (rightmost) toward the validating AS
* (leftmost). For each pair (customer, provider), verifies that the
* customer has authorized the provider via an ASPA object.
*
* The upstream validation procedure (RFC 9582 Section 6):
* - If the path has 0 or 1 unique ASNs, the result is "valid".
* - Walk from index N-1 (origin) toward index 0.
* - At each hop, check if path[i] authorizes path[i-1] as its provider.
* - If any hop yields "not-provider", the path is "invalid".
* - If all hops yield "provider", the path is "valid".
* - Otherwise the path is "unknown".
*
* @param path - AS path to validate (leftmost = closest to validator)
* @param aspaObjects - Map of customer ASN to ASPA object
* @returns Validation result with status, violations, and confidence
*/
export function validateUpstream(
path: ReadonlyArray<number>,
aspaObjects: ReadonlyMap<number, ASPAObject>
): ASPAValidationResult {
const dedupedPath = deduplicatePath(path);
// Trivial paths are always valid
if (dedupedPath.length <= 1) {
return {
status: "valid",
path: [...dedupedPath],
violations: [],
leakDetected: false,
confidence: 1.0,
};
}
const violations: ASPAViolation[] = [];
let hasNoAttestation = false;
let coveredHops = 0;
const totalHops = dedupedPath.length - 1;
// Walk from origin (rightmost) toward the validator (leftmost).
// path[i] is the customer; path[i-1] is the alleged provider.
for (let i = dedupedPath.length - 1; i >= 1; i--) {
const customerAsn = dedupedPath[i];
const providerAsn = dedupedPath[i - 1];
const authResult = checkProviderAuthorization(
customerAsn,
providerAsn,
aspaObjects
);
if (authResult === "provider") {
coveredHops++;
} else if (authResult === "not-provider") {
coveredHops++;
const aspa = aspaObjects.get(customerAsn);
violations.push({
position: i,
asn: customerAsn,
expectedProviders: aspa
? aspa.providers.map((p) => p.asn)
: [],
actualNextHop: providerAsn,
reason: `AS${customerAsn} has an ASPA object but does not list AS${providerAsn} as an authorized provider. ` +
`This indicates a potential route leak or unauthorized path segment.`,
});
} else {
hasNoAttestation = true;
}
}
const confidence = totalHops > 0 ? coveredHops / totalHops : 1.0;
// Determine overall status per RFC 9582 Section 6
if (violations.length > 0) {
const leakingViolation = violations[0];
return {
status: "invalid",
path: [...dedupedPath],
violations,
leakDetected: true,
leakingAsn: leakingViolation.asn,
confidence,
};
}
if (hasNoAttestation) {
return {
status: "unknown",
path: [...dedupedPath],
violations: [],
leakDetected: false,
confidence,
};
}
return {
status: "valid",
path: [...dedupedPath],
violations: [],
leakDetected: false,
confidence,
};
}
/**
* Validate an AS path in the downstream direction per RFC 9582 Section 6.
*
* Reverses the path and applies the upstream validation procedure.
* Downstream validation is used when the validating AS is receiving
* a route from a customer rather than a provider.
*
* Per RFC 9582, the downstream verification is the mirror image of upstream:
* - Reverse the path so the "origin" from the downstream perspective is leftmost.
* - Apply the same provider-authorization checks.
*
* @param path - AS path to validate (leftmost = closest to validator)
* @param aspaObjects - Map of customer ASN to ASPA object
* @returns Validation result with status, violations, and confidence
*/
export function validateDownstream(
path: ReadonlyArray<number>,
aspaObjects: ReadonlyMap<number, ASPAObject>
): ASPAValidationResult {
// Downstream: reverse the path and apply upstream logic.
const reversedPath = [...path].reverse();
const result = validateUpstream(reversedPath, aspaObjects);
// Map violations back to original path positions
const originalLength = deduplicatePath(path).length;
const remappedViolations: ReadonlyArray<ASPAViolation> = result.violations.map(
(v) => ({
...v,
position: originalLength - 1 - v.position,
})
);
return {
...result,
path: [...deduplicatePath(path)],
violations: remappedViolations,
};
}
/**
* Validate an AS path against ASPA objects.
*
* This is the main entry point for ASPA path validation. It dispatches
* to either upstream or downstream validation based on the direction
* parameter.
*
* @param path - The AS path to validate. Leftmost ASN is closest to the
* validating router; rightmost is the origin.
* @param aspaObjects - Map of customer ASN to its ASPA object
* @param direction - "upstream" when receiving from a provider,
* "downstream" when receiving from a customer
* @returns Full validation result including status, violations, leak
* detection, and confidence score
*
* @see RFC 9582 Section 6 — Procedure for Verifying the AS_PATH Attribute
*
* @example
* ```typescript
* // Upstream validation: AS174 -> AS13335 -> AS64501 (origin)
* const result = validatePath(
* [174, 13335, 64501],
* aspaObjects,
* "upstream"
* );
*
* if (result.leakDetected) {
* console.log(`Route leak by AS${result.leakingAsn}`);
* }
* ```
*/
export function validatePath(
path: ReadonlyArray<number>,
aspaObjects: ReadonlyMap<number, ASPAObject>,
direction: "upstream" | "downstream"
): ASPAValidationResult {
if (path.length === 0) {
return {
status: "unverifiable",
path: [],
violations: [],
leakDetected: false,
confidence: 0,
};
}
return direction === "upstream"
? validateUpstream(path, aspaObjects)
: validateDownstream(path, aspaObjects);
}