Skip to content

Python 库使用流程

rpkiparrot 的主要交付对象是可独立嵌入第三方应用的 Python 库。已有程序化数据集、验证、低层 RTR、在线自定义 JSON和事件及热配置可执行示例,真实入口见源码 API 参考。持久化、共享、诊断及远程示例见下文;当前未发布,完整验收状态以对应提交和报告为准。

离线读取和 ROV

此例使用完整的小型合成数据,显式选择离线参考时间。没有修改历史样本时间,也不把这份数据当作在线全网快照。

from datetime import datetime, timezone

from rpkiparrot import MemoryStore
from rpkiparrot.config import FreshnessPolicy
from rpkiparrot.models import EvaluationContext, OriginInput
from rpkiparrot.readers import parse_json
from rpkiparrot.validation import validate_origin, validate_origins

dataset = parse_json(
    {
        "metadata": {"generatedTime": "2026-10-01T00:00:00Z"},
        "roas": [{"asn": "AS64496", "prefix": "192.0.2.0/24", "maxLength": 24}],
        "aspas": [],
    },
    format="routinator-json",
)
store = MemoryStore(
    context=EvaluationContext.offline(at=datetime(2026, 10, 1, 0, 30, tzinfo=timezone.utc))
)
snapshot = store.replace_source("example-json", dataset, freshness=FreshnessPolicy(max_age=3600))
result = validate_origin(snapshot, "192.0.2.0/24", 64496, explain=True)
assert result.status.value == "valid"

batch = validate_origins(
    snapshot,
    [
        OriginInput(prefix="192.0.2.0/24", asn=64496, input_id="a"),
        OriginInput(prefix="192.0.2.0/25", asn=64496, input_id="b"),
        OriginInput(prefix="198.51.100.0/24", asn=64496, input_id="c"),
    ],
)
# 顺序保持 a/b/c;预期分别为 valid、invalid、notfound。
assert batch.complete

真实文件使用 read_json(path, format=...)。文件读取与解析同步,异步应用中的大文件交给受管理工作线程或 Client 的文件来源适配器。没有加载数据时不能调用验证并期待得到正常 notfound;空但完整有效的 roas 数组才具有这种语义。

标准 ASPA

AS_PATH 顺序为邻居在前、起源在后。此片段接续上面的快照:ASPA 已同步为空,所以多 AS 路径的授权数据不足会产生 unknown,区别于 ASPA 未加载异常。

from rpkiparrot.models import AsPath, AspaContext, NeighborRelationship
from rpkiparrot.validation import validate_aspa

aspa_result = validate_aspa(
    snapshot,
    AsPath.sequence((64497, 64496)),
    context=AspaContext(
        local_asn=64498,
        neighbor_asn=64497,
        relationship=NeighborRelationship.CUSTOMER,
    ),
    explain=True,
)
assert aspa_result.status.value == "unknown"

调用方负责 AS_PATH/AS4_PATH 重建和邻居上下文。BMP 观察缺少这些条件时调用独立 analyze_bmp_path,不补造邻居关系。完整规则见输入契约。

在应用任务组中运行 RTR

这个例子需要实际的 RTR 服务端点,文中的 example.net 地址仅为占位。anyio.run 属于示例应用入口,库内部不创建事件循环。

import anyio

from rpkiparrot import Client
from rpkiparrot.config import ClientConfig, RtrSourceConfig, SourceGroupConfig
from rpkiparrot.models import PayloadKind
from rpkiparrot.validation import validate_origin

config = ClientConfig(
    groups=(
        SourceGroupConfig(
            id="primary",
            priority=0,
            sources=(
                RtrSourceConfig(
                    id="cache-a",
                    host="cache.example.net",
                    transport="tls",
                    port=324,
                    trust_profile_id="operator-ca-v1",
                    server_name="cache.example.net",
                    ca_file="operator-ca.pem",
                    client_cert_file="router-cert.pem",
                    client_key_file="router-key.pem",
                ),
            ),
        ),
    ),
)


async def main() -> None:
    async with anyio.create_task_group() as tasks:
        async with Client(config) as client:
            await tasks.start(client.run)
            current = await client.wait_ready(required=frozenset({PayloadKind.VRP}), timeout=30.0)
            print(validate_origin(current, "192.0.2.0/24", 64496))
        # Client 先关闭,run 退出,然后应用任务组完成。


if __name__ == "__main__":
    anyio.run(main)

实际测试以本地协议替身运行同一生命周期,分别选择 asyncio 和 Trio 后端。连接成功不等于已获得数据;wait_ready 结束后保留的快照也可能随后到期,此时重新 get_snapshot。

上例用 ca_file 限定运营者 CA。省略它则使用 Python/OpenSSL 默认信任库,可能受 SSL_CERT_FILE / SSL_CERT_DIR 影响;也可以注入已加载客户端证书的 SSLContext。信任变化时同步修改 trust_profile_id;这个 ID 本身不会选择 CA。默认信任不免除双向 TLS、证书链和 SAN dNSName 验证。参见可执行传输示例和配置契约。

