moonrouteguard

    Explainable RPKI route origin validation for MoonBit.

    rpki
    bgp
    route-origin-validation
    network-security
    Download zip
    Version
    0.1.0
    License
    MIT
    Last updated
    12 hours ago
    Downloads
    2

    #MoonRouteGuard

    MoonRouteGuard is a small, explainable route origin validation library written in MoonBit. It checks BGP route announcements against validated ROA payloads (VRPs) and keeps every covering payload as evidence for the decision.

    The library intentionally starts after cryptographic RPKI validation. It does not fetch repositories, validate certificates, or replace an RPKI relying-party implementation. Applications provide VRPs obtained from a trusted validator.

    #Run locally

    Install MoonBit and Node.js, then clone the repository:

    git clone https://github.com/Wyh123456559/MoonRouteGuard.git cd MoonRouteGuard moon test --target js moon run --target js src/cmd/routeguard -- --help moon run --target js src/cmd/routeguard -- examples/before.csv examples/after.csv examples/routes.csv

    The library package is portable; Node.js is only needed for the file-reading command-line program. The module is available on Mooncakes as Wyh123456559/moonrouteguard; use moon add Wyh123456559/moonrouteguard to add it to another MoonBit project.

    #What works

    • strict parsing of canonical IPv4 CIDR prefixes;
    • IPv6 CIDR parsing, RFC 5952 formatting, and 128-bit prefix containment;
    • VRP validation for prefix length and maxLength consistency;
    • RFC 6811 Valid, Invalid, and NotFound route states;
    • separate evidence for origin-AS and maximum-length mismatches;
    • correct handling of overlapping VRPs where any authorizing payload makes the route valid;
    • Routinator csv and quoted csvcompat VRP input with trust-anchor labels;
    • mixed IPv4/IPv6 Routinator CSV parsing through a separate dual-stack API;
    • two-column IPv4 route CSV input with line-numbered diagnostics;
    • mixed IPv4/IPv6 route CSV parsing for batch assessment;
    • deterministic Routinator csv and csvcompat output with round-trip safety;
    • recoverable, line-numbered diagnostics for malformed or unsupported rows;
    • set-based comparison of complete VRP snapshots, including trust-anchor changes, duplicate suppression, and hash-indexed membership checks;
    • order-preserving batch route validation;
    • indexed validation with at most 33 prefix-key lookups per IPv4 route;
    • IPv6 VRP validation and an index with at most 129 prefix-key lookups per route;
    • route-impact analysis across VRP snapshots with old and new evidence;
    • IPv6 snapshot comparison and indexed route-impact analysis;
    • human-readable explanations for each route transition, including every covering VRP and ASN or prefix-length mismatch;
    • dual-stack Node.js command-line assessment of three CSV files, with an optional failure exit code for newly invalid routes;
    • RFC 8416 IPv4/IPv6 SLURM policy rollout and rollback simulation in the command-line assessment;
    • deterministic JSON assessment reports with exact added/removed VRP records, trust-anchor labels, and old/new route evidence;
    • RFC 8416 IPv4/IPv6 prefix filters and locally added assertions;
    • strict RFC 8416 JSON parsing with atomic configuration rejection;
    • deterministic RFC 8416 JSON output with parse/write round-trip safety;
    • RFC 8210 version 1 control and IPv4/IPv6 prefix PDU encoding and decoding;
    • portable library code without filesystem or network dependencies.

    #Example

    let payload = @moonrouteguard.Vrp::new(
    @moonrouteguard.Ipv4Prefix::parse("203.0.113.0/24").unwrap(),
    24,
    64496U,
    ).unwrap()
    let route = @moonrouteguard.RouteAnnouncement::new(
    @moonrouteguard.Ipv4Prefix::parse("203.0.113.0/24").unwrap(),
    64496U,
    )
    let decision = @moonrouteguard.validate_route(route, [payload])
    println(decision.summary())

    Routinator output can be parsed without preprocessing:

    let csv =
    #|ASN,IP Prefix,Max Length,Trust Anchor
    #|AS64496,203.0.113.0/24,24,arin
    let parsed = @moonrouteguard.parse_vrp_csv(csv)
    if parsed.is_valid() {
    let payloads = parsed.records.map(record => record.vrp)
    let decisions = @moonrouteguard.validate_routes([route], payloads)
    println(decisions[0].summary())
    }

    Run the Node.js command-line example and checks:

    moon run --target js src/cmd/routeguard -- examples/before.csv examples/after.csv examples/routes.csv moon run --target js src/cmd/routeguard -- --json examples/before-dual.csv examples/after-dual.csv examples/routes-dual.csv moon run --target js src/cmd/routeguard -- --json --slurm examples/slurm-dual.json examples/before-dual.csv examples/after-dual.csv examples/routes-dual.csv moon run --target js src/cmd/routeguard -- --fail-on-valid-loss examples/before.csv examples/withdrawn.csv examples/routes.csv moon check --target wasm --deny-warn moon test --target wasm --deny-warn moon test --target js --deny-warn

    For a source-attributed interoperability check using observed BGP routes and Routinator VRPs, run moon build --target js followed by node scripts/verify-real-fixture.mjs. The real-source fixture records the capture time, original snapshot checksum, RIPE RIS observations, Routinator reference results, and the separate hypothetical change used for risk testing.

    The command prints the VRP snapshot difference and route validity transitions. For each changed route, it shows the covering VRPs before and after the change and whether each authorized the route or failed on origin ASN, maximum length, or both. Library users can call RouteImpact::explanation() or Ipv6RouteImpact::explanation() for the same text. To model the same local exception policy on both snapshots, add --slurm examples/slurm.json before the three CSV paths. The snapshot diff still describes the raw validator output; route impact uses the effective VRPs after SLURM. Text and JSON output include the policy's filter and assertion counts. The example policy prevents the sample route from becoming Invalid. For a dual-stack example, use --slurm examples/slurm-dual.json with the *-dual.csv files. ASN-only filters apply to both address families, while a prefix filter applies only to its own family. IPv6 filter and assertion results are reported separately as IPv6 SLURM in text and ipv6Slurm in JSON; the existing slurm JSON section continues to report IPv4 statistics. Use --before-slurm OLD.json and/or --after-slurm NEW.json to evaluate a policy rollout, replacement, or rollback. An omitted side uses the validator VRPs without local exceptions. The shared --slurm option cannot be combined with these side-specific options. Add --json before the paths for a machine-readable report with exact snapshot record changes, route transitions, and the covering VRPs behind each decision. When IPv6 rows are present, separate ipv6Snapshot and ipv6RouteImpact sections appear; the risk exit code counts both address families. The portable library also exposes write_assessment_json(diff, impact). Pass --fail-on-new-invalid before the three paths to make a newly Invalid route fail the check. The generated Node.js process uses exit code 3 for that case and 2 for file or parse errors; moon run may normalize nonzero codes to
    1. For CI that needs the exact code, build with moon build --target js and run node _build/js/debug/build/cmd/routeguard/routeguard.js with the same arguments. Pass --fail-on-valid-loss to also treat a transition from Valid to either Invalid or NotFound as a failed check. The two gates can be combined; either condition uses exit code 3. The examples/withdrawn.csv command above shows a Valid-to-NotFound regression that --fail-on-new-invalid alone would not stop. Text reports show the total number of routes losing Valid status, and each address family's JSON routeImpact section includes validLosses alongside newlyInvalid. Both gates inspect only the supplied route CSV. A successful check is not a claim that every route in the global BGP table has been assessed. The CLI reads mixed IPv4/IPv6 CSV input and requires Node.js; the library remains portable across Wasm, Wasm-GC, and JavaScript. CLI reports group IPv4 and IPv6 transitions by address family rather than preserving interleaved route-row order. Added and removed snapshot records include their trust-anchor labels. Covering VRPs inside route decisions do not yet carry source labels because the current validation decision model retains payloads but not their provenance.

    IPv6 route origin validation uses the same validity states and evidence relations. Its prefix parser accepts compressed hexadecimal addresses and requires host bits to be zero:

    let prefix = @moonrouteguard.Ipv6Prefix::parse("2001:db8::/32").unwrap()
    let payload = @moonrouteguard.Ipv6Vrp::new(prefix, 48, 64496U).unwrap()
    let route = @moonrouteguard.Ipv6RouteAnnouncement::new(
    @moonrouteguard.Ipv6Prefix::parse("2001:db8:1::/48").unwrap(),
    64496U,
    )
    let index = @moonrouteguard.Ipv6VrpIndex::new([payload])
    println(index.validate(route).summary())

    The IPv6 core is available through its own types and validation functions. parse_dual_stack_vrp_csv reads both address families from a single Routinator snapshot while preserving trust-anchor labels and line-numbered diagnostics. The older parse_vrp_csv remains IPv4-only. Mixed route lists can be read with parse_dual_stack_route_csv; IPv6 snapshots can be compared with compare_ipv6_snapshots and assessed with assess_ipv6_snapshot_impact. IPv6 text with zone identifiers or embedded dotted IPv4 is not accepted by the prefix parser.

    The accepted four-column layout follows Routinator's documented csv and csvcompat formats. The IPv4-only parse_vrp_csv reports IPv6 rows as unsupported instead of silently discarding them; use parse_dual_stack_vrp_csv for mixed input.

    Route announcements for batch validation can be loaded from a separate CSV:

    ASN,IP Prefix AS64496,203.0.113.0/24 AS64500,198.51.100.0/24

    parse_route_csv accepts prefixed or bare ASNs and reports malformed rows with line numbers. It currently supports IPv4 routes only.

    Parsed records can be written back in either Routinator layout:

    let output = @moonrouteguard.write_vrp_csv(
    parsed.records,
    @moonrouteguard.RoutinatorCsvCompat,
    ).unwrap()

    The writer preserves record order, uses LF line endings, escapes CSV fields, and rejects trust-anchor labels that cannot round trip through the parser.

    Two parsed snapshots can be compared before a validator update is deployed:

    let diff = @moonrouteguard.compare_snapshots(old.records, current.records)
    println(diff.summary()) // +12 -3 (148921 unchanged)

    The comparison runs in expected linear time and treats exact VRP records as set members. Changes to a prefix, ASN, maximum length, or trust anchor appear as one removal and one addition.

    For repeated checks, build an index once from a complete VRP snapshot:

    let vrps = current.records.map(record => record.vrp)
    let index = @moonrouteguard.VrpIndex::new(vrps)
    let decision = index.validate(route)

    Indexed validation returns the same ordered evidence as the linear API while avoiding a complete VRP scan for every route.

    Snapshot changes can be evaluated against routes before deployment:

    let impact = @moonrouteguard.assess_snapshot_impact(
    routes,
    old_payloads,
    current_payloads,
    )
    for change in impact.changed {
    println(change.summary())
    }

    The impact report includes only validity transitions and retains both complete decisions as evidence. Routes whose evidence changes without changing their validation state are counted as unchanged.

    Local SLURM policy can be applied before building a validation index:

    let filter = @moonrouteguard.SlurmPrefixFilter::by_asn(64496U) let assertion = @moonrouteguard.slurm_assertion(prefix, 64500U).unwrap() let local = @moonrouteguard.apply_slurm(payloads, [filter], [assertion]) let index = @moonrouteguard.VrpIndex::new(local.vrps)

    The same policy can be loaded from a complete SLURM document:

    let policy = @moonrouteguard.parse_slurm_json(source).unwrap() let local = policy.apply(payloads) let ipv6_local = policy.apply_ipv6(ipv6_payloads)

    Validated policies can be normalized for review or checked into configuration repositories:

    let normalized = @moonrouteguard.write_slurm_json(policy)

    The writer preserves filter and assertion order, escapes filter comments through the standard JSON encoder, emits empty BGPsec sections, and omits a redundant maxPrefixLength when it equals the asserted prefix length.

    The implementation follows RFC 8416 ordering: filters apply to validated RPKI output first, then local assertions are appended without exact duplicates. The parser rejects unknown members and unsupported configurations as a whole. Prefix-only, ASN-only, and combined prefix-and-ASN filters are supported for IPv4 and IPv6. ASN-only filters span both families. BGPsec rules are not yet supported.

    RPKI-RTR version 1 data can be exchanged as binary PDUs without coupling the library to a particular socket implementation:

    let query = @moonrouteguard.encode_rtr_pdu(
    @moonrouteguard.SerialQuery(42, 7U),
    ).unwrap()
    let response = @moonrouteguard.decode_rtr_pdus(received_bytes).unwrap()

    The codec currently supports Serial Notify, Serial Query, Reset Query, Cache Response, IPv4 Prefix, IPv6 Prefix, End of Data, and Cache Reset PDUs. It validates framing, prefix payloads, session identifiers, and RFC 8210 timing bounds while preserving the protocol's requirement to ignore reserved fields and flag bits on receipt. It handles bytes only; it does not connect to a cache or maintain an RTR session.

    #Next steps

    Planned work includes BGPsec SLURM rules, Router Key and Error Report PDUs, and an RPKI-RTR session state machine. These capabilities are not part of the current release.

    #License

    MIT.

    CsvDiagnostic

    pub(all) struct CsvDiagnostic {
    line : Int
    message : String
    } derive(Eq,
    Debug
    )

    CsvDiagnostic::equal

    CsvDiagnostic::not_equal

    fn CsvDiagnostic::not_equal(x : CsvDiagnostic, y : CsvDiagnostic) -> Bool

    DualStackRouteCsvResult

    pub(all) struct DualStackRouteCsvResult {
    ipv4_routes : Array[RouteAnnouncement]
    ipv6_routes : Array[Ipv6RouteAnnouncement]
    diagnostics : Array[CsvDiagnostic]
    }

    IPv4 and IPv6 route announcements from one two-column CSV.

    DualStackRouteCsvResult::is_valid

    DualStackVrpCsvResult

    pub(all) struct DualStackVrpCsvResult {
    ipv4_records : Array[VrpRecord]
    ipv6_records : Array[Ipv6VrpRecord]
    diagnostics : Array[CsvDiagnostic]
    }

    The two address families from one Routinator CSV snapshot.

    DualStackVrpCsvResult::is_valid

    fn DualStackVrpCsvResult::is_valid(self : DualStackVrpCsvResult) -> Bool

    Ipv4Prefix

    pub struct Ipv4Prefix {
    address : UInt
    length : Int
    } derive(Eq,
    Debug
    )

    A canonical IPv4 network prefix.

    Ipv4Prefix::covers

    fn Ipv4Prefix::covers(self : Ipv4Prefix, candidate : Ipv4Prefix) -> Bool

    Ipv4Prefix::equal

    fn Ipv4Prefix::equal(Ipv4Prefix, Ipv4Prefix) -> Bool

    Ipv4Prefix::length

    fn Ipv4Prefix::length(self : Ipv4Prefix) -> Int

    Ipv4Prefix::not_equal

    fn Ipv4Prefix::not_equal(x : Ipv4Prefix, y : Ipv4Prefix) -> Bool

    Ipv4Prefix::parse

    fn Ipv4Prefix::parse(text : StringView) -> Result[Ipv4Prefix, String]

    Parse a canonical IPv4 CIDR prefix such as 203.0.113.0/24. Decimal octets and prefix lengths must not have signs or leading zeroes.

    Ipv4Prefix::to_string

    fn Ipv4Prefix::to_string(self : Ipv4Prefix) -> String

    Ipv6Prefix

    pub struct Ipv6Prefix {
    first : UInt
    second : UInt
    third : UInt
    fourth : UInt
    length : Int
    } derive(Eq, Hash,
    Debug
    )

    A canonical IPv6 network prefix stored as four network-order words.

    Ipv6Prefix::covers

    fn Ipv6Prefix::covers(self : Ipv6Prefix, candidate : Ipv6Prefix) -> Bool

    Ipv6Prefix::equal

    fn Ipv6Prefix::equal(Ipv6Prefix, Ipv6Prefix) -> Bool

    Ipv6Prefix::hash

    fn Ipv6Prefix::hash(self : Ipv6Prefix) -> Int

    Ipv6Prefix::hash_combine

    fn Ipv6Prefix::hash_combine(Ipv6Prefix, Hasher) -> Unit

    Ipv6Prefix::length

    fn Ipv6Prefix::length(self : Ipv6Prefix) -> Int

    Ipv6Prefix::not_equal

    fn Ipv6Prefix::not_equal(x : Ipv6Prefix, y : Ipv6Prefix) -> Bool

    Ipv6Prefix::parse

    fn Ipv6Prefix::parse(text : StringView) -> Result[Ipv6Prefix, String]

    Parse an IPv6 CIDR prefix, accepting compressed hexadecimal notation. Host bits must be zero; zone identifiers and embedded IPv4 are not accepted. The decimal prefix length must not have a sign or leading zeroes.

    Ipv6Prefix::to_string

    fn Ipv6Prefix::to_string(self : Ipv6Prefix) -> String

    Format the prefix using RFC 5952's longest zero-run compression.

    Ipv6RouteAnnouncement

    pub struct Ipv6RouteAnnouncement {
    prefix : Ipv6Prefix
    origin_asn : UInt
    } derive(Eq,
    Debug
    )

    Ipv6RouteAnnouncement::equal

    Ipv6RouteAnnouncement::new

    fn Ipv6RouteAnnouncement::new(prefix : Ipv6Prefix, origin_asn : UInt) -> Ipv6RouteAnnouncement

    Ipv6RouteAnnouncement::not_equal

    Ipv6RouteAnnouncement::origin_asn

    fn Ipv6RouteAnnouncement::origin_asn(self : Ipv6RouteAnnouncement) -> UInt

    Ipv6RouteAnnouncement::prefix

    Ipv6RouteImpact

    pub(all) struct Ipv6RouteImpact {
    previous : Ipv6ValidationDecision
    current : Ipv6ValidationDecision
    }

    An IPv6 route whose origin-validation state changes between snapshots.

    Ipv6RouteImpact::explanation

    fn Ipv6RouteImpact::explanation(self : Ipv6RouteImpact) -> String

    Ipv6RouteImpact::summary

    fn Ipv6RouteImpact::summary(self : Ipv6RouteImpact) -> String

    Ipv6SlurmApplyResult

    pub(all) struct Ipv6SlurmApplyResult {
    vrps : Array[Ipv6Vrp]
    filtered : Int
    duplicate_assertions : Int
    } derive(Eq,
    Debug
    )

    Ipv6SlurmApplyResult::equal

    Ipv6SlurmApplyResult::not_equal

    Ipv6SlurmApplyResult::summary

    fn Ipv6SlurmApplyResult::summary(self : Ipv6SlurmApplyResult) -> String

    Ipv6SlurmVrpKey

    type Ipv6SlurmVrpKey derive(Eq, Hash)

    Ipv6SlurmVrpKey::equal

    Ipv6SlurmVrpKey::hash

    fn Ipv6SlurmVrpKey::hash(self : Ipv6SlurmVrpKey) -> Int

    Ipv6SlurmVrpKey::hash_combine

    fn Ipv6SlurmVrpKey::hash_combine(Ipv6SlurmVrpKey, Hasher) -> Unit

    Ipv6SlurmVrpKey::not_equal

    fn Ipv6SlurmVrpKey::not_equal(x : Ipv6SlurmVrpKey, y : Ipv6SlurmVrpKey) -> Bool

    Ipv6SnapshotDiff

    pub(all) struct Ipv6SnapshotDiff {
    added : Array[Ipv6VrpRecord]
    removed : Array[Ipv6VrpRecord]
    unchanged : Int
    }

    Exact IPv6 VRP records added and removed between two snapshots.

    Ipv6SnapshotDiff::is_empty

    fn Ipv6SnapshotDiff::is_empty(self : Ipv6SnapshotDiff) -> Bool

    Ipv6SnapshotDiff::summary

    fn Ipv6SnapshotDiff::summary(self : Ipv6SnapshotDiff) -> String

    Ipv6SnapshotImpact

    pub(all) struct Ipv6SnapshotImpact {
    changed : Array[Ipv6RouteImpact]
    unchanged : Int
    }

    IPv6 route-state changes caused by replacing one VRP snapshot.

    Ipv6SnapshotImpact::is_empty

    fn Ipv6SnapshotImpact::is_empty(self : Ipv6SnapshotImpact) -> Bool

    Ipv6SnapshotImpact::summary

    fn Ipv6SnapshotImpact::summary(self : Ipv6SnapshotImpact) -> String

    Ipv6ValidationDecision

    pub(all) struct Ipv6ValidationDecision {
    route : Ipv6RouteAnnouncement
    validity : RouteValidity
    matches : Array[Ipv6VrpMatch]
    } derive(Eq,
    Debug
    )

    Ipv6ValidationDecision::equal

    Ipv6ValidationDecision::not_equal

    Ipv6ValidationDecision::summary

    fn Ipv6ValidationDecision::summary(self : Ipv6ValidationDecision) -> String

    Ipv6Vrp

    pub struct Ipv6Vrp {
    prefix : Ipv6Prefix
    max_length : Int
    asn : UInt
    } derive(Eq,
    Debug
    )

    An IPv6 validated ROA payload supplied by an external RPKI validator.

    Ipv6Vrp::asn

    fn Ipv6Vrp::asn(self : Ipv6Vrp) -> UInt

    Ipv6Vrp::equal

    fn Ipv6Vrp::equal(Ipv6Vrp, Ipv6Vrp) -> Bool

    Ipv6Vrp::max_length

    fn Ipv6Vrp::max_length(self : Ipv6Vrp) -> Int

    Ipv6Vrp::new

    fn Ipv6Vrp::new(prefix : Ipv6Prefix, max_length : Int, asn : UInt) -> Result[Ipv6Vrp, String]

    Ipv6Vrp::not_equal

    fn Ipv6Vrp::not_equal(x : Ipv6Vrp, y : Ipv6Vrp) -> Bool

    Ipv6Vrp::prefix

    fn Ipv6Vrp::prefix(self : Ipv6Vrp) -> Ipv6Prefix

    Ipv6Vrp::to_repr

    Ipv6VrpIndex

    pub struct Ipv6VrpIndex {
    buckets : Map[Ipv6Prefix, Array[Int]]
    vrps : Array[Ipv6Vrp]
    }

    An IPv6 VRP index that retains the input order of matching evidence.

    Ipv6VrpIndex::length

    fn Ipv6VrpIndex::length(self : Ipv6VrpIndex) -> Int

    Ipv6VrpIndex::new

    Ipv6VrpIndex::validate

    Ipv6VrpIndex::validate_all

    Ipv6VrpMatch

    pub(all) struct Ipv6VrpMatch {
    vrp : Ipv6Vrp
    relation : VrpRelation
    } derive(Eq,
    Debug
    )

    Ipv6VrpMatch::equal

    Ipv6VrpMatch::not_equal

    fn Ipv6VrpMatch::not_equal(x : Ipv6VrpMatch, y : Ipv6VrpMatch) -> Bool

    Ipv6VrpRecord

    pub(all) struct Ipv6VrpRecord {
    vrp : Ipv6Vrp
    trust_anchor : String
    }

    An IPv6 VRP and the trust-anchor label supplied by a validator.

    Ipv6VrpRecordKey

    type Ipv6VrpRecordKey derive(Eq, Hash)

    Ipv6VrpRecordKey::equal

    Ipv6VrpRecordKey::hash

    fn Ipv6VrpRecordKey::hash(self : Ipv6VrpRecordKey) -> Int

    Ipv6VrpRecordKey::hash_combine

    fn Ipv6VrpRecordKey::hash_combine(Ipv6VrpRecordKey, Hasher) -> Unit

    Ipv6VrpRecordKey::not_equal

    fn Ipv6VrpRecordKey::not_equal(x : Ipv6VrpRecordKey, y : Ipv6VrpRecordKey) -> Bool

    RouteAnnouncement

    pub struct RouteAnnouncement {
    prefix : Ipv4Prefix
    origin_asn : UInt
    } derive(Eq,
    Debug
    )

    A BGP route reduced to the fields used by route origin validation.

    RouteAnnouncement::equal

    RouteAnnouncement::new

    fn RouteAnnouncement::new(prefix : Ipv4Prefix, origin_asn : UInt) -> RouteAnnouncement

    RouteAnnouncement::not_equal

    fn RouteAnnouncement::not_equal(x : RouteAnnouncement, y : RouteAnnouncement) -> Bool

    RouteAnnouncement::origin_asn

    fn RouteAnnouncement::origin_asn(self : RouteAnnouncement) -> UInt

    RouteAnnouncement::prefix

    RouteCsvResult

    pub(all) struct RouteCsvResult {
    routes : Array[RouteAnnouncement]
    diagnostics : Array[CsvDiagnostic]
    }

    Route announcements and recoverable diagnostics from a two-column CSV.

    RouteCsvResult::is_valid

    fn RouteCsvResult::is_valid(self : RouteCsvResult) -> Bool

    RouteImpact

    pub(all) struct RouteImpact {
    previous : ValidationDecision
    current : ValidationDecision
    } derive(Eq,
    Debug
    )

    One route whose origin-validation state changes between two VRP snapshots.

    RouteImpact::equal

    fn RouteImpact::equal(RouteImpact, RouteImpact) -> Bool

    Keep methods generated by derives explicit under MoonBit 0.1.20260920+.

    RouteImpact::explanation

    fn RouteImpact::explanation(self : RouteImpact) -> String

    Explain a route-state transition using all old and new covering VRPs.

    Evidence remains in validator order. An empty side is stated explicitly so a NotFound decision cannot be mistaken for a missing report section.

    RouteImpact::not_equal

    fn RouteImpact::not_equal(x : RouteImpact, y : RouteImpact) -> Bool

    Keep methods generated by derives explicit under MoonBit 0.1.20260920+.

    RouteImpact::summary

    fn RouteImpact::summary(self : RouteImpact) -> String

    RouteValidity

    pub(all) enum RouteValidity {
    Valid
    Invalid
    NotFound
    } derive(Eq,
    Debug
    )

    The three route origin validation states defined by RFC 6811.

    RouteValidity::equal

    RouteValidity::not_equal

    fn RouteValidity::not_equal(x : RouteValidity, y : RouteValidity) -> Bool

    RtrPdu

    pub(all) enum RtrPdu {
    SerialNotify(Int, UInt)
    SerialQuery(Int, UInt)
    ResetQuery
    CacheResponse(Int)
    Ipv4PrefixPdu(Bool, Vrp)
    Ipv6PrefixPdu(Bool, Ipv6Vrp)
    EndOfData(Int, UInt, UInt, UInt, UInt)
    CacheReset
    } derive(Eq,
    Debug
    )

    RFC 8210 version 1 control and route-origin prefix PDUs.

    Session IDs are represented as Int values in the range 0..65535. Interval values in EndOfData are seconds.

    Example

    test {
    let prefix = @moonrouteguard.Ipv6Prefix::parse("2001:db8::/32").unwrap()
    let vrp = @moonrouteguard.Ipv6Vrp::new(prefix, 48, 64496U).unwrap()
    let pdu = @moonrouteguard.RtrPdu::Ipv6PrefixPdu(true, vrp)
    assert_eq(
    @moonrouteguard.decode_rtr_pdu(@moonrouteguard.encode_rtr_pdu(pdu).unwrap()).unwrap(),
    pdu,
    )
    }

    RtrPdu::equal

    fn RtrPdu::equal(RtrPdu, RtrPdu) -> Bool

    RtrPdu::not_equal

    fn RtrPdu::not_equal(x : RtrPdu, y : RtrPdu) -> Bool

    RtrPdu::to_repr

    SlurmApplyResult

    pub(all) struct SlurmApplyResult {
    vrps : Array[Vrp]
    filtered : Int
    duplicate_assertions : Int
    } derive(Eq,
    Debug
    )

    SlurmApplyResult::equal

    SlurmApplyResult::not_equal

    fn SlurmApplyResult::not_equal(x : SlurmApplyResult, y : SlurmApplyResult) -> Bool

    SlurmApplyResult::summary

    fn SlurmApplyResult::summary(self : SlurmApplyResult) -> String

    SlurmPolicy

    pub(all) struct SlurmPolicy {
    filters : Array[SlurmPrefixFilter]
    assertions : Array[Vrp]
    ipv6_assertions : Array[Ipv6Vrp]
    assertion_is_ipv6 : Array[Bool]
    } derive(Eq,
    Debug
    )

    The route-origin subset of one RFC 8416 SLURM document.

    SlurmPolicy::apply

    fn SlurmPolicy::apply(self : SlurmPolicy, validated : Array[Vrp]) -> SlurmApplyResult

    SlurmPolicy::apply_ipv6

    fn SlurmPolicy::apply_ipv6(self : SlurmPolicy, validated : Array[Ipv6Vrp]) -> Ipv6SlurmApplyResult

    Apply this policy's ASN-only and IPv6 prefix rules to IPv6 VRPs.

    SlurmPolicy::equal

    fn SlurmPolicy::equal(SlurmPolicy, SlurmPolicy) -> Bool

    SlurmPolicy::not_equal

    fn SlurmPolicy::not_equal(x : SlurmPolicy, y : SlurmPolicy) -> Bool

    SlurmPrefixFilter

    pub struct SlurmPrefixFilter {
    prefix : Ipv4Prefix?
    ipv6_prefix : Ipv6Prefix?
    asn : UInt?
    comment : String
    } derive(Eq,
    Debug
    )

    An RFC 8416 prefix filter for validated ROA payloads.

    SlurmPrefixFilter::asn

    fn SlurmPrefixFilter::asn(self : SlurmPrefixFilter) -> UInt?

    SlurmPrefixFilter::by_asn

    fn SlurmPrefixFilter::by_asn(asn : UInt, comment? : String) -> SlurmPrefixFilter

    SlurmPrefixFilter::by_ipv6_prefix

    fn SlurmPrefixFilter::by_ipv6_prefix(prefix : Ipv6Prefix, comment? : String) -> SlurmPrefixFilter

    SlurmPrefixFilter::by_ipv6_prefix_and_asn

    fn SlurmPrefixFilter::by_ipv6_prefix_and_asn(prefix : Ipv6Prefix, asn : UInt, comment? : String) -> SlurmPrefixFilter

    SlurmPrefixFilter::by_prefix

    fn SlurmPrefixFilter::by_prefix(prefix : Ipv4Prefix, comment? : String) -> SlurmPrefixFilter

    SlurmPrefixFilter::by_prefix_and_asn

    fn SlurmPrefixFilter::by_prefix_and_asn(prefix : Ipv4Prefix, asn : UInt, comment? : String) -> SlurmPrefixFilter

    SlurmPrefixFilter::comment

    fn SlurmPrefixFilter::comment(self : SlurmPrefixFilter) -> String

    SlurmPrefixFilter::equal

    SlurmPrefixFilter::ipv6_prefix

    fn SlurmPrefixFilter::ipv6_prefix(self : SlurmPrefixFilter) -> Ipv6Prefix?

    SlurmPrefixFilter::not_equal

    fn SlurmPrefixFilter::not_equal(x : SlurmPrefixFilter, y : SlurmPrefixFilter) -> Bool

    SlurmPrefixFilter::prefix

    SlurmVrpKey

    type SlurmVrpKey derive(Eq, Hash)

    SlurmVrpKey::equal

    fn SlurmVrpKey::equal(SlurmVrpKey, SlurmVrpKey) -> Bool

    SlurmVrpKey::hash

    fn SlurmVrpKey::hash(self : SlurmVrpKey) -> Int

    SlurmVrpKey::hash_combine

    fn SlurmVrpKey::hash_combine(SlurmVrpKey, Hasher) -> Unit

    SlurmVrpKey::not_equal

    fn SlurmVrpKey::not_equal(x : SlurmVrpKey, y : SlurmVrpKey) -> Bool

    SnapshotDiff

    pub(all) struct SnapshotDiff {
    added : Array[VrpRecord]
    removed : Array[VrpRecord]
    unchanged : Int
    } derive(Eq,
    Debug
    )

    The exact VRP records added to and removed from a validator snapshot.

    Snapshot comparison uses set semantics: duplicate records in either input are counted once. Trust-anchor labels are part of a record's identity.

    SnapshotDiff::equal

    SnapshotDiff::is_empty

    fn SnapshotDiff::is_empty(self : SnapshotDiff) -> Bool

    SnapshotDiff::not_equal

    fn SnapshotDiff::not_equal(x : SnapshotDiff, y : SnapshotDiff) -> Bool

    SnapshotDiff::summary

    fn SnapshotDiff::summary(self : SnapshotDiff) -> String

    SnapshotImpact

    pub(all) struct SnapshotImpact {
    changed : Array[RouteImpact]
    unchanged : Int
    } derive(Eq,
    Debug
    )

    Route-validation changes caused by replacing one complete VRP snapshot.

    SnapshotImpact::equal

    SnapshotImpact::is_empty

    fn SnapshotImpact::is_empty(self : SnapshotImpact) -> Bool

    SnapshotImpact::not_equal

    fn SnapshotImpact::not_equal(x : SnapshotImpact, y : SnapshotImpact) -> Bool

    SnapshotImpact::summary

    fn SnapshotImpact::summary(self : SnapshotImpact) -> String

    ValidationDecision

    pub(all) struct ValidationDecision {
    route : RouteAnnouncement
    validity : RouteValidity
    matches : Array[VrpMatch]
    } derive(Eq,
    Debug
    )

    ValidationDecision::equal

    ValidationDecision::not_equal

    ValidationDecision::summary

    fn ValidationDecision::summary(self : ValidationDecision) -> String

    Vrp

    pub struct Vrp {
    prefix : Ipv4Prefix
    max_length : Int
    asn : UInt
    } derive(Eq,
    Debug
    )

    One validated ROA payload supplied by an external RPKI validator.

    Vrp::asn

    fn Vrp::asn(self : Vrp) -> UInt

    Vrp::equal

    fn Vrp::equal(Vrp, Vrp) -> Bool

    Vrp::max_length

    fn Vrp::max_length(self : Vrp) -> Int

    Vrp::new

    fn Vrp::new(prefix : Ipv4Prefix, max_length : Int, asn : UInt) -> Result[Vrp, String]

    Vrp::not_equal

    fn Vrp::not_equal(x : Vrp, y : Vrp) -> Bool

    Vrp::prefix

    fn Vrp::prefix(self : Vrp) -> Ipv4Prefix

    Vrp::to_repr

    VrpCsvResult

    pub(all) struct VrpCsvResult {
    records : Array[VrpRecord]
    diagnostics : Array[CsvDiagnostic]
    } derive(Eq,
    Debug
    )

    VrpCsvResult::equal

    VrpCsvResult::is_valid

    fn VrpCsvResult::is_valid(self : VrpCsvResult) -> Bool

    VrpCsvResult::not_equal

    fn VrpCsvResult::not_equal(x : VrpCsvResult, y : VrpCsvResult) -> Bool

    VrpCsvStyle

    pub(all) enum VrpCsvStyle {
    RoutinatorCsv
    RoutinatorCsvCompat
    } derive(Eq,
    Debug
    )

    The two Routinator CSV layouts accepted by the parser.

    VrpCsvStyle::equal

    fn VrpCsvStyle::equal(VrpCsvStyle, VrpCsvStyle) -> Bool

    VrpCsvStyle::not_equal

    fn VrpCsvStyle::not_equal(x : VrpCsvStyle, y : VrpCsvStyle) -> Bool

    VrpIndex

    pub struct VrpIndex {
    buckets : Array[Map[UInt, Array[Int]]]
    vrps : Array[Vrp]
    }

    An in-memory IPv4 VRP index for repeated route validation.

    The index groups payloads by prefix length and network address. Validation performs at most 33 prefix-key lookups instead of scanning every payload.

    VrpIndex::length

    fn VrpIndex::length(self : VrpIndex) -> Int

    VrpIndex::new

    fn VrpIndex::new(vrps : Array[Vrp]) -> VrpIndex

    VrpIndex::validate

    fn VrpIndex::validate(self : VrpIndex, route : RouteAnnouncement) -> ValidationDecision

    Validate one route using the indexed VRP set.

    VrpIndex::validate_all

    fn VrpIndex::validate_all(self : VrpIndex, routes : Array[RouteAnnouncement]) -> Array[ValidationDecision]

    Validate routes in input order using the indexed VRP set.

    VrpMatch

    pub(all) struct VrpMatch {
    vrp : Vrp
    relation : VrpRelation
    } derive(Eq,
    Debug
    )

    VrpMatch::equal

    fn VrpMatch::equal(VrpMatch, VrpMatch) -> Bool

    VrpMatch::not_equal

    fn VrpMatch::not_equal(x : VrpMatch, y : VrpMatch) -> Bool

    VrpMatch::to_repr

    VrpRecord

    pub(all) struct VrpRecord {
    vrp : Vrp
    trust_anchor : String
    } derive(Eq,
    Debug
    )

    A VRP together with the trust anchor label reported by the validator.

    VrpRecord::equal

    fn VrpRecord::equal(VrpRecord, VrpRecord) -> Bool

    VrpRecord::not_equal

    fn VrpRecord::not_equal(x : VrpRecord, y : VrpRecord) -> Bool

    VrpRecordKey

    type VrpRecordKey derive(Eq, Hash)

    VrpRecordKey::equal

    VrpRecordKey::hash

    fn VrpRecordKey::hash(self : VrpRecordKey) -> Int

    VrpRecordKey::hash_combine

    fn VrpRecordKey::hash_combine(VrpRecordKey, Hasher) -> Unit

    VrpRecordKey::not_equal

    fn VrpRecordKey::not_equal(x : VrpRecordKey, y : VrpRecordKey) -> Bool

    VrpRelation

    pub(all) enum VrpRelation {
    Authorizes
    AsnMismatch
    LengthExceeded
    AsnAndLengthMismatch
    } derive(Eq,
    Debug
    )

    How one covering VRP relates to the route being checked.

    VrpRelation::equal

    fn VrpRelation::equal(VrpRelation, VrpRelation) -> Bool

    VrpRelation::not_equal

    fn VrpRelation::not_equal(x : VrpRelation, y : VrpRelation) -> Bool

    apply_ipv6_slurm

    fn apply_ipv6_slurm(validated : Array[Ipv6Vrp], filters : Array[SlurmPrefixFilter], assertions : Array[Ipv6Vrp]) -> Ipv6SlurmApplyResult

    Apply RFC 8416 IPv6 filters to validated output, then add local assertions. ASN-only filters apply to both address families; IPv4 prefix filters do not match IPv6 VRPs.

    apply_slurm

    fn apply_slurm(validated : Array[Vrp], filters : Array[SlurmPrefixFilter], assertions : Array[Vrp]) -> SlurmApplyResult

    Apply RFC 8416 IPv4 prefix filters and local assertions atomically.

    Filters are applied only to the validated input. Assertions are appended afterwards and exact duplicates are omitted.

    assess_ipv6_snapshot_impact

    fn assess_ipv6_snapshot_impact(routes : Array[Ipv6RouteAnnouncement], previous_vrps : Array[Ipv6Vrp], current_vrps : Array[Ipv6Vrp]) -> Ipv6SnapshotImpact

    Evaluate IPv6 route decisions against old and new VRP snapshots.

    Only validity transitions appear in changed. Changes to evidence that preserve the validity state are counted as unchanged.

    assess_snapshot_impact

    fn assess_snapshot_impact(routes : Array[RouteAnnouncement], previous_vrps : Array[Vrp], current_vrps : Array[Vrp]) -> SnapshotImpact

    Evaluate how replacing a VRP snapshot changes a set of route decisions.

    Only validity transitions are reported. Changes to covering evidence that leave a route in the same state are counted as unchanged.

    compare_ipv6_snapshots

    fn compare_ipv6_snapshots(previous : Array[Ipv6VrpRecord], current : Array[Ipv6VrpRecord]) -> Ipv6SnapshotDiff

    Compare IPv6 VRP records with set semantics and trust-anchor identity.

    Added records retain their order in current; removed records retain their order in previous. Duplicate exact records count only once.

    compare_snapshots

    fn compare_snapshots(previous : Array[VrpRecord], current : Array[VrpRecord]) -> SnapshotDiff

    Compare two complete VRP snapshots.

    Added records retain their order in current; removed records retain their order in previous. A change to an ASN, prefix, maximum length, or trust anchor is represented by one removal and one addition.

    decode_rtr_pdu

    fn decode_rtr_pdu(data : Bytes) -> Result[RtrPdu, String]

    Decode exactly one supported RFC 8210 version 1 PDU.

    decode_rtr_pdus

    fn decode_rtr_pdus(data : Bytes) -> Result[Array[RtrPdu], String]

    Decode a complete byte stream containing consecutive supported PDUs.

    encode_rtr_pdu

    fn encode_rtr_pdu(pdu : RtrPdu) -> Result[Bytes, String]

    Encode one supported RFC 8210 version 1 PDU in network byte order.

    ipv6_slurm_assertion

    fn ipv6_slurm_assertion(prefix : Ipv6Prefix, asn : UInt, max_length? : Int) -> Result[Ipv6Vrp, String]

    Create an IPv6 local assertion; the default maximum length is the prefix length, just as it is for IPv4 assertions.

    parse_dual_stack_route_csv

    fn parse_dual_stack_route_csv(source : StringView) -> DualStackRouteCsvResult

    Parse IPv4 and IPv6 announcements from ASN,IP Prefix CSV input.

    The family-specific arrays preserve input order within each family. Malformed rows are reported with line numbers; later rows are retained.

    parse_dual_stack_vrp_csv

    fn parse_dual_stack_vrp_csv(source : StringView) -> DualStackVrpCsvResult

    Parse mixed IPv4/IPv6 Routinator csv or csvcompat output.

    Valid rows are retained within their address family in input order. Malformed rows become line-numbered diagnostics without hiding later rows. The existing IPv4-only parse_vrp_csv API remains unchanged.

    parse_route_csv

    fn parse_route_csv(source : StringView) -> RouteCsvResult

    Parse IPv4 route announcements in ASN,IP Prefix CSV layout.

    Both AS64496 and 64496 are accepted as ASN values. Bad rows are reported with line numbers without hiding valid rows that follow them.

    parse_slurm_json

    fn parse_slurm_json(source : StringView) -> Result[SlurmPolicy, String]

    Parse the route-origin subset of an RFC 8416 SLURM JSON file.

    The document is rejected atomically on malformed JSON, unknown members, unsupported versions, invalid prefixes, or non-empty BGPsec sections.

    parse_vrp_csv

    fn parse_vrp_csv(source : StringView) -> VrpCsvResult

    Parse Routinator csv or csvcompat VRP output.

    Invalid data rows are retained as line-numbered diagnostics while later rows continue to be processed.

    slurm_assertion

    fn slurm_assertion(prefix : Ipv4Prefix, asn : UInt, max_length? : Int) -> Result[Vrp, String]

    Create a local SLURM assertion. An omitted maximum length defaults to the asserted prefix length as required by RFC 8416.

    validate_ipv6_route

    fn validate_ipv6_route(route : Ipv6RouteAnnouncement, vrps : Array[Ipv6Vrp]) -> Ipv6ValidationDecision

    Validate an IPv6 route against a complete set of IPv6 VRPs.

    validate_ipv6_routes

    fn validate_ipv6_routes(routes : Array[Ipv6RouteAnnouncement], vrps : Array[Ipv6Vrp]) -> Array[Ipv6ValidationDecision]

    validate_route

    fn validate_route(route : RouteAnnouncement, vrps : Array[Vrp]) -> ValidationDecision

    Validate a route against a complete set of validated ROA payloads.

    Every covering VRP is retained as evidence. A route is valid if at least one covering VRP authorizes both its origin ASN and prefix length.

    validate_routes

    fn validate_routes(routes : Array[RouteAnnouncement], vrps : Array[Vrp]) -> Array[ValidationDecision]

    Validate routes in input order against the same VRP set.

    write_assessment_json

    fn write_assessment_json(diff : SnapshotDiff, impact : SnapshotImpact, slurm? : (SlurmApplyResult?, SlurmApplyResult?), ipv6_slurm? : (Ipv6SlurmApplyResult?, Ipv6SlurmApplyResult?), ipv6? : (Ipv6SnapshotDiff, Ipv6SnapshotImpact)) -> String

    Serialize a snapshot comparison and its route impact as deterministic JSON.

    Only routes whose validity state changes appear in the changes array; the total number of other checked routes appears in unchanged. Covering VRPs explain each old and new decision. Exact added and removed VRP records retain their trust-anchor labels. Per-decision covering VRPs do not carry labels because the validation decision model does not retain their source. IPv4 and IPv6 SLURM application statistics occupy separate optional fields. Each route-impact section counts transitions away from Valid as validLosses.

    write_slurm_json

    fn write_slurm_json(policy : SlurmPolicy) -> String

    Write the route-origin subset of an RFC 8416 SLURM policy.

    Array order is preserved, insignificant assertion maximum lengths are omitted, and the result always uses two-space indentation and LF endings.

    write_vrp_csv

    fn write_vrp_csv(records : Array[VrpRecord], style : VrpCsvStyle) -> Result[String, String]

    Write VRP records in Routinator csv or csvcompat form.

    Input order is preserved and line endings are always LF. Empty trust-anchor labels and embedded line breaks are rejected so the result can be parsed without losing record boundaries.