通过 SSH 连接 RTR 上游

安装 rpkiparrot[ssh] 后,将来源换为下列配置;其余 Client、任务组、就绪和查询流程相同。known_hosts 的主机密钥由运营者通过可信渠道分发,用户名/密钥需在服务端获准访问 rpki-rtr subsystem。示例域名和文件是部署占位。

from rpkiparrot.config import RtrSourceConfig, SshConfig

source = RtrSourceConfig(
    id="ssh-cache",
    host="cache.example.net",
    transport="ssh",  # 默认 22,可显式指定服务端端口
    trust_profile_id="operator-ssh-v1",
    ssh=SshConfig(
        username="rtr-reader",
        known_hosts="credentials/known_hosts",
        client_keys=("credentials/id_ed25519",),
    ),
)

加密密钥可传 passphrase_provider,密码认证可传 password_provider;均为准备阶段调用的同步回调。运行应用使用 asyncio;内置适配器不支持 Trio。新凭据通过来源替换与新 trust_profile_id 提交。无需真实上游的 可执行 SSH 示例 生成临时密钥并演示本地认证、完整同步和 ROV;它的服务端只作为示例夹具,不是库提供的 RTR 服务。

观察变化和重新同步

以下协程由宿主在已经运行的 Client 上调用。为使“跳到更新的当前快照”仍然正确,例子每次重新验证应用持有的全部路由;大型应用可改用事件给出的保守影响范围,但必须维护自己的路由索引和事件连续性。

from rpkiparrot.errors import ResyncRequiredError
from rpkiparrot.models import OriginInput, SnapshotEvent
from rpkiparrot.validation import validate_origins

routes = (OriginInput(prefix="192.0.2.0/24", asn=64496, input_id="route-1"),)


def revalidate_all(snapshot) -> None:
    results = validate_origins(snapshot, routes)
    for item in results.items:
        print(item.input_id, item.result, item.error)


async def observe(client) -> None:
    while True:
        try:
            async with client.watch() as subscription:
                revalidate_all(subscription.initial)
                async for event in subscription:
                    if isinstance(event, SnapshotEvent):
                        revalidate_all(await client.get_snapshot())
            return
        except ResyncRequiredError:
            # 重建订阅时同时取得新 initial,不分开读取和注册。
            continue

正常 Client 关闭使循环结束,取消向宿主传播。批量的逐条不可用也通过 item.error 检查;不要只检查函数有没有抛异常。处理慢时不得静默跳过 ResyncRequired。

运行中增删来源和修改配置

下面的设计示例由宿主在已运行的 Client 上调用,current 是宿主保存的当前冻结配置。每次传入取得配置时对应的 revision;其他任务先修改了配置时应处理 ConfigurationConflictError,重新构造候选,不能盲目覆盖。

from dataclasses import replace

from rpkiparrot.config import ClientConfig, RtrSourceConfig


async def add_cache(client, current: ClientConfig, revision):
    primary = current.groups[0]
    new_source = RtrSourceConfig(
        id="cache-b",
        host="cache-b.example.net",
        port=323,
        transport="tcp",
        trust_profile_id="operator-cache-b-v1",
    )
    candidate = replace(
        current,
        groups=(replace(primary, sources=primary.sources + (new_source,)),) + current.groups[1:],
    )
    receipt = await client.apply_config(candidate, expected_revision=revision)
    return candidate, receipt


async def change_query_timeout(client, current: ClientConfig, revision):
    primary = current.groups[0]
    source = primary.sources[0]  # 此例要求它是 RtrSourceConfig。
    candidate = replace(
        current,
        groups=(
            replace(primary, sources=(replace(source, query_timeout=180),) + primary.sources[1:]),
        )
        + current.groups[1:],
    )
    receipt = await client.apply_config(candidate, expected_revision=revision)
    return candidate, receipt


async def remove_all_sources(client, current: ClientConfig, revision):
    candidate = replace(current, groups=())
    receipt = await client.apply_config(candidate, expected_revision=revision)
    return candidate, receipt

增加来源的回执不代表该来源已同步;按来源状态和事件观察结果。删除单个来源时按 ID 过滤对应 sources,再以同一 apply_config 提交;不要就地修改元组、SSLContext 或注入适配器。删除后 Client 可继续接受新增来源。新配置不回写已经取得的快照,撤换信任来源后应获取新快照并重验,见完整变更契约。

在线自定义 JSON 格式与 ASPA 遍历

宿主显式创建 reader 并绑定来源,不需要更改模块级注册表。此例的 adapter 由宿主实现 JsonFormatAdapter,source 的 format 与 reader_profile_id 必须明确且与它相符。

from rpkiparrot import Client
from rpkiparrot.config import ClientConfig, SourceGroupConfig
from rpkiparrot.query import iter_aspas
from rpkiparrot.readers import JsonReader


def client_for_json(source, adapter) -> Client:
    config = ClientConfig(groups=(SourceGroupConfig(id="json", priority=0, sources=(source,)),))
    return Client(config, readers={source.id: JsonReader(adapters=(adapter,))})


def list_customers(snapshot) -> None:
    for entry in iter_aspas(snapshot):
        print(entry.customer, entry.providers)

Client 按前面的任务组示例打开和运行。ASPA 已同步为空时迭代零项,未加载或到期时明确报错;迭代期间固定快照,不逐项偷偷取得新版本。需要单一来源的原始集合时使用来源诊断/完整导出,source_id 筛选后的生效遍历仍返回合并 provider 集合。

可选能力

需求 使用方式
SQLite/DuckDB 恢复 显式 PersistenceConfig;需要确认落盘时 await client.flush 并检查回执
本机多个进程读取 SQLite SharedSnapshotClient;使用自身快照身份并保留 upstream_id
远程服务 http extra 中的 RemoteClient;异步返回相同领域结果,使用 SnapshotRef 固定远程版本
自定义连接 按来源 ID 注入 TransportFactory,每次 connect 交付新 ByteStream
只解码 RTR 独立 PduDecoder,不创建 Client 或打开网络
只加载导出文件 load_snapshot,保留原始来源与期限,不重新设置 max_age

分发包含核心 rpkiparrot 和按需 extras,例如 rpkiparrot[cli]、rpkiparrot[sqlite]、rpkiparrot[duckdb]、rpkiparrot[http]、rpkiparrot[service]、rpkiparrot[trio]、rpkiparrot[ssh]。当前工作区可通过 uv sync --locked --all-extras --group dev 安装开发环境;正式上传和发布版本另行公布。

显式验证指标

核心包提供 rpkiparrot.metrics.MetricsCollector。使用 collector.validate_origin(snapshot, prefix, asn) 或相应批量/ASPA/BMP 方法代替需要计数的直接调用,再以 collector.get_metrics() 采样。输入、返回类型、异常和固定快照时效规则与纯函数相同;直接调用纯函数不会改动该收集器。默认不计时,MetricsCollector(timing=True) 同时累计耗时。每个收集器独立,无后台任务,可由受管理线程共享。

可执行指标示例验证混合批次的调用数、条目数与错误数。正常的 invalid 或 unknown 结果不表示执行错误;字段单位和计数边界见状态与指标。Client.get_metrics 另外提供来源、快照、事件与持久化指标,HTTP 服务主动收集其自身的领域验证调用。

ASPA 地址族与持久化示例

按地址族验证运行独立的 IPv4/IPv6 授权及验证。给查询和验证传 afi=Afi.IPV4 或 afi=Afi.IPV6;不指定 AFI 只适用于两族完整视图等价的情况。Client.wait_ready(..., aspa_afi=Afi.IPV4) 可等待单族,省略时等待两族分别可用。

在线持久化与恢复可在 SQLite/DuckDB 及 asyncio/Trio 组合运行,实际完成来源同步、flush、关闭和原期限内恢复。恢复发生在 Client 上下文进入时,开始 run 之前即可查询;未启动 run 的只读恢复检查不提交新的 head。CLI 持久化用独立子进程验证写入与 SQLite 共享读取。固定线程后端的同步使用见后端示例,共享轮询生命周期见共享示例。

远程 HTTP 调用

安装 rpkiparrot[http] 后,使用 async with RemoteClient(base_url) 管理 HTTP 连接。构造不联网,导入不要求服务框架。所有网络方法需要 await,领域结果仍是 OriginResult、AspaResult 等核心数据类。get_snapshot() 返回固定的 SnapshotRef;将其传给验证或遍历的 snapshot= 参数,可固定同一版本。ASPA 的 afi 与嵌入式接口相同,不默认合并两族。

iter_vrps 和 iter_aspas 是异步生成器,page_size 默认使用服务端配置,分页全程保留同一 token 和过滤条件;提前结束时使用 contextlib.aclosing。到期或淘汰明确失败,不以新快照补齐余页。watch() 是异步上下文,返回原子取得的 initial 和可异步遍历的后续事件;resync_required 要求重新观察,watch_conflict 可沿原游标重试。响应体和总请求时长均有界,线程完成解析后才释放所占资源。

可执行 SDK 示例默认使用独立固定 HTTP 响应,无需外部服务器,并可选择 asyncio/Trio;显式 --url 改为实际服务。这个 mock 示例不作为真实服务端的互操作或在线信任证据。完整端点、认证和生命周期见 HTTP 契约。

RTR 录制与离线回放

显式 Recorder 包装一个 RTR 会话的传输,记录应用层字节;会话参数和协议资源限制必须与 recorder header 一致。先关闭会话,再关闭 recorder,才能得到完整连接轨迹。录制超限或写入失败不阻塞来源同步,但报告不完整。replay(path, at=...) 同步离线检查录制完整性及协议状态机差异,不启动网络或发布在线数据。

可执行录制和回放示例在宿主管理的 asyncio/Trio 上执行;它只证明所示受控会话。matched 不代表实际宿主接纳或持久化已验证,完整边界见诊断契约。