Compare commits
14 Commits
dev_1.2_xu
...
afc50364f8
| Author | SHA1 | Date | |
|---|---|---|---|
| afc50364f8 | |||
| 6276d13814 | |||
| 0f3d65254e | |||
| 13516c4f73 | |||
| 68cbd3c500 | |||
| 1cc3351bef | |||
| 2a6a963ecd | |||
| 6e135b9d7a | |||
| afc31c02ab | |||
| 7be865d7f1 | |||
| a58e507f92 | |||
| cc9f3f21de | |||
| 56ae2ca4fc | |||
| bcd4829486 |
@@ -1,9 +0,0 @@
|
||||
.git
|
||||
.gitignore
|
||||
.idea
|
||||
target
|
||||
rtr-db
|
||||
tests
|
||||
specs
|
||||
scripts
|
||||
README.md
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,5 +1,3 @@
|
||||
target/
|
||||
rtr-db/
|
||||
.idea/
|
||||
logs/
|
||||
report/
|
||||
Cargo.lock
|
||||
perf.*
|
||||
|
||||
3528
Cargo.lock
generated
3528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -3,38 +3,33 @@ name = "rpki"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "rpki_rtr"
|
||||
path = "src/main_rtr.rs"
|
||||
[features]
|
||||
default = ["full"]
|
||||
# Full build used by the main RP implementation (includes RocksDB-backed storage).
|
||||
full = ["dep:rocksdb"]
|
||||
profile = ["dep:pprof", "dep:flate2"]
|
||||
|
||||
[dependencies]
|
||||
der-parser = "10.0.0"
|
||||
asn1-rs = "0.7.1"
|
||||
der-parser = { version = "10.0.0", features = ["serialize"] }
|
||||
hex = "0.4.3"
|
||||
base64 = "0.22.1"
|
||||
sha2 = "0.10.8"
|
||||
thiserror = "2.0.18"
|
||||
time = "0.3.45"
|
||||
ring = "0.17.14"
|
||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||
url = "2.5.8"
|
||||
asn1-rs = "0.7.1"
|
||||
asn1-rs-derive = "0.6.0"
|
||||
asn1 = "0.23.0"
|
||||
arc-swap = "1.7.0"
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
chrono-tz = "0.10"
|
||||
bytes = "1.11.1"
|
||||
tokio = { version = "1.49.0", features = ["full"] }
|
||||
rand = "0.10.0"
|
||||
rocksdb = { version = "0.21.0", default-features = false }
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
base64 = "0.22"
|
||||
anyhow = "1"
|
||||
tracing = "0.1.44"
|
||||
sha2 = "0.10"
|
||||
tempfile = "3"
|
||||
tokio-rustls = "0.26"
|
||||
rustls = "0.23"
|
||||
rustls-pemfile = "2"
|
||||
rustls-pki-types = "1.14.0"
|
||||
socket2 = "0.5"
|
||||
russh = { version = "0.60.0", default-features = false, features = ["ring", "rsa"] }
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||
serde = { version = "1.0.218", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
toml = "0.8.20"
|
||||
rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] }
|
||||
serde_cbor = "0.11.2"
|
||||
roxmltree = "0.20.0"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
pprof = { version = "0.14.1", optional = true, features = ["flamegraph", "prost-codec"] }
|
||||
flate2 = { version = "1.0.35", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.16.0"
|
||||
|
||||
238
README.client.md
238
README.client.md
@@ -1,238 +0,0 @@
|
||||
# RTR Client 使用说明
|
||||
|
||||
本文说明仓库内三类 RTR Client 的用途、启动方式和验证命令:
|
||||
|
||||
- `deploy/client`: 本仓库自带的 `rtr_debug_client`,适合做 RTR 协议调试、连通性检查和单次查询。
|
||||
- `deploy/frr`: FRR 作为黑盒 RTR Client,适合验证真实路由软件是否能消费 server 下发的 VRP。
|
||||
- `deploy/bird`: BIRD 作为黑盒 RTR Client,适合验证 TCP 或 SSH transport 下的 RPKI/ROA/ASPA 数据导入。
|
||||
|
||||
以下命令默认从仓库根目录执行。
|
||||
|
||||
## 目录
|
||||
|
||||
- [前置条件](#前置条件)
|
||||
- [rtr_debug_client](#rtr_debug_client)
|
||||
- [TCP](#tcp)
|
||||
- [TLS/mTLS](#tlsmtls)
|
||||
- [SSH](#ssh)
|
||||
- [多客户端压测](#多客户端压测)
|
||||
- [FRR Client](#frr-client)
|
||||
- [BIRD Client](#bird-client)
|
||||
- [TCP](#tcp-1)
|
||||
- [SSH](#ssh-1)
|
||||
- [如何判断 Client 正常](#如何判断-client-正常)
|
||||
|
||||
## 前置条件
|
||||
|
||||
先启动 RTR Server。TCP 模式是最常用的本地验证方式:
|
||||
|
||||
```bash
|
||||
./deploy/server/build-run.sh up --mode tcp
|
||||
```
|
||||
|
||||
如果要测试 TLS 或 SSH,需要先启动对应 server 模式,并准备匹配的证书或 SSH key:
|
||||
|
||||
```bash
|
||||
./deploy/server/build-run.sh up --mode tls
|
||||
./deploy/server/build-run.sh up --mode ssh
|
||||
```
|
||||
|
||||
TLS/SSH 所需的本地开发证书和密钥可以通过脚本生成:
|
||||
|
||||
```bash
|
||||
bash scripts/generate-certs.sh
|
||||
```
|
||||
|
||||
已有 `certs/` 时使用 `--force` 覆盖。脚本生成的 `certs/tls` 和 `certs/ssh` 文件名与本仓库 compose 默认配置一致。
|
||||
|
||||
Client 连接地址不要求固定在某个 Docker 网络里。`deploy/client` 的 TCP/TLS/SSH 模式分别通过 `RPKI_RTR_TCP_SERVER_ADDR`、`RPKI_RTR_TLS_SERVER_ADDR`、`RPKI_RTR_SSH_SERVER_ADDR` 指定 server 地址。`deploy/bird` 通过 `RPKI_BIRD_RPKI_HOST`/`RPKI_BIRD_RPKI_PORT` 指定 TCP server 地址,通过 `RPKI_BIRD_SSH_RPKI_HOST`/`RPKI_RTR_SSH_PORT` 指定 SSH server 地址。示例 compose 里的默认值只是为了和本仓库 server compose 直接联调。
|
||||
|
||||
`deploy/frr` 示例默认连接 `127.0.0.1:323`,可以通过 `RPKI_FRR_RPKI_HOST`/`RPKI_FRR_RPKI_PORT` 覆盖。
|
||||
|
||||
## rtr_debug_client
|
||||
|
||||
`rtr_debug_client` 是本仓库提供的调试客户端。它会发起 RTR Reset Query 或 Serial Query,并打印 server 返回的 PDU 摘要。Docker 配置默认执行:
|
||||
|
||||
```text
|
||||
<server_addr> <protocol_version> reset --keep-after-error --summary-only
|
||||
```
|
||||
|
||||
### TCP
|
||||
|
||||
```bash
|
||||
./deploy/client/build-run.sh up --mode tcp
|
||||
./deploy/client/build-run.sh logs --mode tcp
|
||||
./deploy/client/build-run.sh down --mode tcp
|
||||
```
|
||||
|
||||
默认连接 `rpki-rtr:323`,协议版本默认是 `2`。可以通过 `deploy/client/.env` 或环境变量覆盖:
|
||||
|
||||
```bash
|
||||
RPKI_RTR_TCP_SERVER_ADDR=10.0.0.12:323 \
|
||||
RPKI_RTR_PROTOCOL_VERSION=2 \
|
||||
./deploy/client/build-run.sh up --mode tcp
|
||||
```
|
||||
|
||||
### TLS/mTLS
|
||||
|
||||
```bash
|
||||
./deploy/client/build-run.sh up --mode tls
|
||||
./deploy/client/build-run.sh logs --mode tls
|
||||
./deploy/client/build-run.sh down --mode tls
|
||||
```
|
||||
|
||||
TLS 模式默认读取 `certs/tls` 下的示例证书,并使用 `localhost` 作为 server name。常用变量:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `RPKI_RTR_TLS_SERVER_ADDR` | `rpki-rtr:324` | TLS RTR server 地址 |
|
||||
| `RPKI_RTR_TLS_SERVER_NAME` | `localhost` | 必须匹配 server 证书 SAN dNSName |
|
||||
| `RPKI_RTR_TLS_CA_CERT_PATH` | `/app/certs/client-ca.crt` | 容器内 CA 证书路径 |
|
||||
| `RPKI_RTR_TLS_CLIENT_CERT_PATH` | `/app/certs/client-good.crt` | 容器内 client 证书路径 |
|
||||
| `RPKI_RTR_TLS_CLIENT_KEY_PATH` | `/app/certs/client-good.key` | 容器内 client 私钥路径 |
|
||||
| `RPKI_RTR_TLS_CERTS_HOST_DIR` | `../../certs/tls` | 宿主机 TLS 证书目录 |
|
||||
|
||||
### SSH
|
||||
|
||||
```bash
|
||||
./deploy/client/build-run.sh up --mode ssh
|
||||
./deploy/client/build-run.sh logs --mode ssh
|
||||
./deploy/client/build-run.sh down --mode ssh
|
||||
```
|
||||
|
||||
SSH 模式默认连接 `rpki-rtr-ssh:22`,使用 key 认证,并校验 server public key。常用变量:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `RPKI_RTR_SSH_SERVER_ADDR` | `rpki-rtr:22` | SSH RTR server 地址 |
|
||||
| `RPKI_RTR_SSH_USERNAME` | `rpki-rtr` | SSH 用户名 |
|
||||
| `RPKI_RTR_CLIENT_KEYS_VOLUME` | `../../certs/ssh:/app/certs:ro` | 宿主机 SSH key 目录挂载 |
|
||||
| `RPKI_RTR_CLIENT_KEY_PATH` | `/app/certs/rtr-client.key` | 容器内 client 私钥 |
|
||||
| `RPKI_RTR_SSH_SERVER_PUBKEY_PATH` | `/app/certs/ssh_host_rsa_key.pub` | 容器内 server public key |
|
||||
|
||||
### 多客户端压测
|
||||
|
||||
`--clients multi` 会启动 5 个 TCP debug client,用于观察并发连接、client report 和 server 连接统计。
|
||||
|
||||
```bash
|
||||
./deploy/client/build-run.sh up --mode tcp --clients multi
|
||||
./deploy/client/build-run.sh logs --mode tcp --clients multi
|
||||
./deploy/client/build-run.sh down --mode tcp --clients multi
|
||||
```
|
||||
|
||||
## FRR Client
|
||||
|
||||
FRR 示例用于验证标准路由软件能否通过 RTR over TCP 从 server 获取前缀验证数据。
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
默认连接 `127.0.0.1:323`。可以通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
||||
RPKI_FRR_RPKI_PORT=323 \
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
验证连接和数据:
|
||||
|
||||
```bash
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki configuration"
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
||||
```
|
||||
|
||||
停止:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
||||
```
|
||||
|
||||
FRR 容器启动时会读取 `deploy/frr/frr.conf.template`,并生成 `/etc/frr/frr.conf`。模板中的 RTR cache 配置为:
|
||||
|
||||
```text
|
||||
rpki cache tcp ${RPKI_FRR_RPKI_HOST} ${RPKI_FRR_RPKI_PORT} preference ${RPKI_FRR_RPKI_PREFERENCE}
|
||||
```
|
||||
|
||||
如果需要调整 polling、timeout、BGP router-id 等其他 FRR 配置,可以修改 `deploy/frr/frr.conf.template`,或在 compose 中挂载自己的模板和 entrypoint。
|
||||
|
||||
## BIRD Client
|
||||
|
||||
BIRD 示例用于验证 BIRD 3.x 对 RTR v2、ROA 和 ASPA 的导入情况。容器启动后会周期性输出 RPKI 协议状态和表项摘要。
|
||||
|
||||
### TCP
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
查看日志:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
停止:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml down
|
||||
```
|
||||
|
||||
默认连接 `rpki-rtr:323`。可以通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
RPKI_BIRD_RPKI_HOST=10.0.0.12 \
|
||||
RPKI_BIRD_RPKI_PORT=323 \
|
||||
docker compose --env-file deploy/bird/.env -f deploy/bird/docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
### SSH
|
||||
|
||||
先启动 server SSH 模式:
|
||||
|
||||
```bash
|
||||
./deploy/server/build-run.sh up --mode ssh
|
||||
```
|
||||
|
||||
再启动 BIRD SSH client:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/bird/.env \
|
||||
-f deploy/bird/docker-compose.yml \
|
||||
-f deploy/bird/docker-compose.ssh.yml \
|
||||
up -d --build
|
||||
```
|
||||
|
||||
查看日志:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
SSH 模式默认读取 `certs/ssh` 目录下的 key,并使用 `bird.conf.ssh.template` 生成运行时配置。关键默认值:
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `RPKI_BIRD_SSH_RPKI_HOST` | `rpki-rtr` | SSH RTR server 主机名 |
|
||||
| `RPKI_RTR_SSH_PORT` | `22` | SSH RTR server 端口 |
|
||||
| `RPKI_BIRD_SSH_CERTS_HOST_DIR` | `../../certs/ssh` | 宿主机 SSH 证书和 key 目录,复用 `rtr-client.key` |
|
||||
|
||||
## 如何判断 Client 正常
|
||||
|
||||
- `rtr_debug_client`: 日志中能看到 Reset Query 成功、EndOfData 和 payload 统计。
|
||||
- FRR: `show rpki cache-connection` 显示已连接,`show rpki prefix-table` 有 VRP 条目。
|
||||
- BIRD: 日志中 RPKI protocol 状态为 `up`,ROA/ASPA 表项摘要不为空。
|
||||
|
||||
如果 client 无法连接,优先检查:
|
||||
|
||||
- server 是否按对应 transport 启动。
|
||||
- client 配置的 server 地址是否从当前容器或主机可达。
|
||||
- TLS 的 server name 是否匹配证书 SAN。
|
||||
- SSH 的用户名、client key 和 server public key 是否匹配。
|
||||
- `RPKI_RTR_PROTOCOL_VERSION` 或 BIRD 配置中的 `min version`/`max version` 是否和 server 能力一致。
|
||||
377
README.md
377
README.md
@@ -1,373 +1,28 @@
|
||||
# RPKI RTR Server
|
||||
|
||||
这是一个用 Rust 实现的 RPKI-to-Router (RTR) cache server。服务端从 CCR 文件中读取 RPKI 数据,生成 RTR cache snapshot 和 delta,并通过 RTR 协议提供给路由器或调试客户端。
|
||||
|
||||
当前代码重点覆盖:
|
||||
# 单元测试
|
||||
|
||||
- RTR server:TCP 默认启用,可选启用 TLS/mTLS 和进程内 SSH transport。
|
||||
- 数据输入:从 CCR 目录选择最新 `.ccr` 文件,解析 VRP 和 VAP/ASPA。
|
||||
- 本地策略:可选加载 SLURM 文件,对 CCR 解析出的 payload 做过滤和本地断言。
|
||||
- 持久化:使用 RocksDB 保存 RTR cache 状态。
|
||||
- 运行报告:输出 source、client、runtime 三类 JSON report。
|
||||
- 运行期管理:可选启用 HTTP Admin API,修改部分 runtime config、触发 source reload、管理 SLURM 文件。
|
||||
- 调试工具:`rtr_debug_client` 支持 TCP、TLS 和 SSH 连接 RTR server。
|
||||
```
|
||||
cargo test
|
||||
|
||||
## 目录
|
||||
|
||||
- [协议参考](#协议参考)
|
||||
- [项目结构](#项目结构)
|
||||
- [构建与测试](#构建与测试)
|
||||
- [数据输入](#数据输入)
|
||||
- [快速启动](#快速启动)
|
||||
- [前置数据生成](#前置数据生成)
|
||||
- [Docker 运行](#docker-运行)
|
||||
- [跨架构构建](#跨架构构建)
|
||||
- [传输模式](#传输模式)
|
||||
- [主要环境变量](#主要环境变量)
|
||||
- [运行报告](#运行报告)
|
||||
- [Admin API](#admin-api)
|
||||
- [调试客户端](#调试客户端)
|
||||
- [部署入口](#部署入口)
|
||||
- [开发说明](#开发说明)
|
||||
|
||||
## 协议参考
|
||||
|
||||
- RTR: [RFC 6810](https://www.rfc-editor.org/rfc/rfc6810.html), [RFC 8210](https://www.rfc-editor.org/rfc/rfc8210.html)
|
||||
- SLURM: [RFC 8416](https://www.rfc-editor.org/rfc/rfc8416.html)
|
||||
- CCR: [draft-ietf-sidrops-rpki-ccr](https://www.ietf.org/archive/id/draft-ietf-sidrops-rpki-ccr-02.html)
|
||||
|
||||
## 项目结构
|
||||
|
||||
| 路径 | 说明 |
|
||||
| --- | --- |
|
||||
| `src/main_rtr.rs` | RTR server 入口 |
|
||||
| `src/rtr/` | RTR 协议、cache、store、server transport、admin、report |
|
||||
| `src/source/` | CCR/SLURM source 加载流程 |
|
||||
| `src/slurm/` | SLURM 文件解析、合并和策略应用 |
|
||||
| `src/data_model/` | RPKI 相关数据结构 |
|
||||
| `src/bin/rtr_debug_client/` | RTR 调试客户端 |
|
||||
| `deploy/server/` | server Docker 镜像和 compose |
|
||||
| `deploy/client/` | debug client Docker 镜像和 compose |
|
||||
| `deploy/frr/`, `deploy/bird/` | FRR/BIRD 作为 RTR client 的测试部署 |
|
||||
| `tests/` | 单元和集成测试 |
|
||||
|
||||
## 构建与测试
|
||||
|
||||
```bash
|
||||
cargo build --bin rpki_rtr
|
||||
cargo build --bin rtr_debug_client
|
||||
cargo test
|
||||
# 查看输出
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
服务端 binary 名称是 `rpki_rtr`:
|
||||
# 覆盖率(cargo-llvm-cov)
|
||||
|
||||
```bash
|
||||
cargo run --bin rpki_rtr
|
||||
安装工具:
|
||||
|
||||
```
|
||||
rustup component add llvm-tools-preview
|
||||
cargo install cargo-llvm-cov --locked
|
||||
```
|
||||
|
||||
默认配置会监听 `0.0.0.0:323`,从 `./data` 查找 CCR 文件,并使用 `./rtr-db` 保存 RocksDB 数据。启动前需要确保 CCR 目录里存在可解析的 `.ccr` 文件。
|
||||
统计行覆盖率并要求 >=90%:
|
||||
|
||||
## 数据输入
|
||||
|
||||
服务端启动和刷新时会读取 `RPKI_RTR_CCR_DIR` 指向的目录:
|
||||
|
||||
1. 如果该目录下存在包含 `.ccr` 文件的子目录,先按子目录名排序选择最新子目录。
|
||||
2. 在选中的目录中按文件名排序选择最新 `.ccr` 文件。
|
||||
3. 从 CCR 中解析 VRP 和 VAP/ASPA。
|
||||
4. 如果配置了 `RPKI_RTR_SLURM_DIR`,加载其中启用的 `*.slurm` 文件并应用本地策略。
|
||||
|
||||
## 快速启动
|
||||
|
||||
### 前置数据生成
|
||||
|
||||
TLS/mTLS 或 SSH transport 需要本地证书和密钥。开发和 Docker 联调可用脚本生成 compose 默认引用的最小文件集:
|
||||
|
||||
```bash
|
||||
bash scripts/10_generate-certs.sh
|
||||
```
|
||||
./scripts/coverage.sh
|
||||
# 或
|
||||
cargo llvm-cov --fail-under-lines 90
|
||||
```
|
||||
|
||||
如需覆盖已有 `certs/` 内容:
|
||||
|
||||
```bash
|
||||
bash scripts/10_generate-certs.sh --force
|
||||
```
|
||||
|
||||
`10_generate-certs.sh` 常用参数:
|
||||
|
||||
| 参数 | 说明 |
|
||||
| --- | --- |
|
||||
| `--out-dir DIR` | 指定输出目录,默认生成到 `./certs`。 |
|
||||
| `--server-dns NAME` | 指定 TLS server 证书的 dNSName SAN,默认 `localhost`。 |
|
||||
| `--tls-only` | 只生成 `certs/tls` 下的 TLS 证书和私钥。 |
|
||||
| `--ssh-only` | 只生成 `certs/ssh` 下的 SSH host key、client key 和 `authorized_keys`。 |
|
||||
| `--force` | 覆盖已有文件。未指定时,如果目标文件已存在会直接报错退出。 |
|
||||
| `-h`, `--help` | 查看脚本帮助。 |
|
||||
|
||||
TLS server name 默认是 `localhost`。如果需要其它 DNS 名称,需要同时设置 `scripts/10_generate-certs.sh --server-dns <name>` 和 client 的 `RPKI_RTR_TLS_SERVER_NAME=<name>`。
|
||||
|
||||
### Docker 运行
|
||||
|
||||
推荐使用 `deploy/server/build-run.sh` 和 `deploy/client/build-run.sh` 分别管理 server 与 debug client:
|
||||
以下命令默认从仓库根目录执行。
|
||||
|
||||
```bash
|
||||
chmod +x deploy/server/build-run.sh deploy/client/build-run.sh
|
||||
./deploy/server/build-run.sh up --mode tcp
|
||||
./deploy/client/build-run.sh up --mode tcp
|
||||
./deploy/client/build-run.sh down
|
||||
./deploy/server/build-run.sh down
|
||||
```
|
||||
|
||||
脚本常用参数:
|
||||
|
||||
| 参数/变量 | server | client | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `--mode base|tcp|tls|ssh`, `MODE` | yes | yes | 选择 compose 模式,默认 `tcp`。 |
|
||||
| `--clients single|multi`, `CLIENTS` | no | yes | 单 client 或 5 client,默认 `single`。 |
|
||||
| `--no-build`, `BUILD=0` | yes | yes | 只运行已有镜像,不执行 compose build。 |
|
||||
| `--server-image TAG`, `SERVER_IMAGE` | yes | no | server 镜像名;会传给 compose 的 `RPKI_RTR_SERVER_IMAGE`。 |
|
||||
| `--client-image TAG`, `CLIENT_IMAGE` | no | yes | client 镜像名;会传给 compose 的 `RPKI_RTR_CLIENT_IMAGE`。 |
|
||||
| `--platform VALUE`, `TARGET_PLATFORM` | yes | yes | `buildx` 目标平台,默认 `linux/arm64`。 |
|
||||
| `--push`, `BUILDX_PUSH=1` | yes | yes | `buildx` 构建后推送到 registry;不设置时加载到本地 Docker。 |
|
||||
| `--skip-base-pull`, `SKIP_BASE_IMAGE_PULL=1` | yes | yes | 跳过 buildx 前的基础镜像预拉取。 |
|
||||
|
||||
支持的 action 包括 `up`、`build`、`buildx`、`rebuild`、`down`/`stop`、`restart`、`logs`、`ps`。完整帮助见:
|
||||
|
||||
```bash
|
||||
./deploy/server/build-run.sh --help
|
||||
./deploy/client/build-run.sh --help
|
||||
```
|
||||
|
||||
### 跨架构构建
|
||||
示例:x86 开发机本地构建 ARM 镜像,打包后传到 ARM 部署机。
|
||||
|
||||
```bash
|
||||
./deploy/server/build-run.sh buildx \
|
||||
--platform linux/arm64 \
|
||||
--server-image rpki-rtr:arm64
|
||||
|
||||
./deploy/client/build-run.sh buildx \
|
||||
--platform linux/arm64 \
|
||||
--client-image rpki-rtr-debug-client:arm64
|
||||
|
||||
mkdir -p dist/images
|
||||
docker save rpki-rtr:arm64 -o dist/images/rpki-rtr-arm64.tar
|
||||
docker save rpki-rtr-debug-client:arm64 -o dist/images/rpki-rtr-debug-client-arm64.tar
|
||||
|
||||
scp dist/images/*.tar user@arm-host:/tmp/
|
||||
```
|
||||
|
||||
ARM 部署机加载镜像并运行:
|
||||
|
||||
```bash
|
||||
docker load -i /tmp/rpki-rtr-arm64.tar
|
||||
docker load -i /tmp/rpki-rtr-debug-client-arm64.tar
|
||||
|
||||
SERVER_IMAGE=rpki-rtr:arm64 \
|
||||
BUILD=0 ./deploy/server/build-run.sh up --mode tcp
|
||||
|
||||
CLIENT_IMAGE=rpki-rtr-debug-client:arm64 \
|
||||
BUILD=0 ./deploy/client/build-run.sh up --mode tcp
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.yml up -d --build
|
||||
docker compose -f deploy/server/docker-compose.yml down
|
||||
```
|
||||
|
||||
TLS/mTLS 示例:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.tls.yml up -d --build
|
||||
docker compose -f deploy/server/docker-compose.tls.yml down
|
||||
```
|
||||
|
||||
## 传输模式
|
||||
|
||||
TCP 默认启用。TLS 和 SSH 是附加监听,不会替代 TCP。
|
||||
|
||||
| 模式 | 默认地址 | 启用方式 |
|
||||
| --- | --- | --- |
|
||||
| TCP | `0.0.0.0:323` | 默认启用 |
|
||||
| TLS/mTLS | `0.0.0.0:324` | `RPKI_RTR_ENABLE_TLS=true` |
|
||||
| SSH | `0.0.0.0:22` | `RPKI_RTR_ENABLE_SSH=true` |
|
||||
|
||||
TLS server 会加载服务端证书、私钥和 client CA。SSH server 使用 OpenSSH host key 和 authorized_keys 文件;`RPKI_RTR_SSH_AUTH_MODE` 支持 `key`、`password`、`both`。
|
||||
|
||||
## 主要环境变量
|
||||
|
||||
布尔值支持 `true/false`、`1/0`、`yes/no`、`on/off`。
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `RPKI_RTR_ENABLE_TLS` | `false` | 是否启用 TLS/mTLS 监听 |
|
||||
| `RPKI_RTR_ENABLE_SSH` | `false` | 是否启用 SSH 监听 |
|
||||
| `RPKI_RTR_TCP_ADDR` | `0.0.0.0:323` | TCP 监听地址 |
|
||||
| `RPKI_RTR_TLS_ADDR` | `0.0.0.0:324` | TLS 监听地址 |
|
||||
| `RPKI_RTR_SSH_ADDR` | `0.0.0.0:22` | SSH 监听地址 |
|
||||
| `RPKI_RTR_SSH_PORT` | 未设置 | 仅覆盖 `RPKI_RTR_SSH_ADDR` 中的端口 |
|
||||
| `RPKI_RTR_DB_PATH` | `./rtr-db` | RocksDB 路径 |
|
||||
| `RPKI_RTR_CCR_DIR` | `./data` | CCR 输入目录 |
|
||||
| `RPKI_RTR_SLURM_DIR` | 未设置 | SLURM 目录;空值表示禁用 |
|
||||
| `RPKI_RTR_REPORT_DIR` | `./report` | JSON report 输出目录 |
|
||||
| `RPKI_RTR_MAX_DELTA` | `100` | 每个 RTR 版本保留的最大 delta 数,必须大于 0 |
|
||||
| `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE` | `false` | 是否按 snapshot wire size 继续裁剪 delta window |
|
||||
| `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS` | `300` | source 刷新间隔,单位秒 |
|
||||
| `RPKI_RTR_REFRESH_INTERVAL_SECS` | 未设置 | 旧变量名,仍兼容;建议使用 `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS` |
|
||||
| `RPKI_RTR_TIMING_REFRESH_SECS` | `3600` | RTR EndOfData `refresh` 字段 |
|
||||
| `RPKI_RTR_TIMING_RETRY_SECS` | `600` | RTR EndOfData `retry` 字段 |
|
||||
| `RPKI_RTR_TIMING_EXPIRE_SECS` | `7200` | RTR EndOfData `expire` 字段,必须大于 refresh 和 retry |
|
||||
| `RPKI_RTR_MAX_CONNECTIONS` | `512` | 最大并发 RTR 连接数 |
|
||||
| `RPKI_RTR_MAX_CONCURRENT_HANDSHAKES` | `128` | 最大并发握手数,不能大于最大连接数 |
|
||||
| `RPKI_RTR_NOTIFY_QUEUE_SIZE` | `1024` | Serial Notify 广播队列大小 |
|
||||
| `RPKI_RTR_TCP_KEEPALIVE_SECS` | `60` | TCP keepalive;设为 `0` 表示禁用 |
|
||||
| `RPKI_RTR_WARN_INSECURE_TCP` | `true` | TCP 模式是否输出安全提示 |
|
||||
| `RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN` | `false` | 是否要求 TLS 服务端证书包含 dNSName SAN |
|
||||
| `RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH` | `true` | 是否校验 TLS client 证书 SAN IP 与 peer IP 匹配 |
|
||||
| `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS` | `300` | runtime report 周期 |
|
||||
| `RPKI_RTR_REPORT_HISTORY_LIMIT` | `10` | 每类 report 滚动保留数量 |
|
||||
| `RPKI_RTR_TIMEZONE` | `Asia/Shanghai` | 日志和 report 使用的时区 |
|
||||
| `RPKI_RTR_ADMIN_ADDR` | 未设置 | Admin API 监听地址;未设置时关闭 |
|
||||
| `RPKI_RTR_ADMIN_TOKEN` | 未设置 | Admin API Bearer token;非 loopback 监听时必须设置 |
|
||||
|
||||
TLS 相关路径:
|
||||
|
||||
| 变量 | 默认值 |
|
||||
| --- | --- |
|
||||
| `RPKI_RTR_TLS_CERT_PATH` | `./certs/tls/server-dns.crt` |
|
||||
| `RPKI_RTR_TLS_KEY_PATH` | `./certs/tls/server-dns.key` |
|
||||
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | `./certs/tls/client-ca.crt` |
|
||||
|
||||
SSH 相关配置:
|
||||
|
||||
| 变量 | 默认值 |
|
||||
| --- | --- |
|
||||
| `RPKI_RTR_SSH_HOST_KEY_PATH` | `./certs/ssh/ssh_host_rsa_key` |
|
||||
| `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH` | `./certs/ssh/rtr-authorized_keys` |
|
||||
| `RPKI_RTR_SSH_USERNAME` | `rpki-rtr` |
|
||||
| `RPKI_RTR_SSH_SUBSYSTEM_NAME` | `rpki-rtr` |
|
||||
| `RPKI_RTR_SSH_AUTH_MODE` | `key` |
|
||||
| `RPKI_RTR_SSH_PASSWORD` | 未设置 |
|
||||
|
||||
## 运行报告
|
||||
|
||||
服务会在 `RPKI_RTR_REPORT_DIR` 下写入 JSON report,并按 `RPKI_RTR_REPORT_HISTORY_LIMIT` 滚动保留:
|
||||
|
||||
- `rtr-source-*.json`:CCR/SLURM source、fingerprint、刷新状态、数据质量、cache 统计。
|
||||
- `rtr-clients-*.json`:client 连接数和连接方式统计。
|
||||
- `rtr-runtime-*.json`:进程状态、服务状态和当前生效的 runtime config。
|
||||
|
||||
## Admin API
|
||||
|
||||
Admin API 默认关闭。设置 `RPKI_RTR_ADMIN_ADDR` 后启用:
|
||||
|
||||
```bash
|
||||
export RPKI_RTR_ADMIN_ADDR=127.0.0.1:8323
|
||||
export RPKI_RTR_ADMIN_TOKEN=change-me
|
||||
```
|
||||
|
||||
如果监听非 loopback 地址,例如 `0.0.0.0:8323`,必须设置 `RPKI_RTR_ADMIN_TOKEN`。设置 token 后,请求需要携带:
|
||||
|
||||
```http
|
||||
Authorization: Bearer change-me
|
||||
```
|
||||
|
||||
当前实现的主要接口:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `GET` | `/admin/rtr/health` | 健康检查和能力开关 |
|
||||
| `GET` | `/admin/rtr/config` | 查看 runtime config |
|
||||
| `POST` | `/admin/rtr/config` | 修改部分 runtime config |
|
||||
| `GET` | `/admin/rtr/logs/tail` | 读取 stdout/stderr 日志 tail |
|
||||
| `POST` | `/admin/rtr/slurm/reload` | 触发 source reload |
|
||||
| `GET` | `/admin/rtr/slurm/files` | 列出 SLURM 文件 |
|
||||
| `POST` | `/admin/rtr/slurm/files` | 新增或覆盖 SLURM 文件 |
|
||||
| `GET` | `/admin/rtr/slurm/files/{name}` | 读取 SLURM 文件 |
|
||||
| `PUT` | `/admin/rtr/slurm/files/{name}` | 新增或覆盖指定 SLURM 文件 |
|
||||
| `DELETE` | `/admin/rtr/slurm/files/{name}` | 删除 SLURM 文件 |
|
||||
| `POST` | `/admin/rtr/slurm/files/{name}/enable` | 启用 `.slurm.disabled` 文件 |
|
||||
| `POST` | `/admin/rtr/slurm/files/{name}/disable` | 禁用 `.slurm` 文件 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8323/admin/rtr/config \
|
||||
-H "Authorization: Bearer change-me"
|
||||
|
||||
curl -X POST http://127.0.0.1:8323/admin/rtr/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer change-me" \
|
||||
-d '{"max_delta": 6, "source_refresh_interval_seconds": 60}'
|
||||
```
|
||||
|
||||
## 调试客户端
|
||||
|
||||
`rtr_debug_client` 用于手动发起 Reset Query 或 Serial Query,并观察服务端返回的 PDU。
|
||||
|
||||
RTR Client 的 Docker 调试、FRR/BIRD 互通验证和常用排障步骤见 `README.client.md`。
|
||||
|
||||
TCP:
|
||||
|
||||
```bash
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
||||
```
|
||||
|
||||
TLS:
|
||||
|
||||
```bash
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:324 1 reset \
|
||||
--tls \
|
||||
--ca-cert certs/tls/client-ca.crt \
|
||||
--server-name localhost \
|
||||
--client-cert certs/tls/client-good.crt \
|
||||
--client-key certs/tls/client-good.key
|
||||
```
|
||||
|
||||
SSH:
|
||||
|
||||
```bash
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:22 1 reset \
|
||||
--ssh \
|
||||
--ssh-user rpki-rtr \
|
||||
--ssh-key certs/ssh/rtr-client.key \
|
||||
--ssh-server-key certs/ssh/ssh_host_rsa_key.pub
|
||||
```
|
||||
|
||||
SSH password auth 也已实现;SSH 模式下 `--ssh-key` 和 `--ssh-password` 二选一,同时必须通过 `--ssh-known-hosts` 或 `--ssh-server-key` 做 host key 校验。
|
||||
|
||||
更多参数见 `src/bin/rtr_debug_client/README.md`。
|
||||
|
||||
## 部署入口
|
||||
|
||||
| 用途 | Compose 文件 |
|
||||
| --- | --- |
|
||||
| Server | `deploy/server/docker-compose.yml` |
|
||||
| Server TCP 示例 | `deploy/server/docker-compose.tcp.yml` |
|
||||
| Server TLS 示例 | `deploy/server/docker-compose.tls.yml` |
|
||||
| Server SSH 示例 | `deploy/server/docker-compose.ssh.yml` |
|
||||
| Debug client | `deploy/client/docker-compose.yml` |
|
||||
| Debug client 多实例 | `deploy/client/docker-compose.clients.yml` |
|
||||
| FRR client | `deploy/frr/docker-compose.yml` |
|
||||
| BIRD client | `deploy/bird/docker-compose.yml` |
|
||||
|
||||
RTR Client 统一使用说明见 `README.client.md`。
|
||||
|
||||
通用操作:
|
||||
|
||||
```bash
|
||||
COMPOSE_FILE=deploy/server/docker-compose.yml
|
||||
docker compose -f "${COMPOSE_FILE}" up -d --build
|
||||
docker compose -f "${COMPOSE_FILE}" ps
|
||||
docker compose -f "${COMPOSE_FILE}" down
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
- 代码中的 RTR cache 按协议版本维护状态;v0 只包含 Route Origin,v1 包含 Route Origin 和 Router Key,v2 包含 Route Origin、Router Key 和 ASPA。
|
||||
- 当前 source pipeline 从 CCR 生成 Route Origin 和 ASPA,再可选应用 SLURM。
|
||||
- `RtrStore::save_cache_state_versioned(...)` 是 cache 状态写入 RocksDB 的核心入口,相关边界测试在 `tests/test_store_boundary.rs`。
|
||||
- Admin API 的日志 tail 依赖部署入口写出的 stdout/stderr 日志文件;容器入口脚本会把日志写到 `/app/logs`。
|
||||
|
||||
8
benchmark/ours_manifest_bench/Cargo.toml
Normal file
8
benchmark/ours_manifest_bench/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "ours-manifest-bench"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
rpki = { path = "../..", default-features = false }
|
||||
|
||||
145
benchmark/ours_manifest_bench/src/main.rs
Normal file
145
benchmark/ours_manifest_bench/src/main.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use rpki::data_model::manifest::ManifestObject;
|
||||
use std::hint::black_box;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Config {
|
||||
sample: Option<String>,
|
||||
manifest_path: Option<PathBuf>,
|
||||
iterations: u64,
|
||||
warmup_iterations: u64,
|
||||
repeats: u32,
|
||||
}
|
||||
|
||||
fn usage_and_exit() -> ! {
|
||||
eprintln!(
|
||||
"Usage:\n ours-manifest-bench (--sample <name> | --manifest <path>) [--iterations N] [--warmup-iterations N] [--repeats N]\n\nExamples:\n cargo run --release -- --sample small-01 --iterations 20000 --warmup-iterations 2000 --repeats 3\n cargo run --release -- --manifest ../../tests/benchmark/selected_der/small-01.mft"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
fn parse_args() -> Config {
|
||||
let mut sample: Option<String> = None;
|
||||
let mut manifest_path: Option<PathBuf> = None;
|
||||
let mut iterations: u64 = 20_000;
|
||||
let mut warmup_iterations: u64 = 2_000;
|
||||
let mut repeats: u32 = 3;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--sample" => sample = Some(args.next().unwrap_or_else(|| usage_and_exit())),
|
||||
"--manifest" => {
|
||||
manifest_path = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit())))
|
||||
}
|
||||
"--iterations" => {
|
||||
iterations = args
|
||||
.next()
|
||||
.unwrap_or_else(|| usage_and_exit())
|
||||
.parse()
|
||||
.unwrap_or_else(|_| usage_and_exit())
|
||||
}
|
||||
"--warmup-iterations" => {
|
||||
warmup_iterations = args
|
||||
.next()
|
||||
.unwrap_or_else(|| usage_and_exit())
|
||||
.parse()
|
||||
.unwrap_or_else(|_| usage_and_exit())
|
||||
}
|
||||
"--repeats" => {
|
||||
repeats = args
|
||||
.next()
|
||||
.unwrap_or_else(|| usage_and_exit())
|
||||
.parse()
|
||||
.unwrap_or_else(|_| usage_and_exit())
|
||||
}
|
||||
"-h" | "--help" => usage_and_exit(),
|
||||
_ => usage_and_exit(),
|
||||
}
|
||||
}
|
||||
|
||||
if sample.is_none() && manifest_path.is_none() {
|
||||
usage_and_exit();
|
||||
}
|
||||
if sample.is_some() && manifest_path.is_some() {
|
||||
usage_and_exit();
|
||||
}
|
||||
|
||||
Config {
|
||||
sample,
|
||||
manifest_path,
|
||||
iterations,
|
||||
warmup_iterations,
|
||||
repeats,
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_manifest_path(sample: &str) -> PathBuf {
|
||||
// Assumes current working directory is `rpki/benchmark/ours_manifest_bench`.
|
||||
PathBuf::from(format!("../../tests/benchmark/selected_der/{sample}.mft"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cfg = parse_args();
|
||||
let manifest_path = cfg
|
||||
.manifest_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| derive_manifest_path(cfg.sample.as_deref().unwrap()));
|
||||
|
||||
let bytes = std::fs::read(&manifest_path).unwrap_or_else(|e| {
|
||||
eprintln!("read manifest fixture failed: {e}; path={}", manifest_path.display());
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let decoded_once = ManifestObject::decode_der(&bytes).unwrap_or_else(|e| {
|
||||
eprintln!("decode failed: {e}; path={}", manifest_path.display());
|
||||
std::process::exit(1);
|
||||
});
|
||||
let file_count = decoded_once.manifest.file_count();
|
||||
|
||||
let mut round_ns_per_op: Vec<f64> = Vec::with_capacity(cfg.repeats as usize);
|
||||
let mut round_ops_per_s: Vec<f64> = Vec::with_capacity(cfg.repeats as usize);
|
||||
|
||||
for _round in 0..cfg.repeats {
|
||||
for _ in 0..cfg.warmup_iterations {
|
||||
let obj = ManifestObject::decode_der(black_box(&bytes)).expect("warmup decode");
|
||||
black_box(obj);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
for _ in 0..cfg.iterations {
|
||||
let obj = ManifestObject::decode_der(black_box(&bytes)).expect("timed decode");
|
||||
black_box(obj);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let ns_per_op = (elapsed.as_secs_f64() * 1e9) / (cfg.iterations as f64);
|
||||
let ops_per_s = (cfg.iterations as f64) / elapsed.as_secs_f64();
|
||||
round_ns_per_op.push(ns_per_op);
|
||||
round_ops_per_s.push(ops_per_s);
|
||||
}
|
||||
|
||||
let avg_ns_per_op = round_ns_per_op.iter().sum::<f64>() / (round_ns_per_op.len() as f64);
|
||||
let avg_ops_per_s = round_ops_per_s.iter().sum::<f64>() / (round_ops_per_s.len() as f64);
|
||||
|
||||
let sample_name = cfg.sample.clone().unwrap_or_else(|| {
|
||||
manifest_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| manifest_path.display().to_string())
|
||||
});
|
||||
let sample_name = sample_name
|
||||
.strip_suffix(".mft")
|
||||
.unwrap_or(&sample_name)
|
||||
.to_string();
|
||||
|
||||
println!("fixture: {}", manifest_path.display());
|
||||
println!();
|
||||
println!("| sample | avg ns/op | ops/s | file count |");
|
||||
println!("|---|---:|---:|---:|");
|
||||
println!(
|
||||
"| {} | {:.2} | {:.2} | {} |",
|
||||
sample_name, avg_ns_per_op, avg_ops_per_s, file_count
|
||||
);
|
||||
}
|
||||
8
benchmark/routinator_object_bench/Cargo.toml
Normal file
8
benchmark/routinator_object_bench/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "routinator-object-bench"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
rpki = { version = "=0.19.1", features = ["repository"] }
|
||||
552
benchmark/routinator_object_bench/src/main.rs
Normal file
552
benchmark/routinator_object_bench/src/main.rs
Normal file
@@ -0,0 +1,552 @@
|
||||
use rpki::repository::cert::Cert;
|
||||
use rpki::repository::crl::Crl;
|
||||
use rpki::repository::manifest::Manifest;
|
||||
use rpki::repository::roa::Roa;
|
||||
use rpki::repository::aspa::Aspa;
|
||||
use rpki::repository::resources::{AsResources, IpResources};
|
||||
|
||||
use std::hint::black_box;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum ObjType {
|
||||
Cer,
|
||||
Crl,
|
||||
Manifest,
|
||||
Roa,
|
||||
Aspa,
|
||||
}
|
||||
|
||||
impl ObjType {
|
||||
fn parse(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"cer" => Ok(Self::Cer),
|
||||
"crl" => Ok(Self::Crl),
|
||||
"manifest" => Ok(Self::Manifest),
|
||||
"roa" => Ok(Self::Roa),
|
||||
"aspa" => Ok(Self::Aspa),
|
||||
_ => Err("type must be one of: cer, crl, manifest, roa, aspa".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ObjType::Cer => "cer",
|
||||
ObjType::Crl => "crl",
|
||||
ObjType::Manifest => "manifest",
|
||||
ObjType::Roa => "roa",
|
||||
ObjType::Aspa => "aspa",
|
||||
}
|
||||
}
|
||||
|
||||
fn ext(self) -> &'static str {
|
||||
match self {
|
||||
ObjType::Cer => "cer",
|
||||
ObjType::Crl => "crl",
|
||||
ObjType::Manifest => "mft",
|
||||
ObjType::Roa => "roa",
|
||||
ObjType::Aspa => "asa",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Sample {
|
||||
obj_type: ObjType,
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Config {
|
||||
dir: PathBuf,
|
||||
type_filter: Option<ObjType>,
|
||||
sample_filter: Option<String>,
|
||||
fixed_iters: Option<u64>,
|
||||
warmup_iters: u64,
|
||||
rounds: u64,
|
||||
min_round_ms: u64,
|
||||
max_adaptive_iters: u64,
|
||||
strict: bool,
|
||||
cert_inspect: bool,
|
||||
out_csv: Option<PathBuf>,
|
||||
out_md: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn usage_and_exit(err: Option<&str>) -> ! {
|
||||
if let Some(err) = err {
|
||||
eprintln!("error: {err}");
|
||||
eprintln!();
|
||||
}
|
||||
eprintln!(
|
||||
"Usage:\n\
|
||||
cargo run --release --manifest-path rpki/benchmark/routinator_object_bench/Cargo.toml -- [OPTIONS]\n\
|
||||
\n\
|
||||
Options:\n\
|
||||
--dir <PATH> Fixtures root dir (default: ../../tests/benchmark/selected_der_v2)\n\
|
||||
--type <cer|crl|manifest|roa|aspa> Filter by type\n\
|
||||
--sample <NAME> Filter by sample name (e.g. p50)\n\
|
||||
--iters <N> Fixed iterations per round (optional; otherwise adaptive)\n\
|
||||
--warmup-iters <N> Warmup iterations (default: 50)\n\
|
||||
--rounds <N> Rounds (default: 5)\n\
|
||||
--min-round-ms <MS> Adaptive: minimum round time (default: 200)\n\
|
||||
--max-iters <N> Adaptive: maximum iters (default: 1_000_000)\n\
|
||||
--strict <true|false> Strict DER where applicable (default: true)\n\
|
||||
--cert-inspect Also run Cert::inspect_ca/inspect_ee where applicable (default: false)\n\
|
||||
--out-csv <PATH> Write CSV output\n\
|
||||
--out-md <PATH> Write Markdown output\n\
|
||||
"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
fn parse_bool(s: &str, name: &str) -> bool {
|
||||
match s {
|
||||
"1" | "true" | "TRUE" | "yes" | "YES" => true,
|
||||
"0" | "false" | "FALSE" | "no" | "NO" => false,
|
||||
_ => usage_and_exit(Some(&format!("{name} must be true/false"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u64(s: &str, name: &str) -> u64 {
|
||||
s.parse::<u64>()
|
||||
.unwrap_or_else(|_| usage_and_exit(Some(&format!("{name} must be an integer"))))
|
||||
}
|
||||
|
||||
fn default_samples_dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../tests/benchmark/selected_der_v2")
|
||||
}
|
||||
|
||||
fn parse_args() -> Config {
|
||||
let mut dir: PathBuf = default_samples_dir();
|
||||
let mut type_filter: Option<ObjType> = None;
|
||||
let mut sample_filter: Option<String> = None;
|
||||
let mut fixed_iters: Option<u64> = None;
|
||||
let mut warmup_iters: u64 = 50;
|
||||
let mut rounds: u64 = 5;
|
||||
let mut min_round_ms: u64 = 200;
|
||||
let mut max_adaptive_iters: u64 = 1_000_000;
|
||||
let mut strict: bool = true;
|
||||
let mut cert_inspect: bool = false;
|
||||
let mut out_csv: Option<PathBuf> = None;
|
||||
let mut out_md: Option<PathBuf> = None;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--dir" => dir = PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None))),
|
||||
"--type" => {
|
||||
type_filter = Some(ObjType::parse(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
)
|
||||
.unwrap_or_else(|e| usage_and_exit(Some(&e))))
|
||||
}
|
||||
"--sample" => {
|
||||
sample_filter = Some(args.next().unwrap_or_else(|| usage_and_exit(None)))
|
||||
}
|
||||
"--iters" => {
|
||||
fixed_iters = Some(parse_u64(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
"--iters",
|
||||
))
|
||||
}
|
||||
"--warmup-iters" => {
|
||||
warmup_iters = parse_u64(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
"--warmup-iters",
|
||||
)
|
||||
}
|
||||
"--rounds" => {
|
||||
rounds = parse_u64(&args.next().unwrap_or_else(|| usage_and_exit(None)), "--rounds")
|
||||
}
|
||||
"--min-round-ms" => {
|
||||
min_round_ms = parse_u64(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
"--min-round-ms",
|
||||
)
|
||||
}
|
||||
"--max-iters" => {
|
||||
max_adaptive_iters = parse_u64(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
"--max-iters",
|
||||
)
|
||||
}
|
||||
"--strict" => {
|
||||
strict = parse_bool(
|
||||
&args.next().unwrap_or_else(|| usage_and_exit(None)),
|
||||
"--strict",
|
||||
)
|
||||
}
|
||||
"--cert-inspect" => cert_inspect = true,
|
||||
"--out-csv" => out_csv = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))),
|
||||
"--out-md" => out_md = Some(PathBuf::from(args.next().unwrap_or_else(|| usage_and_exit(None)))),
|
||||
"-h" | "--help" => usage_and_exit(None),
|
||||
_ => usage_and_exit(Some(&format!("unknown argument: {arg}"))),
|
||||
}
|
||||
}
|
||||
|
||||
if warmup_iters == 0 {
|
||||
usage_and_exit(Some("--warmup-iters must be > 0"));
|
||||
}
|
||||
if rounds == 0 {
|
||||
usage_and_exit(Some("--rounds must be > 0"));
|
||||
}
|
||||
if min_round_ms == 0 {
|
||||
usage_and_exit(Some("--min-round-ms must be > 0"));
|
||||
}
|
||||
if max_adaptive_iters == 0 {
|
||||
usage_and_exit(Some("--max-iters must be > 0"));
|
||||
}
|
||||
if let Some(n) = fixed_iters {
|
||||
if n == 0 {
|
||||
usage_and_exit(Some("--iters must be > 0"));
|
||||
}
|
||||
}
|
||||
|
||||
Config {
|
||||
dir,
|
||||
type_filter,
|
||||
sample_filter,
|
||||
fixed_iters,
|
||||
warmup_iters,
|
||||
rounds,
|
||||
min_round_ms,
|
||||
max_adaptive_iters,
|
||||
strict,
|
||||
cert_inspect,
|
||||
out_csv,
|
||||
out_md,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_samples(root: &Path) -> Vec<Sample> {
|
||||
let mut out = Vec::new();
|
||||
for obj_type in [
|
||||
ObjType::Cer,
|
||||
ObjType::Crl,
|
||||
ObjType::Manifest,
|
||||
ObjType::Roa,
|
||||
ObjType::Aspa,
|
||||
] {
|
||||
let dir = root.join(obj_type.as_str());
|
||||
let rd = match std::fs::read_dir(&dir) {
|
||||
Ok(rd) => rd,
|
||||
Err(_) => continue,
|
||||
};
|
||||
for ent in rd.flatten() {
|
||||
let path = ent.path();
|
||||
if path.extension().and_then(|s| s.to_str()) != Some(obj_type.ext()) {
|
||||
continue;
|
||||
}
|
||||
let name = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
out.push(Sample { obj_type, name, path });
|
||||
}
|
||||
}
|
||||
out.sort_by(|a, b| a.obj_type.cmp(&b.obj_type).then_with(|| a.name.cmp(&b.name)));
|
||||
out
|
||||
}
|
||||
|
||||
fn choose_iters_adaptive<F: FnMut()>(mut op: F, min_round_ms: u64, max_iters: u64) -> u64 {
|
||||
let min_secs = (min_round_ms as f64) / 1e3;
|
||||
let mut iters: u64 = 1;
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
op();
|
||||
}
|
||||
let elapsed = start.elapsed().as_secs_f64();
|
||||
if elapsed >= min_secs {
|
||||
return iters;
|
||||
}
|
||||
if iters >= max_iters {
|
||||
return iters;
|
||||
}
|
||||
iters = (iters.saturating_mul(2)).min(max_iters);
|
||||
}
|
||||
}
|
||||
|
||||
fn count_ip(res: &IpResources) -> u64 {
|
||||
if res.is_inherited() {
|
||||
return 1;
|
||||
}
|
||||
let Ok(blocks) = res.to_blocks() else {
|
||||
return 0;
|
||||
};
|
||||
blocks.iter().count() as u64
|
||||
}
|
||||
|
||||
fn count_as(res: &AsResources) -> u64 {
|
||||
if res.is_inherited() {
|
||||
return 1;
|
||||
}
|
||||
let Ok(blocks) = res.to_blocks() else {
|
||||
return 0;
|
||||
};
|
||||
blocks.iter().count() as u64
|
||||
}
|
||||
|
||||
fn complexity(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) -> u64 {
|
||||
match obj_type {
|
||||
ObjType::Cer => {
|
||||
let cert = Cert::decode(bytes).expect("decode cert");
|
||||
if cert_inspect {
|
||||
if cert.is_ca() {
|
||||
cert.inspect_ca(strict).expect("inspect ca");
|
||||
} else {
|
||||
cert.inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
}
|
||||
count_ip(cert.v4_resources())
|
||||
.saturating_add(count_ip(cert.v6_resources()))
|
||||
.saturating_add(count_as(cert.as_resources()))
|
||||
}
|
||||
ObjType::Crl => {
|
||||
let crl = Crl::decode(bytes).expect("decode crl");
|
||||
crl.revoked_certs().iter().count() as u64
|
||||
}
|
||||
ObjType::Manifest => {
|
||||
let mft = Manifest::decode(bytes, strict).expect("decode manifest");
|
||||
if cert_inspect {
|
||||
mft.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
mft.content().len() as u64
|
||||
}
|
||||
ObjType::Roa => {
|
||||
let roa = Roa::decode(bytes, strict).expect("decode roa");
|
||||
if cert_inspect {
|
||||
roa.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
roa.content().iter().count() as u64
|
||||
}
|
||||
ObjType::Aspa => {
|
||||
let asa = Aspa::decode(bytes, strict).expect("decode aspa");
|
||||
if cert_inspect {
|
||||
asa.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
asa.content().provider_as_set().len() as u64
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_profile(obj_type: ObjType, bytes: &[u8], strict: bool, cert_inspect: bool) {
|
||||
match obj_type {
|
||||
ObjType::Cer => {
|
||||
let cert = Cert::decode(black_box(bytes)).expect("decode cert");
|
||||
if cert_inspect {
|
||||
if cert.is_ca() {
|
||||
cert.inspect_ca(strict).expect("inspect ca");
|
||||
} else {
|
||||
cert.inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
}
|
||||
black_box(cert);
|
||||
}
|
||||
ObjType::Crl => {
|
||||
let crl = Crl::decode(black_box(bytes)).expect("decode crl");
|
||||
black_box(crl);
|
||||
}
|
||||
ObjType::Manifest => {
|
||||
let mft = Manifest::decode(black_box(bytes), strict).expect("decode manifest");
|
||||
if cert_inspect {
|
||||
mft.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
black_box(mft);
|
||||
}
|
||||
ObjType::Roa => {
|
||||
let roa = Roa::decode(black_box(bytes), strict).expect("decode roa");
|
||||
if cert_inspect {
|
||||
roa.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
black_box(roa);
|
||||
}
|
||||
ObjType::Aspa => {
|
||||
let asa = Aspa::decode(black_box(bytes), strict).expect("decode aspa");
|
||||
if cert_inspect {
|
||||
asa.cert().inspect_ee(strict).expect("inspect ee");
|
||||
}
|
||||
black_box(asa);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ResultRow {
|
||||
obj_type: String,
|
||||
sample: String,
|
||||
size_bytes: usize,
|
||||
complexity: u64,
|
||||
avg_ns_per_op: f64,
|
||||
ops_per_sec: f64,
|
||||
}
|
||||
|
||||
fn render_markdown(title: &str, rows: &[ResultRow]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!("# {title}\n\n"));
|
||||
out.push_str("| type | sample | size_bytes | complexity | avg ns/op | ops/s |\n");
|
||||
out.push_str("|---|---|---:|---:|---:|---:|\n");
|
||||
for r in rows {
|
||||
out.push_str(&format!(
|
||||
"| {} | {} | {} | {} | {:.2} | {:.2} |\n",
|
||||
r.obj_type, r.sample, r.size_bytes, r.complexity, r.avg_ns_per_op, r.ops_per_sec
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn render_csv(rows: &[ResultRow]) -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str("type,sample,size_bytes,complexity,avg_ns_per_op,ops_per_sec\n");
|
||||
for r in rows {
|
||||
let sample = r.sample.replace('"', "\"\"");
|
||||
out.push_str(&format!(
|
||||
"{},{},{},{},{:.6},{:.6}\n",
|
||||
r.obj_type,
|
||||
format!("\"{}\"", sample),
|
||||
r.size_bytes,
|
||||
r.complexity,
|
||||
r.avg_ns_per_op,
|
||||
r.ops_per_sec
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn create_parent_dirs(path: &Path) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap_or_else(|e| {
|
||||
panic!("create_dir_all {}: {e}", parent.display());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn write_text_file(path: &Path, content: &str) {
|
||||
create_parent_dirs(path);
|
||||
std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cfg = parse_args();
|
||||
let mut samples = read_samples(&cfg.dir);
|
||||
if samples.is_empty() {
|
||||
usage_and_exit(Some(&format!(
|
||||
"no samples found under: {}",
|
||||
cfg.dir.display()
|
||||
)));
|
||||
}
|
||||
|
||||
if let Some(t) = cfg.type_filter {
|
||||
samples.retain(|s| s.obj_type == t);
|
||||
if samples.is_empty() {
|
||||
usage_and_exit(Some(&format!("no sample matched --type {}", t.as_str())));
|
||||
}
|
||||
}
|
||||
if let Some(filter) = cfg.sample_filter.as_deref() {
|
||||
samples.retain(|s| s.name == filter);
|
||||
if samples.is_empty() {
|
||||
usage_and_exit(Some(&format!("no sample matched --sample {filter}")));
|
||||
}
|
||||
}
|
||||
|
||||
println!("# Routinator baseline (rpki crate) decode benchmark (selected_der_v2)");
|
||||
println!();
|
||||
println!("- dir: {}", cfg.dir.display());
|
||||
println!("- strict: {}", cfg.strict);
|
||||
println!("- cert_inspect: {}", cfg.cert_inspect);
|
||||
if let Some(t) = cfg.type_filter {
|
||||
println!("- type: {}", t.as_str());
|
||||
}
|
||||
if let Some(s) = cfg.sample_filter.as_deref() {
|
||||
println!("- sample: {}", s);
|
||||
}
|
||||
if let Some(n) = cfg.fixed_iters {
|
||||
println!("- iters: {} (fixed)", n);
|
||||
} else {
|
||||
println!(
|
||||
"- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})",
|
||||
cfg.warmup_iters, cfg.rounds, cfg.min_round_ms, cfg.max_adaptive_iters
|
||||
);
|
||||
}
|
||||
if let Some(p) = cfg.out_csv.as_ref() {
|
||||
println!("- out_csv: {}", p.display());
|
||||
}
|
||||
if let Some(p) = cfg.out_md.as_ref() {
|
||||
println!("- out_md: {}", p.display());
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("| type | sample | size_bytes | complexity | avg ns/op | ops/s |");
|
||||
println!("|---|---|---:|---:|---:|---:|");
|
||||
|
||||
let mut rows: Vec<ResultRow> = Vec::with_capacity(samples.len());
|
||||
for sample in &samples {
|
||||
let bytes = std::fs::read(&sample.path)
|
||||
.unwrap_or_else(|e| panic!("read {}: {e}", sample.path.display()));
|
||||
let size_bytes = bytes.len();
|
||||
let complexity = complexity(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect);
|
||||
|
||||
for _ in 0..cfg.warmup_iters {
|
||||
decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect);
|
||||
}
|
||||
|
||||
let mut per_round_ns_per_op = Vec::with_capacity(cfg.rounds as usize);
|
||||
for _round in 0..cfg.rounds {
|
||||
let iters = if let Some(n) = cfg.fixed_iters {
|
||||
n
|
||||
} else {
|
||||
choose_iters_adaptive(
|
||||
|| decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect),
|
||||
cfg.min_round_ms,
|
||||
cfg.max_adaptive_iters,
|
||||
)
|
||||
};
|
||||
let start = Instant::now();
|
||||
for _ in 0..iters {
|
||||
decode_profile(sample.obj_type, bytes.as_slice(), cfg.strict, cfg.cert_inspect);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
let total_ns = elapsed.as_secs_f64() * 1e9;
|
||||
per_round_ns_per_op.push(total_ns / (iters as f64));
|
||||
}
|
||||
|
||||
let avg_ns = per_round_ns_per_op.iter().sum::<f64>() / (per_round_ns_per_op.len() as f64);
|
||||
let ops_per_sec = 1e9_f64 / avg_ns;
|
||||
|
||||
println!(
|
||||
"| {} | {} | {} | {} | {:.2} | {:.2} |",
|
||||
sample.obj_type.as_str(),
|
||||
sample.name,
|
||||
size_bytes,
|
||||
complexity,
|
||||
avg_ns,
|
||||
ops_per_sec
|
||||
);
|
||||
|
||||
rows.push(ResultRow {
|
||||
obj_type: sample.obj_type.as_str().to_string(),
|
||||
sample: sample.name.clone(),
|
||||
size_bytes,
|
||||
complexity,
|
||||
avg_ns_per_op: avg_ns,
|
||||
ops_per_sec,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(path) = cfg.out_md.as_ref() {
|
||||
let md = render_markdown(
|
||||
"Routinator baseline (rpki crate) decode+inspect (selected_der_v2)",
|
||||
&rows,
|
||||
);
|
||||
write_text_file(path, &md);
|
||||
eprintln!("Wrote {}", path.display());
|
||||
}
|
||||
if let Some(path) = cfg.out_csv.as_ref() {
|
||||
let csv = render_csv(&rows);
|
||||
write_text_file(path, &csv);
|
||||
eprintln!("Wrote {}", path.display());
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"slurmVersion": 2,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [
|
||||
{
|
||||
"prefix": "24.0.0.0/8",
|
||||
"comment": "Filter many VRPs in current CCR sample"
|
||||
}
|
||||
],
|
||||
"bgpsecFilters": [],
|
||||
"aspaFilters": [
|
||||
{
|
||||
"customerAsn": 80,
|
||||
"comment": "Filter one ASPA known to exist in current CCR sample"
|
||||
}
|
||||
]
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": [],
|
||||
"aspaAssertions": []
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
# Build-time image knob.
|
||||
RPKI_BIRD_VERSION=3.2.1
|
||||
|
||||
# TCP mode target endpoint.
|
||||
RPKI_BIRD_RPKI_HOST=rpki-rtr
|
||||
RPKI_BIRD_RPKI_PORT=323
|
||||
|
||||
# SSH mode target endpoint.
|
||||
RPKI_BIRD_SSH_RPKI_HOST=rpki-rtr
|
||||
RPKI_RTR_SSH_PORT=22
|
||||
|
||||
# Config template paths in container.
|
||||
RPKI_BIRD_CONFIG_TEMPLATE_PATH=/config/bird.conf.template
|
||||
RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH=/config/bird.conf.ssh.template
|
||||
|
||||
# Observation and output knobs.
|
||||
RPKI_BIRD_OBSERVE_PROTO=rpki_tcp
|
||||
RPKI_BIRD_OBSERVE_MODE=interval
|
||||
RPKI_BIRD_OBSERVE_DEBOUNCE_SECS=1
|
||||
RPKI_BIRD_OBSERVE_INTERVAL=30
|
||||
RPKI_BIRD_OBSERVE_ASPA_TABLE=rtr_aspa
|
||||
RPKI_BIRD_OBSERVE_ROA4_TABLE=rtr_roa_v4
|
||||
RPKI_BIRD_OBSERVE_ROA6_TABLE=rtr_roa_v6
|
||||
RPKI_BIRD_OBSERVE_ASPA_COUNT=3
|
||||
RPKI_BIRD_OBSERVE_ROA4_COUNT=3
|
||||
RPKI_BIRD_OBSERVE_ROA6_COUNT=3
|
||||
RPKI_BIRD_SHOW_ASPA=1
|
||||
RPKI_BIRD_SHOW_ROA4=1
|
||||
RPKI_BIRD_SHOW_ROA6=1
|
||||
|
||||
# Host volume mounts.
|
||||
RPKI_BIRD_LOG_HOST_DIR=../../logs/bird
|
||||
RPKI_BIRD_SSH_CERTS_HOST_DIR=../../certs/ssh
|
||||
@@ -1,56 +0,0 @@
|
||||
FROM debian:bookworm-slim AS builder
|
||||
|
||||
ARG BIRD_VERSION=3.2.1
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
bison \
|
||||
flex \
|
||||
m4 \
|
||||
perl \
|
||||
ca-certificates \
|
||||
wget \
|
||||
xz-utils \
|
||||
libreadline-dev \
|
||||
libncurses-dev \
|
||||
libssh-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN wget -O bird.tar.gz "https://bird.nic.cz/download/bird-${BIRD_VERSION}.tar.gz" \
|
||||
&& tar -xzf bird.tar.gz \
|
||||
&& mv "bird-${BIRD_VERSION}" bird
|
||||
|
||||
WORKDIR /build/bird
|
||||
|
||||
RUN ./configure \
|
||||
--prefix=/usr \
|
||||
--sysconfdir=/etc/bird \
|
||||
--localstatedir=/run \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
gettext-base \
|
||||
tzdata \
|
||||
netcat-openbsd \
|
||||
libreadline8 \
|
||||
libncurses6 \
|
||||
libtinfo6 \
|
||||
libssh-4 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /usr/sbin/bird /usr/sbin/bird
|
||||
COPY --from=builder /usr/sbin/birdc /usr/sbin/birdc
|
||||
COPY --from=builder /etc/bird /etc/bird
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh \
|
||||
&& mkdir -p /run/bird
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -1,90 +0,0 @@
|
||||
# BIRD Minimal RTR Client Config
|
||||
|
||||
This folder provides a minimal BIRD setup for black-box interop testing
|
||||
against this repository's RTR server defaults.
|
||||
|
||||
Server defaults in this repo:
|
||||
- TCP: `0.0.0.0:323`
|
||||
- SSH: `0.0.0.0:22` (when enabled on server)
|
||||
|
||||
## Files
|
||||
|
||||
- `Dockerfile`: builds a minimal BIRD2 runtime image.
|
||||
- `bird.conf.example`: sample `/etc/bird/bird.conf`.
|
||||
- `bird.conf.ssh.example`: sample SSH transport `/etc/bird/bird.conf`.
|
||||
- `entrypoint.sh`: starts BIRD in foreground mode.
|
||||
- `docker-compose.yml`: one-click local TCP test client.
|
||||
- `docker-compose.ssh.yml`: compose override for SSH transport.
|
||||
|
||||
By default, the container uses event-driven observation and prints snapshots
|
||||
only when BIRD reports RPKI-related changes.
|
||||
|
||||
## Docker quick start
|
||||
|
||||
From repository root:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
||||
```
|
||||
|
||||
Use another terminal to inspect:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
If protocol state is `up`, the RTR client path is working.
|
||||
|
||||
Detached mode:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
Stop:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml down
|
||||
```
|
||||
|
||||
## SSH quick start
|
||||
|
||||
Start server in SSH mode first:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
||||
```
|
||||
|
||||
Start BIRD client with SSH override:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f deploy/bird/docker-compose.yml \
|
||||
-f deploy/bird/docker-compose.ssh.yml \
|
||||
up --build
|
||||
```
|
||||
|
||||
In detached mode, observe with:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This setup renders `bird.conf.template` at startup and uses `RPKI_HOST` / `RPKI_PORT`
|
||||
for the RTR endpoint.
|
||||
- `network_mode: host` expects your RTR server to be reachable at
|
||||
`host.docker.internal:323` from the container.
|
||||
- Observation is controlled by env vars:
|
||||
`OBSERVE_MODE` (`event` by default, `interval` as fallback),
|
||||
`OBSERVE_DEBOUNCE_SECS` (default `1`),
|
||||
`OBSERVE_INTERVAL` (seconds, used when `OBSERVE_MODE=interval`),
|
||||
and `OBSERVE_PROTO`.
|
||||
- SSH mode mounts `../../certs/ssh` into `/config/ssh` and expects:
|
||||
`rtr-client.key` and `ssh_host_rsa_key.pub`.
|
||||
- Entrypoint auto-generates `/run/bird/known_hosts` from
|
||||
`/config/ssh/ssh_host_rsa_key.pub` for BIRD SSH host-key verification.
|
||||
- If your environment does not support Docker host networking, switch to a
|
||||
bridge network and replace `remote` addresses accordingly.
|
||||
@@ -1,80 +0,0 @@
|
||||
# BIRD 最小化 RTR 客户端配置
|
||||
|
||||
本目录提供最小化 BIRD 配置,用于与本仓库 RTR Server 做黑盒互通测试。
|
||||
|
||||
本仓库默认 RTR 监听地址:
|
||||
- TCP: `0.0.0.0:323`
|
||||
- SSH: `0.0.0.0:22`(仅在 server SSH 模式启用时)
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `Dockerfile`: 构建最小 BIRD 运行镜像(包含 SSH transport 支持)。
|
||||
- `bird.conf.example`: `/etc/bird/bird.conf` 的 TCP 示例。
|
||||
- `bird.conf.ssh.example`: `/etc/bird/bird.conf` 的 SSH transport 示例。
|
||||
- `entrypoint.sh`: 前台启动 BIRD,并周期输出协议快照。
|
||||
- `docker-compose.yml`: TCP 一键启动。
|
||||
- `docker-compose.ssh.yml`: SSH 覆盖配置。
|
||||
|
||||
容器默认每 30 秒向日志输出一次 RPKI 协议状态快照。
|
||||
|
||||
## Docker 快速启动(TCP)
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
||||
```
|
||||
|
||||
另开终端查看日志:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
如果协议状态显示 `up`,说明 RTR 客户端链路正常。
|
||||
|
||||
后台模式:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
停止:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/bird/docker-compose.yml down
|
||||
```
|
||||
|
||||
## SSH 快速启动
|
||||
|
||||
先启动 server 的 SSH 模式:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
||||
```
|
||||
|
||||
再启动 BIRD SSH 客户端:
|
||||
|
||||
```bash
|
||||
docker compose \
|
||||
-f deploy/bird/docker-compose.yml \
|
||||
-f deploy/bird/docker-compose.ssh.yml \
|
||||
up --build
|
||||
```
|
||||
|
||||
查看日志:
|
||||
|
||||
```bash
|
||||
docker logs -f bird-rpki-client
|
||||
```
|
||||
|
||||
## 说明
|
||||
|
||||
- 当前 compose 使用 `network_mode: host`,默认通过 `host.docker.internal` 访问 server。
|
||||
- 观测频率由环境变量控制:`OBSERVE_INTERVAL`(秒,默认 `30`)和 `OBSERVE_PROTO`。
|
||||
- SSH 模式会将 `../../certs/ssh` 挂载到容器 `/config/ssh`,并使用:
|
||||
`rtr-client.key`、`ssh_host_rsa_key.pub`。
|
||||
- 入口脚本会基于 `/config/ssh/ssh_host_rsa_key.pub` 自动生成
|
||||
`/run/bird/known_hosts`,用于 BIRD 的 SSH 主机密钥校验。
|
||||
- 如果你运行在 Docker Desktop(非 Linux 原生 host network 场景),建议改为自定义 bridge 网络并将 `remote` 地址改成可达的 server 容器名或宿主机地址。
|
||||
@@ -1,31 +0,0 @@
|
||||
log stderr all;
|
||||
debug protocols all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_ssh {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "host.docker.internal" port 22;
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport ssh {
|
||||
user "rpki-rtr";
|
||||
bird private key "/config/ssh/rtr-client.key";
|
||||
remote public key "/run/bird/known_hosts";
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
log stderr all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_tcp {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "host.docker.internal" port 323;
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport tcp {
|
||||
authentication none;
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
log stderr all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_tcp {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "host.docker.internal" port 323;
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport tcp {
|
||||
authentication none;
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
log stderr all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_ssh {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "host.docker.internal" port 22;
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport ssh {
|
||||
user "rpki-rtr";
|
||||
bird private key "/config/ssh/rtr-client.key";
|
||||
remote public key "/run/bird/known_hosts";
|
||||
};
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
log stderr all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_ssh {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport ssh {
|
||||
user "rpki-rtr";
|
||||
bird private key "/config/ssh/rtr-client.key";
|
||||
remote public key "/run/bird/known_hosts";
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
log stderr all;
|
||||
router id 192.0.2.2;
|
||||
|
||||
roa4 table rtr_roa_v4;
|
||||
roa6 table rtr_roa_v6;
|
||||
aspa table rtr_aspa;
|
||||
|
||||
protocol device {
|
||||
}
|
||||
|
||||
protocol rpki rpki_tcp {
|
||||
roa4 { table rtr_roa_v4; };
|
||||
roa6 { table rtr_roa_v6; };
|
||||
aspa { table rtr_aspa; };
|
||||
|
||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
||||
|
||||
min version 2;
|
||||
max version 2;
|
||||
|
||||
refresh 3600;
|
||||
retry 600;
|
||||
expire 7200;
|
||||
|
||||
transport tcp {
|
||||
authentication none;
|
||||
};
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
services:
|
||||
bird-rpki-client:
|
||||
environment:
|
||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH:-/config/bird.conf.ssh.template}"
|
||||
RPKI_HOST: "${RPKI_BIRD_SSH_RPKI_HOST:-rpki-rtr}"
|
||||
RPKI_PORT: "${RPKI_RTR_SSH_PORT:-22}"
|
||||
OBSERVE_PROTO: "rpki_ssh"
|
||||
volumes:
|
||||
- ./bird.conf.ssh.template:/config/bird.conf.ssh.template:ro
|
||||
- ${RPKI_BIRD_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/config/ssh:ro
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,41 +0,0 @@
|
||||
services:
|
||||
bird-rpki-client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
BIRD_VERSION: "${RPKI_BIRD_VERSION:-3.2.1}"
|
||||
container_name: bird-rpki-client
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
||||
|
||||
RPKI_HOST: "${RPKI_BIRD_RPKI_HOST:-rpki-rtr}"
|
||||
RPKI_PORT: "${RPKI_BIRD_RPKI_PORT:-323}"
|
||||
|
||||
OBSERVE_PROTO: "${RPKI_BIRD_OBSERVE_PROTO:-rpki_tcp}"
|
||||
OBSERVE_MODE: "${RPKI_BIRD_OBSERVE_MODE:-interval}"
|
||||
OBSERVE_DEBOUNCE_SECS: "${RPKI_BIRD_OBSERVE_DEBOUNCE_SECS:-1}"
|
||||
OBSERVE_INTERVAL: "${RPKI_BIRD_OBSERVE_INTERVAL:-30}"
|
||||
|
||||
OBSERVE_ASPA_TABLE: "${RPKI_BIRD_OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
||||
OBSERVE_ROA4_TABLE: "${RPKI_BIRD_OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
||||
OBSERVE_ROA6_TABLE: "${RPKI_BIRD_OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
||||
|
||||
OBSERVE_ASPA_COUNT: "${RPKI_BIRD_OBSERVE_ASPA_COUNT:-3}"
|
||||
OBSERVE_ROA4_COUNT: "${RPKI_BIRD_OBSERVE_ROA4_COUNT:-3}"
|
||||
OBSERVE_ROA6_COUNT: "${RPKI_BIRD_OBSERVE_ROA6_COUNT:-3}"
|
||||
|
||||
SHOW_ASPA: "${RPKI_BIRD_SHOW_ASPA:-1}"
|
||||
SHOW_ROA4: "${RPKI_BIRD_SHOW_ROA4:-1}"
|
||||
SHOW_ROA6: "${RPKI_BIRD_SHOW_ROA6:-1}"
|
||||
volumes:
|
||||
- ./bird.conf.template:/config/bird.conf.template:ro
|
||||
- ${RPKI_BIRD_LOG_HOST_DIR:-../../logs/bird}:/app/logs
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,218 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /run/bird
|
||||
|
||||
SOCK_PATH="/run/bird/bird.ctl"
|
||||
PROTO="${OBSERVE_PROTO:-rpki_tcp}"
|
||||
INTERVAL="${OBSERVE_INTERVAL:-30}"
|
||||
MODE="${OBSERVE_MODE:-event}"
|
||||
DEBOUNCE_SECS="${OBSERVE_DEBOUNCE_SECS:-1}"
|
||||
RPKI_HOST="${RPKI_HOST:-host.docker.internal}"
|
||||
RPKI_PORT="${RPKI_PORT:-323}"
|
||||
|
||||
BIRD_CONFIG_PATH="${BIRD_CONFIG_PATH:-/config/bird.conf}"
|
||||
BIRD_CONFIG_TEMPLATE_PATH="${BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
||||
|
||||
ASPA_TABLE="${OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
||||
ROA4_TABLE="${OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
||||
ROA6_TABLE="${OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
||||
|
||||
ASPA_COUNT="${OBSERVE_ASPA_COUNT:-3}"
|
||||
ROA4_COUNT="${OBSERVE_ROA4_COUNT:-3}"
|
||||
ROA6_COUNT="${OBSERVE_ROA6_COUNT:-3}"
|
||||
|
||||
SHOW_ASPA="${SHOW_ASPA:-1}"
|
||||
SHOW_ROA4="${SHOW_ROA4:-1}"
|
||||
SHOW_ROA6="${SHOW_ROA6:-1}"
|
||||
SSH_HOST_PUBKEY_PATH="${SSH_HOST_PUBKEY_PATH:-/config/ssh/ssh_host_rsa_key.pub}"
|
||||
SSH_KNOWN_HOSTS_PATH="${SSH_KNOWN_HOSTS_PATH:-/run/bird/known_hosts}"
|
||||
|
||||
LOG_DIR="${LOG_DIR:-/app/logs}"
|
||||
LOG_NAME="${LOG_NAME:-${HOSTNAME:-bird-rpki-client}}"
|
||||
STDOUT_LOG="${LOG_DIR}/${LOG_NAME}.stdout.log"
|
||||
STDERR_LOG="${LOG_DIR}/${LOG_NAME}.stderr.log"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
exec >>"$STDOUT_LOG" 2>>"$STDERR_LOG"
|
||||
|
||||
if [ -r "$BIRD_CONFIG_TEMPLATE_PATH" ]; then
|
||||
RENDERED_CONFIG_PATH="/run/bird/bird.generated.conf"
|
||||
envsubst '${RPKI_HOST} ${RPKI_PORT}' < "$BIRD_CONFIG_TEMPLATE_PATH" > "$RENDERED_CONFIG_PATH"
|
||||
BIRD_CONFIG_PATH="$RENDERED_CONFIG_PATH"
|
||||
echo "[entrypoint] rendered config : $BIRD_CONFIG_TEMPLATE_PATH -> $BIRD_CONFIG_PATH"
|
||||
fi
|
||||
|
||||
ensure_ssh_known_hosts() {
|
||||
if [ -s "$SSH_KNOWN_HOSTS_PATH" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -r "$SSH_HOST_PUBKEY_PATH" ]; then
|
||||
echo "[entrypoint] WARNING: SSH host key file not found: $SSH_HOST_PUBKEY_PATH"
|
||||
return
|
||||
fi
|
||||
|
||||
set -- $(awk 'NF >= 2 { print $1, $2; exit }' "$SSH_HOST_PUBKEY_PATH")
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "[entrypoint] WARNING: invalid SSH host key format in $SSH_HOST_PUBKEY_PATH"
|
||||
return
|
||||
fi
|
||||
|
||||
key_type="$1"
|
||||
key_data="$2"
|
||||
|
||||
if echo "$key_type" | grep -q '^ssh-'; then
|
||||
{
|
||||
echo "$RPKI_HOST $key_type $key_data"
|
||||
echo "[$RPKI_HOST]:$RPKI_PORT $key_type $key_data"
|
||||
} > "$SSH_KNOWN_HOSTS_PATH"
|
||||
else
|
||||
cp "$SSH_HOST_PUBKEY_PATH" "$SSH_KNOWN_HOSTS_PATH"
|
||||
fi
|
||||
|
||||
chmod 600 "$SSH_KNOWN_HOSTS_PATH" || true
|
||||
echo "[entrypoint] generated known_hosts: $SSH_KNOWN_HOSTS_PATH"
|
||||
}
|
||||
|
||||
print_first_n_objects() {
|
||||
table_name="$1"
|
||||
max_objects="$2"
|
||||
|
||||
birdc -s "$SOCK_PATH" show route table "$table_name" all 2>/dev/null | awk -v max="$max_objects" '
|
||||
BEGIN {
|
||||
count = 0
|
||||
}
|
||||
|
||||
# 直接跳过空行
|
||||
/^[[:space:]]*$/ {
|
||||
next
|
||||
}
|
||||
|
||||
# 保留 birdc 的表头
|
||||
/^BIRD / {
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
/^Table / {
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
# 非缩进且不是表头,视为一个新对象的开始
|
||||
/^[^[:space:]]/ {
|
||||
count++
|
||||
if (count > max) {
|
||||
exit
|
||||
}
|
||||
print
|
||||
next
|
||||
}
|
||||
|
||||
# 缩进内容,只有在已进入前 max 个对象时才打印
|
||||
{
|
||||
if (count > 0 && count <= max) {
|
||||
print
|
||||
}
|
||||
}
|
||||
' || true
|
||||
}
|
||||
|
||||
print_snapshot() {
|
||||
echo "==== $(date +"%Y-%m-%dT%H:%M:%S%:z") RPKI snapshot ($PROTO) ===="
|
||||
birdc -s "$SOCK_PATH" show protocols all "$PROTO" || true
|
||||
|
||||
if [ "$SHOW_ASPA" = "1" ]; then
|
||||
echo "---- ASPA table ($ASPA_TABLE, first ${ASPA_COUNT} objects) ----"
|
||||
print_first_n_objects "$ASPA_TABLE" "$ASPA_COUNT"
|
||||
fi
|
||||
|
||||
if [ "$SHOW_ROA4" = "1" ]; then
|
||||
echo "---- ROA4 table ($ROA4_TABLE, first ${ROA4_COUNT} objects) ----"
|
||||
print_first_n_objects "$ROA4_TABLE" "$ROA4_COUNT"
|
||||
fi
|
||||
|
||||
if [ "$SHOW_ROA6" = "1" ]; then
|
||||
echo "---- ROA6 table ($ROA6_TABLE, first ${ROA6_COUNT} objects) ----"
|
||||
print_first_n_objects "$ROA6_TABLE" "$ROA6_COUNT"
|
||||
fi
|
||||
}
|
||||
|
||||
is_rpki_related_event() {
|
||||
line="$1"
|
||||
echo "$line" | grep -Eiq "$PROTO|$ASPA_TABLE|$ROA4_TABLE|$ROA6_TABLE|rpki|rtr"
|
||||
}
|
||||
|
||||
echo "[entrypoint] starting bird"
|
||||
echo "[entrypoint] config : $BIRD_CONFIG_PATH"
|
||||
echo "[entrypoint] observe proto : $PROTO"
|
||||
echo "[entrypoint] observe mode : $MODE"
|
||||
echo "[entrypoint] observe interval : $INTERVAL (used when mode=interval)"
|
||||
echo "[entrypoint] debounce secs : $DEBOUNCE_SECS (used when mode=event)"
|
||||
echo "[entrypoint] target : $RPKI_HOST:$RPKI_PORT"
|
||||
echo "[entrypoint] show aspa : $SHOW_ASPA ($ASPA_TABLE, first $ASPA_COUNT objects)"
|
||||
echo "[entrypoint] show roa4 : $SHOW_ROA4 ($ROA4_TABLE, first $ROA4_COUNT objects)"
|
||||
echo "[entrypoint] show roa6 : $SHOW_ROA6 ($ROA6_TABLE, first $ROA6_COUNT objects)"
|
||||
|
||||
if [ "$PROTO" = "rpki_ssh" ] || grep -q 'transport[[:space:]]\+ssh' "$BIRD_CONFIG_PATH"; then
|
||||
ensure_ssh_known_hosts
|
||||
fi
|
||||
|
||||
if nc -zvw3 "$RPKI_HOST" "$RPKI_PORT"; then
|
||||
echo "[entrypoint] TCP connectivity to $RPKI_HOST:$RPKI_PORT OK"
|
||||
else
|
||||
echo "[entrypoint] WARNING: cannot connect to $RPKI_HOST:$RPKI_PORT before BIRD starts"
|
||||
fi
|
||||
|
||||
bird -f -c "$BIRD_CONFIG_PATH" -s "$SOCK_PATH" &
|
||||
BIRD_PID="$!"
|
||||
|
||||
sleep 1
|
||||
|
||||
case "$INTERVAL" in
|
||||
''|*[!0-9]*)
|
||||
INTERVAL=0
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$MODE" in
|
||||
event)
|
||||
case "$DEBOUNCE_SECS" in
|
||||
''|*[!0-9]*)
|
||||
DEBOUNCE_SECS=1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[entrypoint] waiting for BIRD monitor events and only printing on RPKI-related changes"
|
||||
LAST_PRINT_TS=0
|
||||
|
||||
if birdc -s "$SOCK_PATH" monitor all 2>/dev/null | while IFS= read -r line; do
|
||||
[ -n "$line" ] || continue
|
||||
if ! is_rpki_related_event "$line"; then
|
||||
continue
|
||||
fi
|
||||
now_ts="$(date +%s)"
|
||||
if [ $((now_ts - LAST_PRINT_TS)) -lt "$DEBOUNCE_SECS" ]; then
|
||||
continue
|
||||
fi
|
||||
LAST_PRINT_TS="$now_ts"
|
||||
echo "[monitor] $line"
|
||||
print_snapshot
|
||||
done; then
|
||||
:
|
||||
else
|
||||
echo "[entrypoint] WARNING: birdc monitor failed, fallback to interval polling"
|
||||
MODE="interval"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$MODE" = "interval" ] && [ "$INTERVAL" -gt 0 ]; then
|
||||
while kill -0 "$BIRD_PID" 2>/dev/null; do
|
||||
print_snapshot
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
fi
|
||||
|
||||
wait "$BIRD_PID"
|
||||
@@ -1,32 +0,0 @@
|
||||
# Target RTR server addresses for client compose files
|
||||
# RPKI_RTR_CLIENT_IMAGE=rpki-rtr-debug-client-arm64:v1.2
|
||||
RPKI_RTR_CLIENT_IMAGE=rpki-rtr-debug-client-amd64:v1.2
|
||||
|
||||
# TCP example: 10.0.0.12:323
|
||||
# TLS example: rpki.example.com:324
|
||||
# SSH example: 10.0.0.12:22
|
||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
||||
RPKI_RTR_TLS_SERVER_ADDR=rpki-rtr:324
|
||||
RPKI_RTR_SSH_SERVER_ADDR=rpki-rtr:22
|
||||
|
||||
# RTR protocol version used as client command second argument (supported: 0,1,2)
|
||||
RPKI_RTR_PROTOCOL_VERSION=2
|
||||
|
||||
# TLS server name used by --server-name in TLS mode
|
||||
# Must match server certificate SAN dNSName.
|
||||
RPKI_RTR_TLS_SERVER_NAME=localhost
|
||||
RPKI_RTR_TLS_CA_CERT_PATH=/app/certs/client-ca.crt
|
||||
RPKI_RTR_TLS_CLIENT_CERT_PATH=/app/certs/client-good.crt
|
||||
RPKI_RTR_TLS_CLIENT_KEY_PATH=/app/certs/client-good.key
|
||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
||||
|
||||
# Shared client logs mount on host.
|
||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=../../logs/client
|
||||
|
||||
# SSH mode examples:
|
||||
# RPKI_RTR_SSH_SERVER_ADDR=10.0.0.12:2222
|
||||
RPKI_RTR_CLIENT_KEYS_VOLUME=../../certs/ssh:/app/certs:ro
|
||||
RPKI_RTR_CLIENT_KEY_PATH=/app/certs/rtr-client.key
|
||||
RPKI_RTR_SSH_SERVER_PUBKEY_PATH=/app/certs/ssh_host_rsa_key.pub
|
||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
||||
RPKI_RTR_SSH_PASSWORD=
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM rust:1.89-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends clang libclang-dev pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --release --bin rtr_debug_client
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
||||
COPY --chmod=755 deploy/client/entrypoint.sh /usr/local/bin/rtr-debug-client-entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/rtr-debug-client-entrypoint.sh"]
|
||||
@@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
MODE="${MODE:-tcp}"
|
||||
ACTION="up"
|
||||
DETACH="${DETACH:-1}"
|
||||
BUILD="${BUILD:-1}"
|
||||
CLIENTS="${CLIENTS:-single}"
|
||||
TARGET_PLATFORM="${TARGET_PLATFORM:-linux/arm64}"
|
||||
CLIENT_IMAGE="${CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}}"
|
||||
BUILDX_PUSH="${BUILDX_PUSH:-0}"
|
||||
BUILDX_BUILDER="${BUILDX_BUILDER:-}"
|
||||
SKIP_BASE_IMAGE_PULL="${SKIP_BASE_IMAGE_PULL:-0}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
deploy/client/build-run.sh [action] [options]
|
||||
|
||||
Actions:
|
||||
up Build and run debug client (default)
|
||||
build Build debug client image
|
||||
buildx Build debug client image with docker buildx
|
||||
rebuild Rebuild image without cache, then run client
|
||||
down|stop Stop client compose stack
|
||||
restart Stop, then build and run client
|
||||
logs Follow client logs
|
||||
ps Show client compose status
|
||||
|
||||
Options:
|
||||
--mode MODE base|tcp|tls|ssh (default: tcp)
|
||||
--clients MODE single|multi (default: single)
|
||||
--platform VALUE Target platform for buildx (default: linux/arm64)
|
||||
--client-image TAG
|
||||
Client image tag for buildx and compose
|
||||
--push Push buildx image instead of loading it locally
|
||||
--builder NAME buildx builder name
|
||||
--skip-base-pull Do not pull Dockerfile base images before buildx
|
||||
--foreground Run single client in foreground
|
||||
--no-build Start without --build
|
||||
-h|--help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
has_compose() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_buildx() {
|
||||
docker buildx version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pull_base_images() {
|
||||
[[ "${SKIP_BASE_IMAGE_PULL}" == "1" ]] && return
|
||||
|
||||
local platform image
|
||||
local -a platforms images
|
||||
IFS=',' read -r -a platforms <<< "${TARGET_PLATFORM}"
|
||||
images=(rust:1.89-bookworm debian:bookworm-slim)
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
for image in "${images[@]}"; do
|
||||
docker pull --platform "${platform}" "${image}"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
compose_files() {
|
||||
if [[ "${CLIENTS}" == "multi" ]]; then
|
||||
[[ "${MODE}" == "tcp" || "${MODE}" == "base" ]] || die "--clients multi is only defined for tcp/base mode"
|
||||
printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.clients.yml"
|
||||
return
|
||||
fi
|
||||
|
||||
case "${MODE}" in
|
||||
base) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.yml" ;;
|
||||
tcp) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.ssh.yml" ;;
|
||||
*) die "unsupported mode '${MODE}', expected base|tcp|tls|ssh" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
compose_args() {
|
||||
local -a args files
|
||||
args=()
|
||||
[[ -f "${SCRIPT_DIR}/.env" ]] && args+=(--env-file "${SCRIPT_DIR}/.env")
|
||||
mapfile -t files < <(compose_files)
|
||||
args+=("${files[@]}")
|
||||
printf '%s\n' "${args[@]}"
|
||||
}
|
||||
|
||||
compose_run() {
|
||||
local -a args
|
||||
mapfile -t args < <(compose_args)
|
||||
docker compose "${args[@]}" "$@"
|
||||
}
|
||||
|
||||
up_client() {
|
||||
local -a up_args
|
||||
up_args=(up)
|
||||
[[ "${CLIENTS}" == "multi" || "${DETACH}" == "1" ]] && up_args+=(-d)
|
||||
[[ "${BUILD}" == "1" && "${CLIENTS}" != "multi" ]] && up_args+=(--build)
|
||||
compose_run "${up_args[@]}"
|
||||
}
|
||||
|
||||
build_client() {
|
||||
if [[ "${CLIENTS}" == "multi" ]]; then
|
||||
docker build "$@" -f "${SCRIPT_DIR}/Dockerfile" -t "${CLIENT_IMAGE}" "${REPO_ROOT}"
|
||||
else
|
||||
compose_run build "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
buildx_client() {
|
||||
has_buildx || die "docker buildx is not available"
|
||||
if [[ "${TARGET_PLATFORM}" == *,* && "${BUILDX_PUSH}" != "1" ]]; then
|
||||
die "multi-platform buildx output requires --push"
|
||||
fi
|
||||
|
||||
local -a output_args builder_args
|
||||
[[ "${BUILDX_PUSH}" == "1" ]] && output_args=(--push) || output_args=(--load)
|
||||
[[ -n "${BUILDX_BUILDER}" ]] && builder_args=(--builder "${BUILDX_BUILDER}") || builder_args=()
|
||||
|
||||
pull_base_images
|
||||
|
||||
docker buildx build "${builder_args[@]}" \
|
||||
--platform "${TARGET_PLATFORM}" \
|
||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
||||
-t "${CLIENT_IMAGE}" \
|
||||
"${output_args[@]}" \
|
||||
"${REPO_ROOT}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
up|build|buildx|rebuild|down|stop|restart|logs|ps)
|
||||
ACTION="$1"
|
||||
shift
|
||||
;;
|
||||
--mode)
|
||||
[[ $# -ge 2 ]] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--clients)
|
||||
[[ $# -ge 2 ]] || die "--clients requires a value"
|
||||
CLIENTS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
[[ $# -ge 2 ]] || die "--platform requires a value"
|
||||
TARGET_PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--client-image)
|
||||
[[ $# -ge 2 ]] || die "--client-image requires a value"
|
||||
CLIENT_IMAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--push)
|
||||
BUILDX_PUSH=1
|
||||
shift
|
||||
;;
|
||||
--builder)
|
||||
[[ $# -ge 2 ]] || die "--builder requires a value"
|
||||
BUILDX_BUILDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-base-pull)
|
||||
SKIP_BASE_IMAGE_PULL=1
|
||||
shift
|
||||
;;
|
||||
--foreground)
|
||||
DETACH=0
|
||||
shift
|
||||
;;
|
||||
--no-build)
|
||||
BUILD=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown argument '$1'"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ "${CLIENTS}" == "single" || "${CLIENTS}" == "multi" ]] || die "unsupported clients '${CLIENTS}', expected single|multi"
|
||||
export RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}"
|
||||
if [[ "${ACTION}" == "buildx" ]]; then
|
||||
has_buildx || die "docker buildx is not available"
|
||||
else
|
||||
has_compose || die "docker compose is not available"
|
||||
fi
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
case "${ACTION}" in
|
||||
up)
|
||||
up_client
|
||||
;;
|
||||
build)
|
||||
build_client
|
||||
;;
|
||||
buildx)
|
||||
buildx_client
|
||||
;;
|
||||
rebuild)
|
||||
build_client --no-cache
|
||||
up_client
|
||||
;;
|
||||
down|stop)
|
||||
compose_run down
|
||||
;;
|
||||
restart)
|
||||
compose_run down
|
||||
up_client
|
||||
;;
|
||||
logs)
|
||||
compose_run logs -f
|
||||
;;
|
||||
ps)
|
||||
compose_run ps
|
||||
;;
|
||||
esac
|
||||
@@ -1,52 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-client-1:
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
rtr-client-2:
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
rtr-client-3:
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
rtr-client-4:
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
rtr-client-5:
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,36 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/client/Dockerfile
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command:
|
||||
[
|
||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
||||
"reset",
|
||||
"--ssh",
|
||||
"--ssh-user",
|
||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
||||
"--ssh-password",
|
||||
"${RPKI_RTR_SSH_PASSWORD}",
|
||||
"--ssh-server-key",
|
||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
||||
"--keep-after-error",
|
||||
"--summary-only"
|
||||
]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,36 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/client/Dockerfile
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command:
|
||||
[
|
||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
||||
"reset",
|
||||
"--ssh",
|
||||
"--ssh-user",
|
||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
||||
"--ssh-key",
|
||||
"${RPKI_RTR_CLIENT_KEY_PATH:-/app/certs/rtr-client.key}",
|
||||
"--ssh-server-key",
|
||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
||||
"--keep-after-error",
|
||||
"--summary-only"
|
||||
]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,19 +0,0 @@
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/client/Dockerfile
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,38 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/client/Dockerfile
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command:
|
||||
[
|
||||
"${RPKI_RTR_TLS_SERVER_ADDR:-rpki-rtr:324}",
|
||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
||||
"reset",
|
||||
"--tls",
|
||||
"--ca-cert",
|
||||
"${RPKI_RTR_TLS_CA_CERT_PATH:-/app/certs/client-ca.crt}",
|
||||
"--server-name",
|
||||
"${RPKI_RTR_TLS_SERVER_NAME:-localhost}",
|
||||
"--client-cert",
|
||||
"${RPKI_RTR_TLS_CLIENT_CERT_PATH:-/app/certs/client-good.crt}",
|
||||
"--client-key",
|
||||
"${RPKI_RTR_TLS_CLIENT_KEY_PATH:-/app/certs/client-good.key}",
|
||||
"--keep-after-error",
|
||||
"--summary-only"
|
||||
]
|
||||
volumes:
|
||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,19 +0,0 @@
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/client/Dockerfile
|
||||
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
|
||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
||||
volumes:
|
||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
||||
restart: no
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /app/logs
|
||||
|
||||
log_name="${HOSTNAME:-rtr-debug-client}"
|
||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
||||
|
||||
exec /usr/local/bin/rtr_debug_client "$@" >>"$stdout_log" 2>>"$stderr_log"
|
||||
@@ -1,4 +0,0 @@
|
||||
# RTR server endpoint used by deploy/frr/frr.conf.template.
|
||||
RPKI_FRR_RPKI_HOST=127.0.0.1
|
||||
RPKI_FRR_RPKI_PORT=323
|
||||
RPKI_FRR_RPKI_PREFERENCE=1
|
||||
@@ -1,78 +0,0 @@
|
||||
# FRR Minimal RTR Client Config
|
||||
|
||||
中文文档: [README.zh.md](./README.zh.md)
|
||||
|
||||
This folder provides a minimal FRR setup for black-box interop testing
|
||||
against this repository's RTR server defaults.
|
||||
|
||||
Server defaults in this repo:
|
||||
- TCP: `0.0.0.0:323`
|
||||
- TLS: `0.0.0.0:324`
|
||||
|
||||
Reference:
|
||||
- `src/main.rs`
|
||||
|
||||
## Files
|
||||
|
||||
- `daemons.example`: sample `/etc/frr/daemons`
|
||||
- `frr.conf.example`: static sample `/etc/frr/frr.conf`
|
||||
- `frr.conf.template`: Docker template used to generate `/etc/frr/frr.conf`
|
||||
- `entrypoint.sh`: renders `frr.conf.template` from environment variables before starting FRR
|
||||
- `.env`: default Docker environment values
|
||||
|
||||
## How to apply on an FRR host
|
||||
|
||||
1. Copy `daemons.example` to `/etc/frr/daemons`.
|
||||
2. Copy `frr.conf.example` to `/etc/frr/frr.conf`.
|
||||
3. Restart FRR:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart frr
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
vtysh -c "show rpki configuration"
|
||||
vtysh -c "show rpki cache-server"
|
||||
vtysh -c "show rpki cache-connection"
|
||||
vtysh -c "show rpki prefix-table"
|
||||
```
|
||||
|
||||
If `show rpki cache-connection` is connected and `show rpki prefix-table`
|
||||
contains VRPs, the RTR client path is working.
|
||||
|
||||
## Docker quick start
|
||||
|
||||
From repository root:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
||||
```
|
||||
|
||||
The Docker example defaults to `127.0.0.1:323`. Override the RTR server
|
||||
endpoint with environment variables:
|
||||
|
||||
```bash
|
||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
||||
RPKI_FRR_RPKI_PORT=323 \
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
Stop:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- This setup targets RTR over TCP (`rpki cache tcp`).
|
||||
- Docker mode renders `frr.conf.template` into `/etc/frr/frr.conf` at startup.
|
||||
- Keep protocol-level conformance checks in Rust tests and
|
||||
`src/bin/rtr_debug_client`.
|
||||
- `network_mode: host` is kept for the minimal FRR container example, but the
|
||||
RTR server address is controlled by `RPKI_FRR_RPKI_HOST` and
|
||||
`RPKI_FRR_RPKI_PORT`.
|
||||
@@ -1,83 +0,0 @@
|
||||
# FRR 最小化 RTR 客户端配置
|
||||
|
||||
本目录提供一个 FRR 最小配置,用于和本仓库 RTR Server 做黑盒互通测试。
|
||||
|
||||
本仓库默认 RTR 监听地址:
|
||||
- TCP:`0.0.0.0:323`
|
||||
- TLS:`0.0.0.0:324`
|
||||
|
||||
参考实现:
|
||||
- `src/main.rs`
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `daemons.example`:示例 `/etc/frr/daemons`
|
||||
- `frr.conf.example`:静态示例 `/etc/frr/frr.conf`
|
||||
- `frr.conf.template`:Docker 启动时生成 `/etc/frr/frr.conf` 的模板
|
||||
- `entrypoint.sh`:启动 FRR 前根据环境变量渲染 `frr.conf.template`
|
||||
- `.env`:Docker 示例使用的默认环境变量
|
||||
|
||||
## 在 FRR 主机上应用
|
||||
|
||||
1. 复制 `daemons.example` 到 `/etc/frr/daemons`
|
||||
2. 复制 `frr.conf.example` 到 `/etc/frr/frr.conf`
|
||||
3. 重启 FRR:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart frr
|
||||
```
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
vtysh -c "show rpki configuration"
|
||||
vtysh -c "show rpki cache-server"
|
||||
vtysh -c "show rpki cache-connection"
|
||||
vtysh -c "show rpki prefix-table"
|
||||
```
|
||||
|
||||
当 `show rpki cache-connection` 显示已连接,且 `show rpki prefix-table` 出现 VRP 时,说明 RTR 客户端链路工作正常。
|
||||
|
||||
## Docker 快速启动
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
||||
```
|
||||
|
||||
Docker 示例默认连接 `127.0.0.1:323`。如果 RTR Server 在其他地址,可以通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
||||
RPKI_FRR_RPKI_PORT=323 \
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
停止:
|
||||
|
||||
```bash
|
||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
||||
```
|
||||
|
||||
## 可测试范围
|
||||
|
||||
- FRR 作为 RTR Client 与本仓库 Server 的 TCP 建链能力
|
||||
- FRR 侧基础会话状态可见性(`cache-server` / `cache-connection`)
|
||||
- VRP 下发与导入是否成功(`prefix-table` 是否有条目)
|
||||
- 基于服务端数据变化触发的前缀表更新(可通过替换 `data` 后观察)
|
||||
- 黑盒互通回归:用于确认“路由器客户端视角”功能可用
|
||||
|
||||
## 不覆盖范围
|
||||
|
||||
- 不替代协议级单元测试/集成测试(PDU 细节、异常路径、边界条件)
|
||||
- 不替代 `src/bin/rtr_debug_client` 的逐报文调试能力
|
||||
- 默认示例以 TCP 为主,TLS/mTLS 需按你的证书与 FRR 配置单独扩展
|
||||
|
||||
## 说明
|
||||
|
||||
- 当前示例主要针对 RTR over TCP(`rpki cache tcp`)
|
||||
- Docker 模式会在启动时把 `frr.conf.template` 渲染为 `/etc/frr/frr.conf`
|
||||
- `network_mode: host` 仍用于最小化 FRR 容器示例,但 RTR Server 地址由 `RPKI_FRR_RPKI_HOST` 和 `RPKI_FRR_RPKI_PORT` 控制
|
||||
@@ -1,7 +0,0 @@
|
||||
# Minimal FRR daemons config for RPKI testing
|
||||
|
||||
zebra=yes
|
||||
bgpd=yes
|
||||
|
||||
# Enable bgpd RPKI module
|
||||
bgpd_options=" -A 127.0.0.1 -M rpki"
|
||||
@@ -1,16 +0,0 @@
|
||||
services:
|
||||
frr-rpki-client:
|
||||
image: quay.io/frrouting/frr:10.2.2
|
||||
container_name: frr-rpki-client
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
privileged: true
|
||||
environment:
|
||||
RPKI_FRR_RPKI_HOST: "${RPKI_FRR_RPKI_HOST:-127.0.0.1}"
|
||||
RPKI_FRR_RPKI_PORT: "${RPKI_FRR_RPKI_PORT:-323}"
|
||||
RPKI_FRR_RPKI_PREFERENCE: "${RPKI_FRR_RPKI_PREFERENCE:-1}"
|
||||
entrypoint: ["/bin/sh", "/config/entrypoint.sh"]
|
||||
volumes:
|
||||
- ./daemons.example:/etc/frr/daemons:ro
|
||||
- ./frr.conf.template:/config/frr.conf.template:ro
|
||||
- ./entrypoint.sh:/config/entrypoint.sh:ro
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${RPKI_FRR_RPKI_HOST:=127.0.0.1}"
|
||||
: "${RPKI_FRR_RPKI_PORT:=323}"
|
||||
: "${RPKI_FRR_RPKI_PREFERENCE:=1}"
|
||||
: "${RPKI_FRR_CONFIG_TEMPLATE_PATH:=/config/frr.conf.template}"
|
||||
: "${RPKI_FRR_CONFIG_PATH:=/etc/frr/frr.conf}"
|
||||
|
||||
escape_sed() {
|
||||
printf '%s' "$1" | sed 's/[&|\\]/\\&/g'
|
||||
}
|
||||
|
||||
host="$(escape_sed "${RPKI_FRR_RPKI_HOST}")"
|
||||
port="$(escape_sed "${RPKI_FRR_RPKI_PORT}")"
|
||||
preference="$(escape_sed "${RPKI_FRR_RPKI_PREFERENCE}")"
|
||||
|
||||
sed \
|
||||
-e "s|\${RPKI_FRR_RPKI_HOST}|${host}|g" \
|
||||
-e "s|\${RPKI_FRR_RPKI_PORT}|${port}|g" \
|
||||
-e "s|\${RPKI_FRR_RPKI_PREFERENCE}|${preference}|g" \
|
||||
"${RPKI_FRR_CONFIG_TEMPLATE_PATH}" > "${RPKI_FRR_CONFIG_PATH}"
|
||||
|
||||
exec /usr/lib/frr/docker-start
|
||||
@@ -1,22 +0,0 @@
|
||||
frr version 10.2
|
||||
frr defaults traditional
|
||||
hostname rpki-lab
|
||||
service integrated-vtysh-config
|
||||
!
|
||||
debug rpki
|
||||
!
|
||||
rpki
|
||||
rpki polling_period 10
|
||||
rpki timeout 10
|
||||
rpki retry_interval 10
|
||||
rpki expire_interval 7200
|
||||
rpki cache tcp 127.0.0.1 323 preference 1
|
||||
exit
|
||||
!
|
||||
router bgp 65001
|
||||
bgp router-id 192.0.2.1
|
||||
!
|
||||
address-family ipv4 unicast
|
||||
exit-address-family
|
||||
!
|
||||
line vty
|
||||
@@ -1,22 +0,0 @@
|
||||
frr version 10.2
|
||||
frr defaults traditional
|
||||
hostname rpki-lab
|
||||
service integrated-vtysh-config
|
||||
!
|
||||
debug rpki
|
||||
!
|
||||
rpki
|
||||
rpki polling_period 10
|
||||
rpki timeout 10
|
||||
rpki retry_interval 10
|
||||
rpki expire_interval 7200
|
||||
rpki cache tcp ${RPKI_FRR_RPKI_HOST} ${RPKI_FRR_RPKI_PORT} preference ${RPKI_FRR_RPKI_PREFERENCE}
|
||||
exit
|
||||
!
|
||||
router bgp 65001
|
||||
bgp router-id 192.0.2.1
|
||||
!
|
||||
address-family ipv4 unicast
|
||||
exit-address-family
|
||||
!
|
||||
line vty
|
||||
@@ -1,49 +0,0 @@
|
||||
# Core runtime knobs.
|
||||
# RPKI_RTR_SERVER_IMAGE=rpki-rtr-arm64:v1.2
|
||||
RPKI_RTR_SERVER_IMAGE=rpki-rtr-amd64:v1.2
|
||||
|
||||
# Data source directories on host.
|
||||
RPKI_RTR_CCR_HOST_DIR=../../data
|
||||
RPKI_RTR_SLURM_HOST_DIR=../../data
|
||||
|
||||
# In-container data source directories.
|
||||
RPKI_RTR_CCR_DIR=/app/data
|
||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
||||
RPKI_RTR_REPORT_HOST_DIR=../../report
|
||||
|
||||
# In-container runtime paths.
|
||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
||||
RPKI_RTR_REPORT_DIR=/app/report
|
||||
|
||||
# Persistent directories on host.
|
||||
RPKI_RTR_DB_HOST_DIR=../../rtr-db
|
||||
RPKI_RTR_LOG_HOST_DIR=../../logs/server
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=300
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10
|
||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
||||
RPKI_RTR_MAX_DELTA=10
|
||||
RPKI_RTR_MAX_CONNECTIONS=100000
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=128
|
||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
||||
RPKI_RTR_ADMIN_TOKEN=qwert
|
||||
RUST_LOG=info
|
||||
|
||||
# TLS mode knobs.
|
||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH=false
|
||||
RPKI_RTR_TLS_CERT_PATH=/app/certs/server-dns.crt
|
||||
RPKI_RTR_TLS_KEY_PATH=/app/certs/server-dns.key
|
||||
RPKI_RTR_TLS_CLIENT_CA_PATH=/app/certs/client-ca.crt
|
||||
|
||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
||||
|
||||
# SSH mode knobs.
|
||||
RPKI_RTR_SSH_HOST_PORT=2222
|
||||
RPKI_RTR_SSH_CONTAINER_PORT=22
|
||||
RPKI_RTR_SSH_AUTH_MODE=key
|
||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
||||
RPKI_RTR_SSH_SUBSYSTEM_NAME=rpki-rtr
|
||||
RPKI_RTR_SSH_HOST_KEY_PATH=/app/certs/ssh_host_rsa_key
|
||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH=/app/certs/rtr-authorized_keys
|
||||
|
||||
RPKI_RTR_SSH_CERTS_HOST_DIR=../../certs/ssh
|
||||
@@ -1,99 +0,0 @@
|
||||
# Deployment (Docker Compose)
|
||||
|
||||
This project runs `src/main.rs` as a long-running server that:
|
||||
|
||||
1. loads latest `.ccr` from a configured directory,
|
||||
2. applies optional SLURM filtering,
|
||||
3. starts RTR server.
|
||||
|
||||
The container runs `rpki` directly as PID 1.
|
||||
|
||||
## Files
|
||||
|
||||
- `deploy/server/Dockerfile`
|
||||
- `deploy/server/docker-compose.yml`
|
||||
|
||||
## Runtime Paths in Container
|
||||
|
||||
- CCR directory: `/app/data`
|
||||
- RocksDB directory: `/app/rtr-db`
|
||||
- SLURM directory: `/app/slurm`
|
||||
- Report directory: `/app/report`
|
||||
- TLS cert directory (optional): `/app/certs`
|
||||
|
||||
## Path Configuration via `.env`
|
||||
|
||||
- `RPKI_RTR_CCR_HOST_DIR`: host CCR directory mounted into container
|
||||
- `RPKI_RTR_SLURM_HOST_DIR`: host SLURM directory mounted into container
|
||||
- `RPKI_RTR_CCR_DIR`: in-container CCR directory path
|
||||
- `RPKI_RTR_SLURM_DIR`: in-container SLURM directory path
|
||||
- `RPKI_RTR_DB_HOST_DIR`: host RocksDB directory
|
||||
- `RPKI_RTR_LOG_HOST_DIR`: host log directory
|
||||
- `RPKI_RTR_REPORT_HOST_DIR`: host directory receiving split RTR JSON reports
|
||||
- `RPKI_RTR_DB_PATH`: in-container RocksDB directory
|
||||
- `RPKI_RTR_REPORT_DIR`: in-container report directory
|
||||
|
||||
## Runtime Configuration via `.env`
|
||||
|
||||
- Core: `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS`, `RPKI_RTR_MAX_DELTA`, `RPKI_RTR_MAX_CONCURRENT_HANDSHAKES`, `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, `RPKI_RTR_REPORT_HISTORY_LIMIT`, `RPKI_RTR_TIMEZONE`, `RPKI_RTR_ADMIN_ADDR`, `RPKI_RTR_ADMIN_TOKEN`, `RUST_LOG`
|
||||
- TCP mode: `RPKI_RTR_MAX_CONNECTIONS`
|
||||
- TLS mode: `RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH`, `RPKI_RTR_TLS_CERT_PATH`, `RPKI_RTR_TLS_KEY_PATH`, `RPKI_RTR_TLS_CLIENT_CA_PATH`, `RPKI_RTR_TLS_CERTS_HOST_DIR`
|
||||
- SSH mode: `RPKI_RTR_SSH_HOST_PORT`, `RPKI_RTR_SSH_CONTAINER_PORT`, `RPKI_RTR_SSH_AUTH_MODE`, `RPKI_RTR_SSH_USERNAME`, `RPKI_RTR_SSH_SUBSYSTEM_NAME`, `RPKI_RTR_SSH_HOST_KEY_PATH`, `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH`, `RPKI_RTR_SSH_CERTS_HOST_DIR`
|
||||
|
||||
## Start
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.yml up -d --build
|
||||
```
|
||||
|
||||
## Stop
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.yml down
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
|
||||
```
|
||||
|
||||
The admin API can also stream the redirected log file:
|
||||
|
||||
```bash
|
||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200" \
|
||||
-H "Authorization: Bearer $RPKI_RTR_ADMIN_TOKEN"
|
||||
```
|
||||
|
||||
It reads `/app/logs/${HOSTNAME}.stdout.log` or `.stderr.log` by default. Set
|
||||
`RPKI_RTR_LOG_DIR` and `RPKI_RTR_LOG_NAME` to override that lookup.
|
||||
|
||||
## Runtime Report
|
||||
|
||||
The server writes split JSON reports. Each report file uses a local-time
|
||||
timestamp suffix and each category keeps `RPKI_RTR_REPORT_HISTORY_LIMIT` files,
|
||||
defaulting to 10.
|
||||
|
||||
- `rtr-source-*.json`: CCR and SLURM source metadata, latest refresh status,
|
||||
data quality counts, cache snapshot counts, and delta counts. Written on
|
||||
startup and source refresh events.
|
||||
- `rtr-clients-*.json`: active client connection counts and counts by transport
|
||||
(`tcp`, `tls`, `ssh`). Written on startup, whenever the active connection
|
||||
count changes.
|
||||
- `rtr-runtime-*.json`: service start time, uptime, process RSS, and
|
||||
non-sensitive runtime configuration. Written on startup and every
|
||||
`RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, defaulting to 300 seconds.
|
||||
|
||||
Timestamps in logs and report JSON files use `RPKI_RTR_TIMEZONE`, which
|
||||
defaults to `Asia/Shanghai`. Use IANA timezone names such as `Asia/Shanghai`,
|
||||
`Europe/London`, `America/New_York`, or `UTC`; `Shanghai` is accepted as a
|
||||
convenience alias for `Asia/Shanghai`.
|
||||
|
||||
## Runtime Admin Config
|
||||
|
||||
The admin endpoint is disabled by default. Set `RPKI_RTR_ADMIN_ADDR` to enable
|
||||
`POST /admin/rtr/config`. If the address is not loopback, `RPKI_RTR_ADMIN_TOKEN`
|
||||
must also be set and requests must include `Authorization: Bearer <token>`.
|
||||
|
||||
The endpoint accepts partial JSON updates. See `docs/rtr-admin-api.md` for the
|
||||
complete request/response schema, examples, and runtime apply semantics.
|
||||
@@ -1,85 +0,0 @@
|
||||
FROM rust:1.89-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ENV CARGO_BUILD_JOBS=2
|
||||
|
||||
RUN set -eux; \
|
||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
||||
Types: deb
|
||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
Suites: bookworm bookworm-updates
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
Suites: bookworm-security
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
||||
-o Acquire::Retries=10 \
|
||||
-o Acquire::http::Timeout=60 \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
clang \
|
||||
libclang-dev \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --release --bin rpki_rtr
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
|
||||
RUN set -eux; \
|
||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
||||
Types: deb
|
||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
||||
Suites: bookworm bookworm-updates
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
||||
Suites: bookworm-security
|
||||
Components: main
|
||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
||||
-o Acquire::Retries=10 \
|
||||
-o Acquire::http::Timeout=60 \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /build/target/release/rpki_rtr /usr/local/bin/rpki_rtr
|
||||
COPY --chmod=755 deploy/server/entrypoint.sh /usr/local/bin/rpki-rtr-entrypoint.sh
|
||||
|
||||
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs /app/report
|
||||
|
||||
ENV RPKI_RTR_ENABLE_TLS=false \
|
||||
RPKI_RTR_TCP_ADDR=0.0.0.0:323 \
|
||||
RPKI_RTR_TLS_ADDR=0.0.0.0:324 \
|
||||
RPKI_RTR_DB_PATH=/app/rtr-db \
|
||||
RPKI_RTR_CCR_DIR=/app/data \
|
||||
RPKI_RTR_SLURM_DIR=/app/slurm \
|
||||
RPKI_RTR_REPORT_DIR=/app/report \
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300 \
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10 \
|
||||
RPKI_RTR_REFRESH_INTERVAL_SECS=300 \
|
||||
RPKI_RTR_ADMIN_ADDR="" \
|
||||
RPKI_RTR_ADMIN_TOKEN=""
|
||||
|
||||
EXPOSE 323 324
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/rpki-rtr-entrypoint.sh"]
|
||||
@@ -1,215 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
MODE="${MODE:-tcp}"
|
||||
ACTION="up"
|
||||
DETACH="${DETACH:-1}"
|
||||
BUILD="${BUILD:-1}"
|
||||
TARGET_PLATFORM="${TARGET_PLATFORM:-linux/arm64}"
|
||||
SERVER_IMAGE="${SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}}"
|
||||
BUILDX_PUSH="${BUILDX_PUSH:-0}"
|
||||
BUILDX_BUILDER="${BUILDX_BUILDER:-}"
|
||||
SKIP_BASE_IMAGE_PULL="${SKIP_BASE_IMAGE_PULL:-0}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
deploy/server/build-run.sh [action] [options]
|
||||
|
||||
Actions:
|
||||
up Build and start server (default)
|
||||
build Build server image with docker compose
|
||||
buildx Build server image with docker buildx
|
||||
rebuild Rebuild image without cache, then start server
|
||||
down|stop Stop server compose stack
|
||||
restart Stop, then build and start server
|
||||
logs Follow server logs
|
||||
ps Show server compose status
|
||||
|
||||
Options:
|
||||
--mode MODE base|tcp|tls|ssh (default: tcp)
|
||||
--platform VALUE Target platform for buildx (default: linux/arm64)
|
||||
--server-image TAG
|
||||
Server image tag for buildx and compose
|
||||
--push Push buildx image instead of loading it locally
|
||||
--builder NAME buildx builder name
|
||||
--skip-base-pull Do not pull Dockerfile base images before buildx
|
||||
--foreground Run server in foreground
|
||||
--no-build Start without --build
|
||||
-h|--help Show this help
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
has_compose() {
|
||||
docker compose version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
has_buildx() {
|
||||
docker buildx version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
pull_base_images() {
|
||||
[[ "${SKIP_BASE_IMAGE_PULL}" == "1" ]] && return
|
||||
|
||||
local platform image
|
||||
local -a platforms images
|
||||
IFS=',' read -r -a platforms <<< "${TARGET_PLATFORM}"
|
||||
images=(rust:1.89-bookworm debian:bookworm-slim)
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
for image in "${images[@]}"; do
|
||||
docker pull --platform "${platform}" "${image}"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
compose_files() {
|
||||
case "${MODE}" in
|
||||
base) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.yml" ;;
|
||||
tcp) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' -f "${SCRIPT_DIR}/docker-compose.ssh.yml" ;;
|
||||
*) die "unsupported mode '${MODE}', expected base|tcp|tls|ssh" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
compose_args() {
|
||||
local -a args files
|
||||
args=()
|
||||
[[ -f "${SCRIPT_DIR}/.env" ]] && args+=(--env-file "${SCRIPT_DIR}/.env")
|
||||
mapfile -t files < <(compose_files)
|
||||
args+=("${files[@]}")
|
||||
printf '%s\n' "${args[@]}"
|
||||
}
|
||||
|
||||
compose_run() {
|
||||
local -a args
|
||||
mapfile -t args < <(compose_args)
|
||||
docker compose "${args[@]}" "$@"
|
||||
}
|
||||
|
||||
up_server() {
|
||||
local -a up_args
|
||||
up_args=(up)
|
||||
[[ "${DETACH}" == "1" ]] && up_args+=(-d)
|
||||
[[ "${BUILD}" == "1" ]] && up_args+=(--build)
|
||||
compose_run "${up_args[@]}"
|
||||
}
|
||||
|
||||
buildx_server() {
|
||||
has_buildx || die "docker buildx is not available"
|
||||
if [[ "${TARGET_PLATFORM}" == *,* && "${BUILDX_PUSH}" != "1" ]]; then
|
||||
die "multi-platform buildx output requires --push"
|
||||
fi
|
||||
|
||||
local -a output_args builder_args
|
||||
[[ "${BUILDX_PUSH}" == "1" ]] && output_args=(--push) || output_args=(--load)
|
||||
[[ -n "${BUILDX_BUILDER}" ]] && builder_args=(--builder "${BUILDX_BUILDER}") || builder_args=()
|
||||
|
||||
pull_base_images
|
||||
|
||||
docker buildx build "${builder_args[@]}" \
|
||||
--platform "${TARGET_PLATFORM}" \
|
||||
-f "${SCRIPT_DIR}/Dockerfile" \
|
||||
-t "${SERVER_IMAGE}" \
|
||||
"${output_args[@]}" \
|
||||
"${REPO_ROOT}"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
up|build|buildx|rebuild|down|stop|restart|logs|ps)
|
||||
ACTION="$1"
|
||||
shift
|
||||
;;
|
||||
--mode)
|
||||
[[ $# -ge 2 ]] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
[[ $# -ge 2 ]] || die "--platform requires a value"
|
||||
TARGET_PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--server-image)
|
||||
[[ $# -ge 2 ]] || die "--server-image requires a value"
|
||||
SERVER_IMAGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--push)
|
||||
BUILDX_PUSH=1
|
||||
shift
|
||||
;;
|
||||
--builder)
|
||||
[[ $# -ge 2 ]] || die "--builder requires a value"
|
||||
BUILDX_BUILDER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--skip-base-pull)
|
||||
SKIP_BASE_IMAGE_PULL=1
|
||||
shift
|
||||
;;
|
||||
--foreground)
|
||||
DETACH=0
|
||||
shift
|
||||
;;
|
||||
--no-build)
|
||||
BUILD=0
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown argument '$1'"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
export RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}"
|
||||
if [[ "${ACTION}" == "buildx" ]]; then
|
||||
has_buildx || die "docker buildx is not available"
|
||||
else
|
||||
has_compose || die "docker compose is not available"
|
||||
fi
|
||||
|
||||
cd "${REPO_ROOT}"
|
||||
|
||||
case "${ACTION}" in
|
||||
up)
|
||||
up_server
|
||||
;;
|
||||
build)
|
||||
compose_run build
|
||||
;;
|
||||
buildx)
|
||||
buildx_server
|
||||
;;
|
||||
rebuild)
|
||||
compose_run build --no-cache
|
||||
up_server
|
||||
;;
|
||||
down|stop)
|
||||
compose_run down
|
||||
;;
|
||||
restart)
|
||||
compose_run down
|
||||
up_server
|
||||
;;
|
||||
logs)
|
||||
compose_run logs -f
|
||||
;;
|
||||
ps)
|
||||
compose_run ps
|
||||
;;
|
||||
esac
|
||||
@@ -1,54 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rpki-rtr:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/server/Dockerfile
|
||||
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
|
||||
container_name: rpki-rtr-ssh
|
||||
restart: no
|
||||
ports:
|
||||
- "323:323"
|
||||
- "${RPKI_RTR_SSH_HOST_PORT:-2222}:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
||||
- "8323:8323"
|
||||
environment:
|
||||
RPKI_RTR_ENABLE_TLS: "false"
|
||||
RPKI_RTR_ENABLE_SSH: "true"
|
||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||
RPKI_RTR_SSH_ADDR: "0.0.0.0:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
||||
RPKI_RTR_SSH_HOST_KEY_PATH: "${RPKI_RTR_SSH_HOST_KEY_PATH:-/app/certs/ssh_host_rsa_key}"
|
||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "${RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH:-/app/certs/rtr-authorized_keys}"
|
||||
RPKI_RTR_SSH_USERNAME: "${RPKI_RTR_SSH_USERNAME:-rpki-rtr}"
|
||||
RPKI_RTR_SSH_SUBSYSTEM_NAME: "${RPKI_RTR_SSH_SUBSYSTEM_NAME:-rpki-rtr}"
|
||||
# SSH auth mode: key | password | both
|
||||
RPKI_RTR_SSH_AUTH_MODE: "${RPKI_RTR_SSH_AUTH_MODE:-key}"
|
||||
# Optional: enable password authentication in addition to publickey
|
||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
||||
RUST_LOG: "${RUST_LOG:-info}"
|
||||
volumes:
|
||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
||||
- ${RPKI_RTR_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/app/certs:ro
|
||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,44 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rpki-rtr:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/server/Dockerfile
|
||||
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
|
||||
container_name: rpki-rtr-tcp
|
||||
restart: no
|
||||
ports:
|
||||
- "323:323"
|
||||
- "8323:8323"
|
||||
environment:
|
||||
RPKI_RTR_ENABLE_TLS: "false"
|
||||
RPKI_RTR_ENABLE_SSH: "false"
|
||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-60}"
|
||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
||||
RPKI_RTR_MAX_CONNECTIONS: "${RPKI_RTR_MAX_CONNECTIONS:-100000}"
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
||||
RUST_LOG: "${RUST_LOG:-info}"
|
||||
volumes:
|
||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,48 +0,0 @@
|
||||
services:
|
||||
rpki-rtr:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/server/Dockerfile
|
||||
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
|
||||
container_name: rpki-rtr-tls
|
||||
restart: no
|
||||
ports:
|
||||
# - "323:323"
|
||||
- "324:324"
|
||||
- "8323:8323"
|
||||
environment:
|
||||
RPKI_RTR_ENABLE_TLS: "true"
|
||||
RPKI_RTR_ENABLE_SSH: "false"
|
||||
# RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
||||
RPKI_RTR_TLS_CERT_PATH: "${RPKI_RTR_TLS_CERT_PATH:-/app/certs/server-dns.crt}"
|
||||
RPKI_RTR_TLS_KEY_PATH: "${RPKI_RTR_TLS_KEY_PATH:-/app/certs/server-dns.key}"
|
||||
RPKI_RTR_TLS_CLIENT_CA_PATH: "${RPKI_RTR_TLS_CLIENT_CA_PATH:-/app/certs/client-ca.crt}"
|
||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "${RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH:-false}"
|
||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
||||
RUST_LOG: "${RUST_LOG:-info}"
|
||||
volumes:
|
||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,58 +0,0 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rpki-rtr:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/server/Dockerfile
|
||||
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
|
||||
container_name: rpki-rtr
|
||||
restart: no
|
||||
ports:
|
||||
- "323:323"
|
||||
- "324:324"
|
||||
- "8323:8323"
|
||||
# SSH mode example:
|
||||
# - "22:22"
|
||||
environment:
|
||||
RPKI_RTR_ENABLE_TLS: "false"
|
||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
||||
RUST_LOG: "${RUST_LOG:-info}"
|
||||
# SSH mode example:
|
||||
# RPKI_RTR_ENABLE_SSH: "true"
|
||||
# RPKI_RTR_SSH_ADDR: "0.0.0.0:22"
|
||||
# RPKI_RTR_SSH_PORT: "22"
|
||||
# RPKI_RTR_SSH_HOST_KEY_PATH: "/app/certs/ssh_host_rsa_key"
|
||||
# RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "/app/certs/rtr-authorized_keys"
|
||||
# RPKI_RTR_SSH_USERNAME: "rpki-rtr"
|
||||
# RPKI_RTR_SSH_SUBSYSTEM_NAME: "rpki-rtr"
|
||||
# Optional: enable password auth in addition to publickey
|
||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
||||
volumes:
|
||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
||||
# TLS mode example:
|
||||
# - ../../certs/ssh:/app/certs:ro
|
||||
networks:
|
||||
- rpki_net
|
||||
|
||||
networks:
|
||||
rpki_net:
|
||||
name: rpki_net
|
||||
driver: bridge
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
mkdir -p /app/logs
|
||||
|
||||
log_name="${HOSTNAME:-rpki-rtr}"
|
||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
||||
|
||||
exec /usr/local/bin/rpki_rtr "$@" >>"$stdout_log" 2>>"$stderr_log"
|
||||
@@ -1,310 +0,0 @@
|
||||
# RTR Admin API
|
||||
|
||||
本文档描述 RTR server 当前提供的 HTTP 管理接口。
|
||||
|
||||
## 启用方式
|
||||
|
||||
Admin API 默认关闭。设置 `RPKI_RTR_ADMIN_ADDR` 后启用:
|
||||
|
||||
```env
|
||||
RPKI_RTR_ADMIN_ADDR=127.0.0.1:8323
|
||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
||||
```
|
||||
|
||||
如果在 Docker 中需要从宿主机访问,容器内建议监听:
|
||||
|
||||
```env
|
||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
||||
```
|
||||
|
||||
并在 compose 中映射端口:
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
- "8323:8323"
|
||||
```
|
||||
|
||||
安全规则:
|
||||
|
||||
- `RPKI_RTR_ADMIN_ADDR` 为空时,不启动 Admin API。
|
||||
- 非 loopback 地址,例如 `0.0.0.0:8323`,必须配置 `RPKI_RTR_ADMIN_TOKEN`,否则 admin server 会拒绝启动。
|
||||
- 设置 token 后,请求必须带 `Authorization: Bearer <token>`。
|
||||
|
||||
## 通用 Headers
|
||||
|
||||
```http
|
||||
Authorization: Bearer change-me
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
GET 请求不需要 `Content-Type`。
|
||||
|
||||
## GET /admin/rtr/health
|
||||
|
||||
用于检查 Admin API 是否可达,以及当前启用了哪些管理能力。
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8323/admin/rtr/health \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"config_api": true,
|
||||
"source_reload_api": true,
|
||||
"slurm_api": true,
|
||||
"logs_api": true
|
||||
}
|
||||
```
|
||||
|
||||
## GET /admin/rtr/logs/tail
|
||||
|
||||
实时读取 RTR 服务日志,效果类似 `tail -f`。默认读取 stdout 日志,先返回最近 200 行,然后持续输出新增内容。
|
||||
|
||||
```bash
|
||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200&follow=true" \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
Query 参数:
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `stream` | `stdout` | 可选 `stdout` 或 `stderr`。 |
|
||||
| `lines` | `200` | 首次返回的历史行数,范围会限制在 `1..=5000`。 |
|
||||
| `follow` | `true` | `true` 时保持连接并持续输出新增日志;`false` 时只返回当前 tail 内容。 |
|
||||
|
||||
日志文件路径按容器 entrypoint 的规则解析:
|
||||
|
||||
- 目录:`RPKI_RTR_LOG_DIR`,默认 `/app/logs`。
|
||||
- 文件名前缀:`RPKI_RTR_LOG_NAME`,未设置时使用 `HOSTNAME`,再未设置时为 `rpki-rtr`。
|
||||
- stdout 文件:`<name>.stdout.log`。
|
||||
- stderr 文件:`<name>.stderr.log`。
|
||||
|
||||
## GET /admin/rtr/config
|
||||
|
||||
查询当前运行中的 runtime 配置。
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8323/admin/rtr/config \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"config": {
|
||||
"max_delta": 10,
|
||||
"prune_delta_by_snapshot_size": true,
|
||||
"source_refresh_interval_seconds": 300,
|
||||
"runtime_report_interval_seconds": 300,
|
||||
"report_history_limit": 10,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"timing": {
|
||||
"refresh": 3600,
|
||||
"retry": 600,
|
||||
"expire": 7200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## POST /admin/rtr/config
|
||||
|
||||
运行中动态修改 RTR server 的部分 runtime 配置。请求体是 JSON object,支持部分更新,只需要传要修改的字段。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `max_delta` | integer | 每个 RTR 版本最多保留的 delta 条数,必须 `>= 1`。 |
|
||||
| `prune_delta_by_snapshot_size` | boolean | 是否按 snapshot wire size 裁剪 delta window。 |
|
||||
| `source_refresh_interval_seconds` | integer | CCR/SLURM source refresh 间隔,单位秒,必须 `>= 1`。 |
|
||||
| `runtime_report_interval_seconds` | integer | `rtr-runtime-*.json` 周期写入间隔,单位秒,必须 `>= 1`。 |
|
||||
| `report_history_limit` | integer | 每类 report 文件滚动保留数量,必须 `>= 1`。 |
|
||||
| `timezone` | string | IANA 时区名,例如 `Asia/Shanghai`、`UTC`、`Europe/London`。 |
|
||||
| `timing.refresh` | integer | RTR EndOfData `refresh`,单位秒。 |
|
||||
| `timing.retry` | integer | RTR EndOfData `retry`,单位秒。 |
|
||||
| `timing.expire` | integer | RTR EndOfData `expire`,必须大于 `refresh` 和 `retry`。 |
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8323/admin/rtr/config \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer change-me" \
|
||||
-d '{"max_delta": 6}'
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"config": {
|
||||
"max_delta": 6,
|
||||
"prune_delta_by_snapshot_size": true,
|
||||
"source_refresh_interval_seconds": 300,
|
||||
"runtime_report_interval_seconds": 300,
|
||||
"report_history_limit": 10,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"timing": {
|
||||
"refresh": 3600,
|
||||
"retry": 600,
|
||||
"expire": 7200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GET /admin/rtr/slurm/files
|
||||
|
||||
列出 `RPKI_RTR_SLURM_DIR` 下的 SLURM 文件。只返回合法的 `*.slurm` 和 `*.slurm.disabled` 文件,不返回 `.backup/`。
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"files": [
|
||||
{
|
||||
"name": "local-policy.slurm",
|
||||
"path": "/app/slurm/local-policy.slurm",
|
||||
"enabled": true,
|
||||
"size_bytes": 168,
|
||||
"modified_unix_seconds": 1782100000
|
||||
},
|
||||
{
|
||||
"name": "old-policy.slurm.disabled",
|
||||
"path": "/app/slurm/old-policy.slurm.disabled",
|
||||
"enabled": false,
|
||||
"size_bytes": 168,
|
||||
"modified_unix_seconds": 1782100100
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## GET /admin/rtr/slurm/files/{name}
|
||||
|
||||
读取单个 SLURM 文件,前端编辑页面可以直接使用这个接口加载内容。
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"file": {
|
||||
"name": "local-policy.slurm",
|
||||
"path": "/app/slurm/local-policy.slurm",
|
||||
"enabled": true,
|
||||
"size_bytes": 168,
|
||||
"modified_unix_seconds": 1782100000,
|
||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## POST /admin/rtr/slurm/files
|
||||
|
||||
新增或覆盖一个 SLURM 文件。文件名放在请求体中。
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "local-policy.slurm",
|
||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
||||
"reload": true
|
||||
}
|
||||
```
|
||||
|
||||
## PUT /admin/rtr/slurm/files/{name}
|
||||
|
||||
新增或覆盖指定 SLURM 文件。
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer change-me" \
|
||||
-d '{
|
||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
||||
"reload": true
|
||||
}'
|
||||
```
|
||||
|
||||
## DELETE /admin/rtr/slurm/files/{name}
|
||||
|
||||
删除指定 SLURM 文件。删除前会先备份。
|
||||
|
||||
```bash
|
||||
curl -X DELETE "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm?reload=true" \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
## POST /admin/rtr/slurm/files/{name}/disable
|
||||
|
||||
将启用文件改名为失效文件:
|
||||
|
||||
```text
|
||||
local-policy.slurm -> local-policy.slurm.disabled
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm/disable?reload=true" \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
## POST /admin/rtr/slurm/files/{name}/enable
|
||||
|
||||
将失效文件改名为启用文件:
|
||||
|
||||
```text
|
||||
local-policy.slurm.disabled -> local-policy.slurm
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm.disabled/enable?reload=true" \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
## POST /admin/rtr/slurm/reload
|
||||
|
||||
不修改文件,只立即触发一次 CCR/SLURM source reload。
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8323/admin/rtr/slurm/reload \
|
||||
-H "Authorization: Bearer change-me"
|
||||
```
|
||||
|
||||
## SLURM 文件规则
|
||||
|
||||
- `*.slurm`:启用,会被 source refresh 加载。
|
||||
- `*.slurm.disabled`:失效,不会被加载。
|
||||
- `.backup/`:接口自动生成的备份目录。
|
||||
- 文件名只能是 basename,不能包含 `/`、`\` 或 `..`。
|
||||
- 文件名只能包含 ASCII 字母、数字、`.`、`_`、`-`。
|
||||
- 写入内容必须是合法 JSON,并且能被解析为合法 SLURM 文件。
|
||||
- 单个请求体默认限制为 5 MiB。
|
||||
- 覆盖、删除前会写入 `.backup/` 备份。
|
||||
|
||||
## 错误响应
|
||||
|
||||
| HTTP 状态码 | 含义 |
|
||||
| --- | --- |
|
||||
| `400 Bad Request` | JSON 非法、配置值非法、SLURM 文件名非法、目标文件不存在、reload 失败等。 |
|
||||
| `401 Unauthorized` | 配置了 token,但请求缺少或使用了错误的 `Authorization`。 |
|
||||
| `404 Not Found` | 路径或方法不支持。 |
|
||||
| `413 Payload Too Large` | 请求体超过限制。 |
|
||||
@@ -1,203 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
IMAGE_DIR="${ROOT_DIR}/images"
|
||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
||||
|
||||
SERVER_TAR="${RPKI_RTR_SERVER_IMAGE_TAR:-}"
|
||||
CLIENT_TAR="${RPKI_RTR_CLIENT_IMAGE_TAR:-}"
|
||||
CONSOLE_TAR="${RPKI_RTR_CONSOLE_IMAGE_TAR:-}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/00_0_install.sh [options]
|
||||
|
||||
Load offline Docker image tarballs from ./images, then run scripts/00_1_pre_check.sh.
|
||||
|
||||
Options:
|
||||
--server-tar FILE Server image tarball. Env: RPKI_RTR_SERVER_IMAGE_TAR
|
||||
--client-tar FILE Debug client image tarball. Env: RPKI_RTR_CLIENT_IMAGE_TAR
|
||||
--console-tar FILE Console image tarball. Env: RPKI_RTR_CONSOLE_IMAGE_TAR
|
||||
-h, --help Show this help.
|
||||
|
||||
Default tarballs under ./images:
|
||||
server: rpki-rtr-*.tar, excluding rpki-rtr-debug-client-*.tar
|
||||
client: rpki-rtr-debug-client-*.tar
|
||||
console: rpki-console-ui-*.tar
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[00_install] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[00_install] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
set_env_var() {
|
||||
local env_file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_file
|
||||
|
||||
[ -f "${env_file}" ] || die "env file not found: ${env_file}"
|
||||
tmp_file="${env_file}.tmp.$$"
|
||||
|
||||
awk -v key="${key}" -v value="${value}" '
|
||||
BEGIN { done = 0 }
|
||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
done = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "${env_file}" >"${tmp_file}"
|
||||
mv "${tmp_file}" "${env_file}"
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--server-tar)
|
||||
[ "$#" -ge 2 ] || die "--server-tar requires a value"
|
||||
SERVER_TAR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--client-tar)
|
||||
[ "$#" -ge 2 ] || die "--client-tar requires a value"
|
||||
CLIENT_TAR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--console-tar)
|
||||
[ "$#" -ge 2 ] || die "--console-tar requires a value"
|
||||
CONSOLE_TAR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
resolve_tar() {
|
||||
local path="$1"
|
||||
|
||||
if [ -f "${path}" ]; then
|
||||
cd "$(dirname "${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f "${ROOT_DIR}/${path}" ]; then
|
||||
cd "$(dirname "${ROOT_DIR}/${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
load_image() {
|
||||
local label="$1"
|
||||
local tar_path="$2"
|
||||
local output image
|
||||
|
||||
[ -f "${tar_path}" ] || die "${label} image tarball not found: ${tar_path}"
|
||||
output="$(docker load -i "${tar_path}" 2>&1)" || {
|
||||
printf '%s\n' "${output}" >&2
|
||||
die "failed to load ${label} image from ${tar_path}"
|
||||
}
|
||||
printf '%s\n' "${output}" >&2
|
||||
|
||||
image="$(printf '%s\n' "${output}" | sed -n 's/^Loaded image: //p' | tail -n 1)"
|
||||
[ -n "${image}" ] || die "cannot determine ${label} image name from docker load output"
|
||||
printf '%s\n' "${image}"
|
||||
}
|
||||
|
||||
find_single_tar() {
|
||||
local label="$1"
|
||||
local include_pattern="$2"
|
||||
local exclude_pattern="${3:-}"
|
||||
local matches=()
|
||||
local file
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
matches+=("${file}")
|
||||
done < <(
|
||||
if [ -n "${exclude_pattern}" ]; then
|
||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" ! -name "${exclude_pattern}" -print0 | sort -z
|
||||
else
|
||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" -print0 | sort -z
|
||||
fi
|
||||
)
|
||||
|
||||
case "${#matches[@]}" in
|
||||
0)
|
||||
die "no ${label} image tarball found under ${IMAGE_DIR} matching ${include_pattern}"
|
||||
;;
|
||||
1)
|
||||
printf '%s\n' "${matches[0]}"
|
||||
;;
|
||||
*)
|
||||
printf '[00_install] ERROR: multiple %s image tarballs found:\n' "${label}" >&2
|
||||
printf ' %s\n' "${matches[@]}" >&2
|
||||
die "please specify --${label}-tar or RPKI_RTR_${label^^}_IMAGE_TAR"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
stage "Checking Docker"
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
[ -d "${IMAGE_DIR}" ] || die "image directory not found: ${IMAGE_DIR}"
|
||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
||||
|
||||
if [ -n "${SERVER_TAR}" ]; then
|
||||
SERVER_TAR="$(resolve_tar "${SERVER_TAR}")" || die "server image tarball not found: ${SERVER_TAR}"
|
||||
else
|
||||
SERVER_TAR="$(find_single_tar server 'rpki-rtr-*.tar' 'rpki-rtr-debug-client-*.tar')"
|
||||
fi
|
||||
|
||||
if [ -n "${CLIENT_TAR}" ]; then
|
||||
CLIENT_TAR="$(resolve_tar "${CLIENT_TAR}")" || die "client image tarball not found: ${CLIENT_TAR}"
|
||||
else
|
||||
CLIENT_TAR="$(find_single_tar client 'rpki-rtr-debug-client-*.tar')"
|
||||
fi
|
||||
|
||||
if [ -n "${CONSOLE_TAR}" ]; then
|
||||
CONSOLE_TAR="$(resolve_tar "${CONSOLE_TAR}")" || die "console image tarball not found: ${CONSOLE_TAR}"
|
||||
else
|
||||
CONSOLE_TAR="$(find_single_tar console 'rpki-console-ui-*.tar')"
|
||||
fi
|
||||
|
||||
stage "Loading Docker Images"
|
||||
SERVER_IMAGE="$(load_image server "${SERVER_TAR}")"
|
||||
CLIENT_IMAGE="$(load_image client "${CLIENT_TAR}")"
|
||||
CONSOLE_IMAGE="$(load_image console "${CONSOLE_TAR}")"
|
||||
|
||||
stage "Updating Deploy Env"
|
||||
set_env_var "${SERVER_ENV_FILE}" RPKI_RTR_SERVER_IMAGE "${SERVER_IMAGE}"
|
||||
set_env_var "${CONSOLE_ENV_FILE}" RPKI_CONSOLE_UI_IMAGE "${CONSOLE_IMAGE}"
|
||||
|
||||
stage "Running Pre-Check"
|
||||
RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}" \
|
||||
RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}" \
|
||||
bash "${SCRIPT_DIR}/00_1_pre_check.sh" || die "pre-check failed"
|
||||
|
||||
stage "Install Finished"
|
||||
@@ -1,155 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
||||
PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
||||
PRECHECK_WAIT_SECS="${PRECHECK_WAIT_SECS:-60}"
|
||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
||||
|
||||
log() {
|
||||
printf '[00_1_pre_check] %s\n' "$*"
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[00_1_pre_check] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
stage "Preparing Pre-Check Data"
|
||||
bash "${SCRIPT_DIR}/00_2_pre_check_prepare.sh" || die "prepare step failed"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${PRECHECK_ENV_FILE}"
|
||||
set +a
|
||||
|
||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN}"
|
||||
|
||||
server_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_SERVER_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
client_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_CLIENT_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
KEEP_PRECHECK="${KEEP_PRECHECK}" PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR}" PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE}" \
|
||||
bash "${SCRIPT_DIR}/00_3_pre_check_cleanup.sh"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
http_get() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local path="$3"
|
||||
local token="$4"
|
||||
|
||||
exec 3<>"/dev/tcp/${host}/${port}" || return 1
|
||||
printf 'GET %s HTTP/1.1\r\nHost: %s:%s\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' \
|
||||
"${path}" "${host}" "${port}" "${token}" >&3
|
||||
cat <&3
|
||||
exec 3>&-
|
||||
}
|
||||
|
||||
wait_for_admin_health() {
|
||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
||||
local response
|
||||
|
||||
log "waiting for server health"
|
||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
||||
response="$(http_get 127.0.0.1 "${ADMIN_PORT}" /admin/rtr/health "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
||||
if printf '%s' "${response}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then
|
||||
log "admin health check passed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$(server_compose ps -q rpki-rtr | wc -l | tr -d ' ')" = "0" ]; then
|
||||
server_compose logs rpki-rtr || true
|
||||
die "server container is not running"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
server_compose logs rpki-rtr || true
|
||||
die "admin health check did not pass within ${PRECHECK_WAIT_SECS}s"
|
||||
}
|
||||
|
||||
wait_for_client_eod() {
|
||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
||||
local log_file
|
||||
|
||||
log "waiting for client response"
|
||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
||||
for log_file in "${PRECHECK_WORK_DIR}"/logs/client/*.stdout.log; do
|
||||
[ -f "${log_file}" ] || continue
|
||||
if grep -q 'EndOfData:' "${log_file}" && grep -q 'summary[[:space:]]*:' "${log_file}"; then
|
||||
log "client received EndOfData and payload summary"
|
||||
return 0
|
||||
fi
|
||||
if grep -qi 'Error Report' "${log_file}"; then
|
||||
cat "${log_file}" >&2
|
||||
die "client received RTR Error Report"
|
||||
fi
|
||||
done
|
||||
sleep 1
|
||||
done
|
||||
|
||||
find "${PRECHECK_WORK_DIR}/logs/client" -type f -maxdepth 1 -print -exec cat {} \; >&2 || true
|
||||
die "client did not receive EndOfData and summary within ${PRECHECK_WAIT_SECS}s"
|
||||
}
|
||||
|
||||
assert_no_error_logs() {
|
||||
local bad
|
||||
log "checking logs"
|
||||
bad="$(find "${PRECHECK_WORK_DIR}/logs" -type f -name '*.log' -print0 \
|
||||
| xargs -0 grep -Ein 'panic|failed|RTR admin config server exited|Error Report' 2>/dev/null || true)"
|
||||
if [ -n "${bad}" ]; then
|
||||
printf '%s\n' "${bad}" >&2
|
||||
die "error pattern found in pre-check logs"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_report_written() {
|
||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
||||
|
||||
log "waiting for source report"
|
||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
||||
if find "${PRECHECK_WORK_DIR}/report" -type f -name 'rtr-source-*.json' | grep -q .; then
|
||||
log "source report written"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
die "server did not write rtr-source report within ${PRECHECK_WAIT_SECS}s"
|
||||
}
|
||||
|
||||
stage "Starting RTR Server"
|
||||
server_compose up -d --no-build rpki-rtr
|
||||
wait_for_admin_health
|
||||
assert_report_written
|
||||
|
||||
stage "Starting Debug Client"
|
||||
client_compose up -d --no-build rtr-debug-client
|
||||
wait_for_client_eod
|
||||
|
||||
assert_no_error_logs
|
||||
stage "Pre-Check Passed"
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
OUT_PATH="${1:-}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/00_2_generate_ccr.sh OUT.ccr
|
||||
|
||||
Generate a small deterministic CCR snapshot for pre-check.
|
||||
This script writes a fixed base64-encoded CCR fixture and does not require Python.
|
||||
The generated snapshot contains:
|
||||
- VRP: 10.0.0.0/24 maxLen 24 AS65001
|
||||
- VRP: 10.0.1.0/24 maxLen 24 AS65003
|
||||
- VRP: 2001:db8:1::/48 maxLen 48 AS65002
|
||||
- ASPA: customer AS65010 providers AS65011, AS65012, AS65013
|
||||
- ASPA: customer AS65020 provider AS65021
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[00_2_generate_ccr] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ "${OUT_PATH}" = "-h" ] || [ "${OUT_PATH}" = "--help" ]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ -n "${OUT_PATH}" ] || die "missing OUT.ccr"
|
||||
mkdir -p "$(dirname "${OUT_PATH}")"
|
||||
|
||||
decode_base64() {
|
||||
if command -v base64 >/dev/null 2>&1; then
|
||||
base64 -d
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl base64 -d
|
||||
return
|
||||
fi
|
||||
|
||||
die "base64 or openssl command is required to write CCR fixture"
|
||||
}
|
||||
|
||||
cat <<'EOF' | decode_base64 >"${OUT_PATH}"
|
||||
MIIBAwYLKoZIhvcNAQkQATaggfMwgfACAQAwDQYJYIZIAWUDBAIBBQAYDzIwMjYwNDAzMDAwMjAx
|
||||
WqJ9MHswVzAaAgMA/ekwEzARBAIAATALMAkDBAAKAAACARgwGgIDAP3rMBMwEQQCAAEwCzAJAwQA
|
||||
CgABAgEYMB0CAwD96jAWMBQEAgACMA4wDAMHACABDbgAAQIBMAQgsUgCHfy5AJcFp5RymOD0HuGO
|
||||
wfhoWpll90ugYECGQPSjTDBKMCYwFgIDAP3yMA8CAwD98wIDAP30AgMA/fUwDAIDAP38MAUCAwD9
|
||||
/QQgRPxVPOOUsUuuV4tZ8BG5PHNtKiaEr23Ird+yNxlWJ+w=
|
||||
EOF
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
||||
PRECHECK_ADMIN_TOKEN="${PRECHECK_ADMIN_TOKEN:-rpki-precheck-token}"
|
||||
PRECHECK_PROTOCOL_VERSION="${PRECHECK_PROTOCOL_VERSION:-2}"
|
||||
PRECHECK_SERVER_IMAGE="${PRECHECK_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-rpki-rtr-arm64:v1.2}}"
|
||||
PRECHECK_CLIENT_IMAGE="${PRECHECK_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client-arm64:v1.2}}"
|
||||
|
||||
log() {
|
||||
printf '[00_1_pre_check_prepare] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[00_1_pre_check_prepare] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
abs_dir() {
|
||||
local path="$1"
|
||||
mkdir -p "${path}"
|
||||
cd "${path}" && pwd -P
|
||||
}
|
||||
|
||||
[ -f "${SCRIPT_DIR}/00_2_generate_ccr.sh" ] || die "missing ${SCRIPT_DIR}/00_2_generate_ccr.sh"
|
||||
|
||||
rm -rf "${PRECHECK_WORK_DIR}"
|
||||
mkdir -p \
|
||||
"${PRECHECK_WORK_DIR}/ccr" \
|
||||
"${PRECHECK_WORK_DIR}/slurm" \
|
||||
"${PRECHECK_WORK_DIR}/db" \
|
||||
"${PRECHECK_WORK_DIR}/logs/server" \
|
||||
"${PRECHECK_WORK_DIR}/logs/client" \
|
||||
"${PRECHECK_WORK_DIR}/report"
|
||||
|
||||
bash "${SCRIPT_DIR}/00_2_generate_ccr.sh" \
|
||||
"${PRECHECK_WORK_DIR}/ccr/20260403T000201Z-precheck.ccr"
|
||||
|
||||
cat >"${PRECHECK_WORK_DIR}/slurm/precheck.slurm" <<'EOF'
|
||||
{
|
||||
"slurmVersion": 2,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": [],
|
||||
"aspaFilters": []
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [
|
||||
{
|
||||
"prefix": "203.0.113.0/24",
|
||||
"maxPrefixLength": 24,
|
||||
"asn": 64496,
|
||||
"comment": "pre-check local assertion"
|
||||
}
|
||||
],
|
||||
"bgpsecAssertions": [],
|
||||
"aspaAssertions": []
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
PRECHECK_WORK_DIR_ABS="$(abs_dir "${PRECHECK_WORK_DIR}")"
|
||||
cat >"${PRECHECK_WORK_DIR}/precheck.env" <<EOF
|
||||
PRECHECK_WORK_DIR=${PRECHECK_WORK_DIR_ABS}
|
||||
PRECHECK_PROJECT=${PRECHECK_PROJECT}
|
||||
RPKI_RTR_SERVER_IMAGE=${PRECHECK_SERVER_IMAGE}
|
||||
RPKI_RTR_CLIENT_IMAGE=${PRECHECK_CLIENT_IMAGE}
|
||||
PRECHECK_SERVER_COMPOSE_FILE=${ROOT_DIR}/deploy/server/docker-compose.tcp.yml
|
||||
PRECHECK_CLIENT_COMPOSE_FILE=${ROOT_DIR}/deploy/client/docker-compose.tcp.yml
|
||||
RPKI_RTR_CCR_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/ccr
|
||||
RPKI_RTR_SLURM_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/slurm
|
||||
RPKI_RTR_DB_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/db
|
||||
RPKI_RTR_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/server
|
||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/client
|
||||
RPKI_RTR_REPORT_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/report
|
||||
RPKI_RTR_CCR_DIR=/app/data
|
||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
||||
RPKI_RTR_REPORT_DIR=/app/report
|
||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
||||
RPKI_RTR_ADMIN_TOKEN=${PRECHECK_ADMIN_TOKEN}
|
||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=5
|
||||
RPKI_RTR_REPORT_HISTORY_LIMIT=3
|
||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=30
|
||||
RPKI_RTR_MAX_DELTA=10
|
||||
RPKI_RTR_MAX_CONNECTIONS=32
|
||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=8
|
||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
||||
RPKI_RTR_PROTOCOL_VERSION=${PRECHECK_PROTOCOL_VERSION}
|
||||
RUST_LOG=info
|
||||
EOF
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
||||
ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
||||
|
||||
if [ -f "${ENV_FILE}" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${ENV_FILE}"
|
||||
set +a
|
||||
fi
|
||||
|
||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
||||
PRECHECK_SERVER_COMPOSE_FILE="${PRECHECK_SERVER_COMPOSE_FILE:-${ROOT_DIR}/deploy/server/docker-compose.tcp.yml}"
|
||||
PRECHECK_CLIENT_COMPOSE_FILE="${PRECHECK_CLIENT_COMPOSE_FILE:-${ROOT_DIR}/deploy/client/docker-compose.tcp.yml}"
|
||||
COMPOSE_BIN=()
|
||||
|
||||
log() {
|
||||
printf '[00_1_pre_check_cleanup] %s\n' "$*"
|
||||
}
|
||||
|
||||
compose_run() {
|
||||
local compose_file="$1"
|
||||
shift
|
||||
|
||||
if [ "${#COMPOSE_BIN[@]}" -eq 0 ]; then
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
"${COMPOSE_BIN[@]}" --env-file "${ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${compose_file}" "$@"
|
||||
}
|
||||
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
log "stopping pre-check client compose stack"
|
||||
[ -f "${PRECHECK_CLIENT_COMPOSE_FILE}" ] && compose_run "${PRECHECK_CLIENT_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
||||
log "stopping pre-check server compose stack"
|
||||
[ -f "${PRECHECK_SERVER_COMPOSE_FILE}" ] && compose_run "${PRECHECK_SERVER_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ "${KEEP_PRECHECK}" = "1" ]; then
|
||||
log "KEEP_PRECHECK=1, preserving ${PRECHECK_WORK_DIR}"
|
||||
else
|
||||
rm -rf "${PRECHECK_WORK_DIR}"
|
||||
log "removed ${PRECHECK_WORK_DIR}"
|
||||
fi
|
||||
@@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.server"
|
||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
||||
CONSOLE_COMPOSE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
||||
WAIT_SECS="${RPKI_START_WAIT_SECS:-20}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/01_start.sh [options]
|
||||
|
||||
Read root ./environment.server, update deploy/server/.env, then start the RTR server and console.
|
||||
|
||||
Options:
|
||||
--env FILE Common env file. Default: ./environment.server
|
||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
||||
--wait SECS Wait timeout for container running checks. Default: 20
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[01_start] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[01_start] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
set_env_var() {
|
||||
local env_file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_file
|
||||
|
||||
tmp_file="${env_file}.tmp.$$"
|
||||
awk -v key="${key}" -v value="${value}" '
|
||||
BEGIN { done = 0 }
|
||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
done = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "${env_file}" >"${tmp_file}"
|
||||
mv "${tmp_file}" "${env_file}"
|
||||
}
|
||||
|
||||
compose_file_for_mode() {
|
||||
case "$1" in
|
||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
update_env_from_common() {
|
||||
local target_env_file="$1"
|
||||
local updated_count=0
|
||||
local line key value
|
||||
|
||||
while IFS= read -r line || [ -n "${line}" ]; do
|
||||
case "${line}" in
|
||||
''|\#*) continue ;;
|
||||
*=*)
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
set_env_var "${target_env_file}" "${key}" "${value}"
|
||||
updated_count=$((updated_count + 1))
|
||||
;;
|
||||
esac
|
||||
done <"${COMMON_ENV_FILE}"
|
||||
|
||||
printf '%s\n' "${updated_count}"
|
||||
}
|
||||
|
||||
server_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
console_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_COMPOSE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
container_running() {
|
||||
local compose_kind="$1"
|
||||
local service="$2"
|
||||
local cid state
|
||||
|
||||
if [ "${compose_kind}" = "server" ]; then
|
||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
||||
else
|
||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
[ -n "${cid}" ] || return 1
|
||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
||||
[ "${state}" = "true" ]
|
||||
}
|
||||
|
||||
show_service_debug() {
|
||||
local compose_kind="$1"
|
||||
local service="$2"
|
||||
|
||||
log "${service} is not running, current compose status:"
|
||||
if [ "${compose_kind}" = "server" ]; then
|
||||
server_compose ps "${service}" >&2 || true
|
||||
log "${service} recent logs:"
|
||||
server_compose logs --tail=80 "${service}" >&2 || true
|
||||
else
|
||||
console_compose ps "${service}" >&2 || true
|
||||
log "${service} recent logs:"
|
||||
console_compose logs --tail=80 "${service}" >&2 || true
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_running() {
|
||||
local compose_kind="$1"
|
||||
local service="$2"
|
||||
local deadline=$((SECONDS + WAIT_SECS))
|
||||
|
||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
||||
if container_running "${compose_kind}" "${service}"; then
|
||||
log "${service} is running"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
show_service_debug "${compose_kind}" "${service}"
|
||||
die "${service} failed to reach running state after ${WAIT_SECS}s"
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--env)
|
||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
||||
COMMON_ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--mode)
|
||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--wait)
|
||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
||||
WAIT_SECS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
||||
|
||||
stage "Checking Docker"
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
||||
|
||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
||||
stage "Reading Environment"
|
||||
server_updated_count="$(update_env_from_common "${SERVER_ENV_FILE}")"
|
||||
log "updated ${server_updated_count} server env variable(s)"
|
||||
else
|
||||
stage "Reading Environment"
|
||||
log "environment.server file not found, skip env update"
|
||||
fi
|
||||
|
||||
stage "Starting RTR Server"
|
||||
log "starting docker compose service: rpki-rtr"
|
||||
server_compose up -d --no-build rpki-rtr
|
||||
log "waiting for rpki-rtr to be running"
|
||||
wait_for_running server rpki-rtr
|
||||
|
||||
stage "Starting Console"
|
||||
log "starting docker compose service: rpki-console-ui"
|
||||
console_compose up -d --no-build rpki-console-ui
|
||||
log "waiting for rpki-console-ui to be running"
|
||||
wait_for_running console rpki-console-ui
|
||||
|
||||
stage "Start Finished"
|
||||
@@ -1,184 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
||||
SERVER_COMPOSE_FILE="${ROOT_DIR}/deploy/server/docker-compose.tcp.yml"
|
||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
||||
WAIT_SECS="${RPKI_SELF_CHECK_WAIT_SECS:-60}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/02_self_check.sh [options]
|
||||
|
||||
Check whether the RTR server and console started by scripts/01_start.sh are healthy.
|
||||
|
||||
Options:
|
||||
--wait SECS Wait timeout for each check. Default: 60
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[02_self_check] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[02_self_check] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--wait)
|
||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
||||
WAIT_SECS="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1090
|
||||
. "${SERVER_ENV_FILE}"
|
||||
# shellcheck disable=SC1090
|
||||
. "${CONSOLE_ENV_FILE}"
|
||||
set +a
|
||||
|
||||
RTR_PORT="${RPKI_RTR_TCP_HOST_PORT:-323}"
|
||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
||||
ADMIN_PORT="${ADMIN_PORT:-8323}"
|
||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN:-}"
|
||||
CONSOLE_PORT="${RPKI_CONSOLE_UI_PORT:-5173}"
|
||||
|
||||
server_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
console_compose() {
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
||||
}
|
||||
|
||||
container_running() {
|
||||
local compose_kind="$1"
|
||||
local service="$2"
|
||||
local cid state
|
||||
|
||||
if [ "${compose_kind}" = "server" ]; then
|
||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
||||
else
|
||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
[ -n "${cid}" ] || return 1
|
||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
||||
[ "${state}" = "true" ]
|
||||
}
|
||||
|
||||
tcp_open() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
|
||||
if command -v nc >/dev/null 2>&1; then
|
||||
nc -z "${host}" "${port}" >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
|
||||
(exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1
|
||||
}
|
||||
|
||||
http_get() {
|
||||
local url="$1"
|
||||
local token="${2:-}"
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${token}" ]; then
|
||||
curl -fsS -H "Authorization: Bearer ${token}" "${url}"
|
||||
else
|
||||
curl -fsS "${url}"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
die "curl command is required for HTTP health checks"
|
||||
}
|
||||
|
||||
wait_until() {
|
||||
local label="$1"
|
||||
shift
|
||||
local deadline=$((SECONDS + WAIT_SECS))
|
||||
|
||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
||||
if "$@"; then
|
||||
log "${label}: ok"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
die "${label}: failed after ${WAIT_SECS}s"
|
||||
}
|
||||
|
||||
show_command() {
|
||||
printf '[02_self_check] check command: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
check_admin_health() {
|
||||
local body
|
||||
body="$(http_get "http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health" "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
||||
printf '%s' "${body}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'
|
||||
}
|
||||
|
||||
check_console_http() {
|
||||
http_get "http://127.0.0.1:${CONSOLE_PORT}/" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stage "Checking Containers"
|
||||
show_command "${COMPOSE_BIN[*]} --env-file ${SERVER_ENV_FILE} -p ${SERVER_PROJECT} -f ${SERVER_COMPOSE_FILE} ps -q rpki-rtr && docker inspect"
|
||||
wait_until "server container" container_running server rpki-rtr
|
||||
show_command "${COMPOSE_BIN[*]} --env-file ${CONSOLE_ENV_FILE} -p ${CONSOLE_PROJECT} -f ${CONSOLE_COMPOSE_FILE} ps -q rpki-console-ui && docker inspect"
|
||||
wait_until "console container" container_running console rpki-console-ui
|
||||
|
||||
stage "Checking Server"
|
||||
show_command "tcp connect 127.0.0.1:${RTR_PORT}"
|
||||
wait_until "RTR port ${RTR_PORT}" tcp_open 127.0.0.1 "${RTR_PORT}"
|
||||
show_command "curl -fsS -H 'Authorization: Bearer <token>' http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health"
|
||||
wait_until "Admin health" check_admin_health
|
||||
|
||||
stage "Checking Console"
|
||||
show_command "tcp connect 127.0.0.1:${CONSOLE_PORT}"
|
||||
wait_until "Console port ${CONSOLE_PORT}" tcp_open 127.0.0.1 "${CONSOLE_PORT}"
|
||||
show_command "curl -fsS http://127.0.0.1:${CONSOLE_PORT}/"
|
||||
wait_until "Console HTTP" check_console_http
|
||||
|
||||
stage "Self-Check Passed"
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/03_shutdown.sh [options]
|
||||
|
||||
Stop services started by scripts/01_start.sh.
|
||||
|
||||
Options:
|
||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[03_shutdown] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[03_shutdown] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
compose_file_for_mode() {
|
||||
case "$1" in
|
||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
||||
|
||||
stage "Checking Docker"
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
stage "Stopping Console"
|
||||
if [ -f "${CONSOLE_ENV_FILE}" ] && [ -f "${CONSOLE_COMPOSE_FILE}" ]; then
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" down --remove-orphans
|
||||
else
|
||||
log "console compose/env not found, skip"
|
||||
fi
|
||||
|
||||
stage "Stopping RTR Server"
|
||||
if [ -f "${SERVER_ENV_FILE}" ] && [ -f "${SERVER_COMPOSE_FILE}" ]; then
|
||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" down --remove-orphans
|
||||
else
|
||||
log "server compose/env not found, skip"
|
||||
fi
|
||||
|
||||
stage "Shutdown Finished"
|
||||
@@ -1,152 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_DIR="${ROOT_DIR}/deploy/server"
|
||||
SERVER_ENV_FILE="${SERVER_DIR}/.env"
|
||||
SERVER_COMPOSE_FILE="${SERVER_DIR}/docker-compose.tcp.yml"
|
||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
||||
FORCE=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/04_cleanup.sh [options]
|
||||
|
||||
Remove server generated directories: logs, reports, and RTR database.
|
||||
|
||||
Options:
|
||||
--force Clean even if the server container appears to be running.
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[04_cleanup] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[04_cleanup] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1"
|
||||
local default_value="$2"
|
||||
local line
|
||||
|
||||
line="$(grep -E "^[[:space:]]*${key}=" "${SERVER_ENV_FILE}" | tail -n 1 || true)"
|
||||
if [ -n "${line}" ]; then
|
||||
printf '%s\n' "${line#*=}"
|
||||
else
|
||||
printf '%s\n' "${default_value}"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_host_path() {
|
||||
local raw_path="$1"
|
||||
|
||||
case "${raw_path}" in
|
||||
/*|[A-Za-z]:/*|[A-Za-z]:\\*)
|
||||
printf '%s\n' "${raw_path}"
|
||||
;;
|
||||
*)
|
||||
(cd "${SERVER_DIR}" && cd "${raw_path}" 2>/dev/null && pwd -P) || printf '%s/%s\n' "${SERVER_DIR}" "${raw_path}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
compose_available() {
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
server_running() {
|
||||
local cid state
|
||||
|
||||
[ -f "${SERVER_COMPOSE_FILE}" ] || return 1
|
||||
compose_available || return 1
|
||||
|
||||
cid="$("${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" ps -q rpki-rtr 2>/dev/null || true)"
|
||||
[ -n "${cid}" ] || return 1
|
||||
|
||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
||||
[ "${state}" = "true" ]
|
||||
}
|
||||
|
||||
clean_dir() {
|
||||
local label="$1"
|
||||
local path="$2"
|
||||
local root_with_slash
|
||||
|
||||
if [ ! -d "${path}" ]; then
|
||||
log "${label}: ${path} not found, skip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
root_with_slash="${ROOT_DIR}/"
|
||||
case "${path}/" in
|
||||
"${root_with_slash}"*) ;;
|
||||
*)
|
||||
log "ERROR: ${label} path is outside repo root, refuse to remove: ${path}"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
log "${label}: removing ${path}"
|
||||
if ! rm -rf "${path}"; then
|
||||
log "ERROR: failed to remove ${label}: ${path}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
stage "Checking Server"
|
||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
||||
if [ "${FORCE}" -ne 1 ] && command -v docker >/dev/null 2>&1 && server_running; then
|
||||
die "rpki-rtr is still running, run scripts/03_shutdown.sh first or use --force"
|
||||
fi
|
||||
|
||||
LOG_DIR="${ROOT_DIR}/logs"
|
||||
REPORT_DIR="$(resolve_host_path "$(env_value RPKI_RTR_REPORT_HOST_DIR '../../report')")"
|
||||
DB_DIR="$(resolve_host_path "$(env_value RPKI_RTR_DB_HOST_DIR '../../rtr-db')")"
|
||||
|
||||
stage "Removing Server Files"
|
||||
failed_count=0
|
||||
clean_dir "logs" "${LOG_DIR}" || failed_count=$((failed_count + 1))
|
||||
clean_dir "report" "${REPORT_DIR}" || failed_count=$((failed_count + 1))
|
||||
clean_dir "rtr-db" "${DB_DIR}" || failed_count=$((failed_count + 1))
|
||||
|
||||
if [ "${failed_count}" -gt 0 ]; then
|
||||
die "${failed_count} cleanup item(s) failed"
|
||||
fi
|
||||
|
||||
stage "Cleanup Finished"
|
||||
@@ -1,303 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
OUT_DIR="${ROOT_DIR}/certs"
|
||||
TLS_DIR="${OUT_DIR}/tls"
|
||||
SSH_DIR="${OUT_DIR}/ssh"
|
||||
SERVER_DNS="localhost"
|
||||
FORCE=0
|
||||
DO_TLS=1
|
||||
DO_SSH=1
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Generate local development TLS certificates and SSH keys for RTR Docker deploys.
|
||||
|
||||
Usage:
|
||||
scripts/10_generate-certs.sh [options]
|
||||
|
||||
Options:
|
||||
--out-dir DIR Output directory, default: ./certs
|
||||
--server-dns NAME TLS server dNSName SAN, default: localhost
|
||||
--tls-only Generate only TLS files
|
||||
--ssh-only Generate only SSH files
|
||||
--force Overwrite existing files
|
||||
-h, --help Show this help
|
||||
|
||||
Generated TLS files:
|
||||
certs/tls/client-ca.crt
|
||||
certs/tls/client-ca.key
|
||||
certs/tls/server-dns.crt
|
||||
certs/tls/server-dns.key
|
||||
certs/tls/client-good.crt
|
||||
certs/tls/client-good.key
|
||||
|
||||
Generated SSH files:
|
||||
certs/ssh/ssh_host_rsa_key
|
||||
certs/ssh/ssh_host_rsa_key.pub
|
||||
certs/ssh/rtr-client.key
|
||||
certs/ssh/rtr-client.key.pub
|
||||
certs/ssh/rtr-authorized_keys
|
||||
USAGE
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
info() {
|
||||
echo "[generate-certs] $*"
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
||||
}
|
||||
|
||||
run_quiet() {
|
||||
local log_file
|
||||
log_file="$(mktemp)"
|
||||
if "$@" >"${log_file}" 2>&1; then
|
||||
rm -f "${log_file}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cat "${log_file}" >&2
|
||||
rm -f "${log_file}"
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_abs_out_dir() {
|
||||
case "${OUT_DIR}" in
|
||||
/*) ;;
|
||||
[A-Za-z]:*) ;;
|
||||
*) OUT_DIR="${ROOT_DIR}/${OUT_DIR}" ;;
|
||||
esac
|
||||
TLS_DIR="${OUT_DIR}/tls"
|
||||
SSH_DIR="${OUT_DIR}/ssh"
|
||||
}
|
||||
|
||||
check_overwrite() {
|
||||
local path="$1"
|
||||
if [[ -e "${path}" && "${FORCE}" != "1" ]]; then
|
||||
die "${path} already exists; rerun with --force to overwrite"
|
||||
fi
|
||||
}
|
||||
|
||||
write_tls_config_files() {
|
||||
local tmp_dir="$1"
|
||||
|
||||
cat >"${tmp_dir}/ca.cnf" <<'EOF'
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[ req_distinguished_name ]
|
||||
CN = rpki-rtr-client-ca
|
||||
EOF
|
||||
|
||||
cat >"${tmp_dir}/server.cnf" <<EOF
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[ req_distinguished_name ]
|
||||
CN = ${SERVER_DNS}
|
||||
EOF
|
||||
|
||||
cat >"${tmp_dir}/client.cnf" <<'EOF'
|
||||
[ req ]
|
||||
distinguished_name = req_distinguished_name
|
||||
prompt = no
|
||||
|
||||
[ req_distinguished_name ]
|
||||
CN = rpki-rtr-client
|
||||
EOF
|
||||
|
||||
cat >"${tmp_dir}/server.ext" <<EOF
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage=digitalSignature,keyEncipherment
|
||||
extendedKeyUsage=serverAuth
|
||||
subjectAltName=DNS:${SERVER_DNS}
|
||||
EOF
|
||||
|
||||
cat >"${tmp_dir}/client.ext" <<'EOF'
|
||||
basicConstraints=CA:FALSE
|
||||
keyUsage=digitalSignature,keyEncipherment
|
||||
extendedKeyUsage=clientAuth
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_tls() {
|
||||
need_cmd openssl
|
||||
mkdir -p "${TLS_DIR}"
|
||||
|
||||
info "generating TLS certificates in ${TLS_DIR}"
|
||||
info "TLS server dNSName SAN: ${SERVER_DNS}"
|
||||
|
||||
local files=(
|
||||
"${TLS_DIR}/client-ca.crt"
|
||||
"${TLS_DIR}/client-ca.key"
|
||||
"${TLS_DIR}/server-dns.crt"
|
||||
"${TLS_DIR}/server-dns.key"
|
||||
"${TLS_DIR}/client-good.crt"
|
||||
"${TLS_DIR}/client-good.key"
|
||||
)
|
||||
for file in "${files[@]}"; do
|
||||
check_overwrite "${file}"
|
||||
done
|
||||
|
||||
local tmp_dir
|
||||
tmp_dir="$(mktemp -d)"
|
||||
write_tls_config_files "${tmp_dir}"
|
||||
|
||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-ca.key" 2048
|
||||
info "created TLS CA key: ${TLS_DIR}/client-ca.key"
|
||||
run_quiet openssl req -x509 -new -nodes \
|
||||
-config "${tmp_dir}/ca.cnf" \
|
||||
-key "${TLS_DIR}/client-ca.key" \
|
||||
-sha256 -days 3650 \
|
||||
-out "${TLS_DIR}/client-ca.crt"
|
||||
info "created TLS CA cert: ${TLS_DIR}/client-ca.crt"
|
||||
|
||||
run_quiet openssl genrsa -out "${TLS_DIR}/server-dns.key" 2048
|
||||
info "created TLS server key: ${TLS_DIR}/server-dns.key"
|
||||
run_quiet openssl req -new \
|
||||
-config "${tmp_dir}/server.cnf" \
|
||||
-key "${TLS_DIR}/server-dns.key" \
|
||||
-out "${tmp_dir}/server-dns.csr"
|
||||
run_quiet openssl x509 -req \
|
||||
-in "${tmp_dir}/server-dns.csr" \
|
||||
-CA "${TLS_DIR}/client-ca.crt" \
|
||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
||||
-CAcreateserial \
|
||||
-out "${TLS_DIR}/server-dns.crt" \
|
||||
-days 3650 -sha256 \
|
||||
-extfile "${tmp_dir}/server.ext"
|
||||
info "created TLS server cert: ${TLS_DIR}/server-dns.crt"
|
||||
|
||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-good.key" 2048
|
||||
info "created TLS client key: ${TLS_DIR}/client-good.key"
|
||||
run_quiet openssl req -new \
|
||||
-config "${tmp_dir}/client.cnf" \
|
||||
-key "${TLS_DIR}/client-good.key" \
|
||||
-out "${tmp_dir}/client-good.csr"
|
||||
run_quiet openssl x509 -req \
|
||||
-in "${tmp_dir}/client-good.csr" \
|
||||
-CA "${TLS_DIR}/client-ca.crt" \
|
||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
||||
-CAcreateserial \
|
||||
-out "${TLS_DIR}/client-good.crt" \
|
||||
-days 3650 -sha256 \
|
||||
-extfile "${tmp_dir}/client.ext"
|
||||
info "created TLS client cert: ${TLS_DIR}/client-good.crt"
|
||||
|
||||
rm -f "${TLS_DIR}/client-ca.srl"
|
||||
rm -rf "${tmp_dir}"
|
||||
chmod 600 "${TLS_DIR}"/*.key
|
||||
chmod 644 "${TLS_DIR}"/*.crt
|
||||
}
|
||||
|
||||
generate_ssh_key() {
|
||||
local path="$1"
|
||||
local comment="$2"
|
||||
check_overwrite "${path}"
|
||||
check_overwrite "${path}.pub"
|
||||
rm -f "${path}" "${path}.pub"
|
||||
run_quiet ssh-keygen -q -t rsa -b 3072 -m PEM -N "" -C "${comment}" -f "${path}"
|
||||
}
|
||||
|
||||
generate_ssh() {
|
||||
need_cmd ssh-keygen
|
||||
mkdir -p "${SSH_DIR}"
|
||||
|
||||
info "generating SSH keys in ${SSH_DIR}"
|
||||
generate_ssh_key "${SSH_DIR}/ssh_host_rsa_key" "rpki-rtr-host"
|
||||
info "created SSH host key: ${SSH_DIR}/ssh_host_rsa_key"
|
||||
generate_ssh_key "${SSH_DIR}/rtr-client.key" "rpki-rtr-debug-client"
|
||||
info "created SSH client key: ${SSH_DIR}/rtr-client.key"
|
||||
|
||||
check_overwrite "${SSH_DIR}/rtr-authorized_keys"
|
||||
cp "${SSH_DIR}/rtr-client.key.pub" "${SSH_DIR}/rtr-authorized_keys"
|
||||
info "created SSH authorized_keys: ${SSH_DIR}/rtr-authorized_keys"
|
||||
|
||||
chmod 600 \
|
||||
"${SSH_DIR}/ssh_host_rsa_key" \
|
||||
"${SSH_DIR}/rtr-client.key"
|
||||
chmod 644 "${SSH_DIR}"/*.pub "${SSH_DIR}/rtr-authorized_keys"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out-dir)
|
||||
[[ $# -ge 2 ]] || die "--out-dir requires a value"
|
||||
OUT_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--server-dns)
|
||||
[[ $# -ge 2 ]] || die "--server-dns requires a value"
|
||||
SERVER_DNS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--tls-only)
|
||||
DO_TLS=1
|
||||
DO_SSH=0
|
||||
shift
|
||||
;;
|
||||
--ssh-only)
|
||||
DO_TLS=0
|
||||
DO_SSH=1
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
ensure_abs_out_dir
|
||||
|
||||
if [[ "${DO_TLS}" == "1" ]]; then
|
||||
generate_tls
|
||||
fi
|
||||
|
||||
if [[ "${DO_SSH}" == "1" ]]; then
|
||||
generate_ssh
|
||||
fi
|
||||
|
||||
info "done"
|
||||
info "generated files under ${OUT_DIR}"
|
||||
|
||||
if [[ "${DO_TLS}" == "1" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
TLS files:
|
||||
${TLS_DIR}/client-ca.crt
|
||||
${TLS_DIR}/server-dns.crt
|
||||
${TLS_DIR}/server-dns.key
|
||||
${TLS_DIR}/client-good.crt
|
||||
${TLS_DIR}/client-good.key
|
||||
${TLS_DIR}/client-ca.key (local signing key; not mounted by compose)
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [[ "${DO_SSH}" == "1" ]]; then
|
||||
cat <<EOF
|
||||
|
||||
SSH files:
|
||||
${SSH_DIR}/ssh_host_rsa_key
|
||||
${SSH_DIR}/ssh_host_rsa_key.pub
|
||||
${SSH_DIR}/rtr-client.key
|
||||
${SSH_DIR}/rtr-client.key.pub
|
||||
${SSH_DIR}/rtr-authorized_keys
|
||||
EOF
|
||||
fi
|
||||
@@ -1,136 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.client"
|
||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/11_start_client.sh [options]
|
||||
|
||||
Read root ./environment.client, update deploy/client/.env, then start the RTR debug client.
|
||||
|
||||
Options:
|
||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
||||
--env FILE Client common env file. Default: ./environment.client
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[11_start_client] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[11_start_client] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
set_env_var() {
|
||||
local env_file="$1"
|
||||
local key="$2"
|
||||
local value="$3"
|
||||
local tmp_file
|
||||
|
||||
tmp_file="${env_file}.tmp.$$"
|
||||
awk -v key="${key}" -v value="${value}" '
|
||||
BEGIN { done = 0 }
|
||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
done = 1
|
||||
}
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!done) {
|
||||
print key "=" value
|
||||
}
|
||||
}
|
||||
' "${env_file}" >"${tmp_file}"
|
||||
mv "${tmp_file}" "${env_file}"
|
||||
}
|
||||
|
||||
compose_file_for_mode() {
|
||||
case "$1" in
|
||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--env)
|
||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
||||
COMMON_ENV_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
||||
|
||||
stage "Checking Docker"
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
[ -f "${CLIENT_ENV_FILE}" ] || die "deploy client env file not found: ${CLIENT_ENV_FILE}"
|
||||
[ -f "${CLIENT_COMPOSE_FILE}" ] || die "client compose file not found: ${CLIENT_COMPOSE_FILE}"
|
||||
|
||||
stage "Reading Client Environment"
|
||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
||||
updated_count=0
|
||||
while IFS= read -r line || [ -n "${line}" ]; do
|
||||
case "${line}" in
|
||||
''|\#*) continue ;;
|
||||
*=*)
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
set_env_var "${CLIENT_ENV_FILE}" "${key}" "${value}"
|
||||
updated_count=$((updated_count + 1))
|
||||
;;
|
||||
esac
|
||||
done <"${COMMON_ENV_FILE}"
|
||||
log "updated ${updated_count} client env variable(s)"
|
||||
else
|
||||
log "environment.client file not found, skip env update"
|
||||
fi
|
||||
|
||||
stage "Starting RTR Debug Client"
|
||||
log "mode: ${MODE}"
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" up -d --no-build rtr-debug-client
|
||||
|
||||
log "checking client compose status"
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" ps rtr-debug-client
|
||||
|
||||
stage "Client Started"
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/12_shutdown_client.sh [options]
|
||||
|
||||
Stop the RTR debug client started by scripts/11_start_client.sh.
|
||||
|
||||
Options:
|
||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[12_shutdown_client] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
stage() {
|
||||
printf '\n====== %s ======\n' "$*" >&2
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[12_shutdown_client] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
compose_file_for_mode() {
|
||||
case "$1" in
|
||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
||||
MODE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
||||
|
||||
stage "Checking Docker"
|
||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
||||
if docker compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker compose)
|
||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
||||
COMPOSE_BIN=(docker-compose)
|
||||
else
|
||||
die "docker compose is not available"
|
||||
fi
|
||||
|
||||
stage "Stopping RTR Debug Client"
|
||||
log "mode: ${MODE}"
|
||||
if [ -f "${CLIENT_ENV_FILE}" ] && [ -f "${CLIENT_COMPOSE_FILE}" ]; then
|
||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" down --remove-orphans
|
||||
else
|
||||
log "client compose/env not found, skip"
|
||||
fi
|
||||
|
||||
stage "Client Shutdown Finished"
|
||||
@@ -1,218 +0,0 @@
|
||||
# 脚本说明
|
||||
|
||||
`scripts/` 目录用于离线安装、启动、健康检查、关闭和清理。
|
||||
|
||||
所有脚本建议在仓库根目录执行:
|
||||
|
||||
```bash
|
||||
bash scripts/00_0_install.sh
|
||||
```
|
||||
|
||||
## 目录
|
||||
|
||||
- [推荐流程](#推荐流程)
|
||||
- [环境变量文件](#环境变量文件)
|
||||
- [00 安装](#00-安装)
|
||||
- [00 Pre-Check](#00-pre-check)
|
||||
- [01 启动 Server 和 Console](#01-启动-server-和-console)
|
||||
- [02 自检](#02-自检)
|
||||
- [03 关闭 Server 和 Console](#03-关闭-server-和-console)
|
||||
- [04 清理](#04-清理)
|
||||
- [10 生成证书](#10-生成证书)
|
||||
- [11 启动 Client](#11-启动-client)
|
||||
- [12 关闭 Client](#12-关闭-client)
|
||||
- [Windows 路径注意事项](#windows-路径注意事项)
|
||||
|
||||
## 推荐流程
|
||||
|
||||
```bash
|
||||
# 1. 加载镜像并执行 pre-check。
|
||||
bash scripts/00_0_install.sh
|
||||
|
||||
# 2. 启动真实 server 和 console。
|
||||
bash scripts/01_start.sh --mode tcp
|
||||
|
||||
# 3. 检查 server 和 console 是否正常。
|
||||
bash scripts/02_self_check.sh
|
||||
|
||||
# 4. 关闭 server 和 console。
|
||||
bash scripts/03_shutdown.sh --mode tcp
|
||||
|
||||
# 5. 清理 server 运行产生的文件。
|
||||
bash scripts/04_cleanup.sh
|
||||
```
|
||||
|
||||
client 单独启动和关闭:
|
||||
|
||||
```bash
|
||||
bash scripts/11_start_client.sh --mode tcp
|
||||
bash scripts/12_shutdown_client.sh --mode tcp
|
||||
```
|
||||
|
||||
支持的模式:
|
||||
|
||||
- server:`tcp`、`tls`、`ssh`、`base`
|
||||
- client:`tcp`、`tls`、`ssh`
|
||||
|
||||
## 环境变量文件
|
||||
|
||||
`01_start.sh` 会读取根目录下的 `environment.server`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/server/.env`。
|
||||
|
||||
示例:
|
||||
|
||||
```env
|
||||
RPKI_RTR_CCR_HOST_DIR=/opt/rpki-rtr/data
|
||||
RPKI_RTR_SLURM_HOST_DIR=/opt/rpki-rtr/data
|
||||
RPKI_RTR_DB_HOST_DIR=/opt/rpki-rtr/rtr-db
|
||||
RPKI_RTR_LOG_HOST_DIR=/opt/rpki-rtr/logs/server
|
||||
RPKI_RTR_REPORT_HOST_DIR=/opt/rpki-rtr/report
|
||||
```
|
||||
|
||||
`11_start_client.sh` 会读取根目录下的 `environment.client`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`。
|
||||
|
||||
如果 `environment.server` 或 `environment.client` 不存在,对应脚本会跳过更新,直接使用已有的 deploy `.env` 文件。
|
||||
|
||||
## 00 安装
|
||||
|
||||
`00_0_install.sh` 用于从 `images/` 目录加载离线 Docker 镜像,更新 deploy `.env` 中的镜像名,然后执行 pre-check。
|
||||
|
||||
默认镜像包查找规则:
|
||||
|
||||
- server:`images/rpki-rtr-*.tar`,排除 `rpki-rtr-debug-client-*.tar`
|
||||
- client:`images/rpki-rtr-debug-client-*.tar`
|
||||
- console:`images/rpki-console-ui-*.tar`
|
||||
|
||||
也可以显式指定镜像包:
|
||||
|
||||
```bash
|
||||
bash scripts/00_0_install.sh \
|
||||
--server-tar images/rpki-rtr-amd64-v1.2.tar \
|
||||
--client-tar images/rpki-rtr-debug-client-amd64-v1.2.tar \
|
||||
--console-tar images/rpki-console-ui-amd64-v1.0.tar
|
||||
```
|
||||
|
||||
脚本会从 `docker load` 输出中解析真实镜像名,并写入:
|
||||
|
||||
- `deploy/server/.env` 的 `RPKI_RTR_SERVER_IMAGE`
|
||||
- `deploy/console/.env` 的 `RPKI_CONSOLE_UI_IMAGE`
|
||||
|
||||
## 00 Pre-Check
|
||||
|
||||
`00_1_pre_check.sh` 会在 `.precheck/` 下准备隔离测试数据,使用仓库里的 Docker Compose 文件启动 server 和 debug client,然后检查:
|
||||
|
||||
- admin health 接口返回正常。
|
||||
- server 写出了 source report。
|
||||
- debug client 收到了 EndOfData 和 payload summary。
|
||||
- pre-check 日志中没有 fatal 错误模式。
|
||||
|
||||
相关辅助脚本:
|
||||
|
||||
- `00_2_generate_ccr.sh`:生成固定的 CCR 测试文件。
|
||||
- `00_2_pre_check_prepare.sh`:创建 `.precheck/` 数据、SLURM 文件和 env 文件。
|
||||
- `00_3_pre_check_cleanup.sh`:停止 pre-check 容器并清理 `.precheck/`。
|
||||
|
||||
## 01 启动 Server 和 Console
|
||||
|
||||
`01_start.sh` 用于启动真实的 RTR server 和 console。
|
||||
|
||||
```bash
|
||||
bash scripts/01_start.sh --mode tcp
|
||||
```
|
||||
|
||||
启动后脚本会检查容器是否进入 `running` 状态。如果容器启动后退出,脚本会打印 compose 状态和最近日志,然后报错退出,不会继续打印 `Start Finished`。
|
||||
|
||||
常用参数:
|
||||
|
||||
```bash
|
||||
bash scripts/01_start.sh --mode tls
|
||||
bash scripts/01_start.sh --mode ssh
|
||||
bash scripts/01_start.sh --wait 60
|
||||
bash scripts/01_start.sh --env ./environment.server
|
||||
```
|
||||
|
||||
注意:console 不读取 `environment.server`,只使用 `deploy/console/.env`。
|
||||
|
||||
## 02 自检
|
||||
|
||||
`02_self_check.sh` 用于检查 `01_start.sh` 启动的 server 和 console 是否正常。
|
||||
|
||||
```bash
|
||||
bash scripts/02_self_check.sh
|
||||
```
|
||||
|
||||
检查内容包括:
|
||||
|
||||
- server 容器是否 running。
|
||||
- console 容器是否 running。
|
||||
- RTR TCP 端口是否可连接。
|
||||
- admin health 接口是否健康。
|
||||
- console 端口和 HTTP 根路径是否可访问。
|
||||
|
||||
如果启动较慢,可以增加等待时间:
|
||||
|
||||
```bash
|
||||
bash scripts/02_self_check.sh --wait 120
|
||||
```
|
||||
|
||||
## 03 关闭 Server 和 Console
|
||||
|
||||
`03_shutdown.sh` 用于关闭 `01_start.sh` 启动的服务。
|
||||
|
||||
```bash
|
||||
bash scripts/03_shutdown.sh --mode tcp
|
||||
```
|
||||
|
||||
脚本会先关闭 console,再关闭 RTR server。
|
||||
|
||||
## 04 清理
|
||||
|
||||
`04_cleanup.sh` 用于删除 server 运行产生的目录:
|
||||
|
||||
- `logs/`
|
||||
- `report/`
|
||||
- `rtr-db/`
|
||||
|
||||
推荐先关闭服务,再清理:
|
||||
|
||||
```bash
|
||||
bash scripts/03_shutdown.sh
|
||||
bash scripts/04_cleanup.sh
|
||||
```
|
||||
|
||||
清理前脚本会检查 `rpki-rtr` 是否仍在运行。如果仍在运行,会拒绝清理。只有明确需要强制清理时才使用:
|
||||
|
||||
```bash
|
||||
bash scripts/04_cleanup.sh --force
|
||||
```
|
||||
|
||||
清理脚本会拒绝删除仓库根目录之外的路径。如果某一项清理失败,会继续清理后续项,最后返回失败。
|
||||
|
||||
## 10 生成证书
|
||||
|
||||
`10_generate_certs.sh` 用于生成 TLS/SSH 模式需要的本地证书材料(测试用)。
|
||||
|
||||
```bash
|
||||
bash scripts/10_generate_certs.sh
|
||||
```
|
||||
|
||||
## 11 启动 Client
|
||||
|
||||
`11_start_client.sh` 用于启动 RTR debug client。
|
||||
|
||||
```bash
|
||||
bash scripts/11_start_client.sh --mode tcp
|
||||
bash scripts/11_start_client.sh --mode tls
|
||||
bash scripts/11_start_client.sh --mode ssh
|
||||
```
|
||||
|
||||
如果 `environment.client` 存在,脚本会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`;如果不存在,则直接使用已有的 `deploy/client/.env`。
|
||||
|
||||
## 12 关闭 Client
|
||||
|
||||
`12_shutdown_client.sh` 用于关闭 `11_start_client.sh` 启动的 client。
|
||||
|
||||
```bash
|
||||
bash scripts/12_shutdown_client.sh --mode tcp
|
||||
```
|
||||
|
||||
关闭时建议使用和启动时相同的 mode。
|
||||
70
scripts/benchmark/README.md
Normal file
70
scripts/benchmark/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# RPKI Benchmarks (Stage2, selected_der_v2)
|
||||
|
||||
This directory contains a reproducible, one-click benchmark to measure **decode + profile validate**
|
||||
performance for all supported object types and compare **OURS** against the **Routinator baseline**
|
||||
(`rpki` crate `=0.19.1` with `repository` feature).
|
||||
|
||||
## What it measures
|
||||
|
||||
Dataset:
|
||||
|
||||
- Fixtures: `rpki/tests/benchmark/selected_der_v2/`
|
||||
- Objects: `cer`, `crl`, `manifest` (`.mft`), `roa`, `aspa` (`.asa`)
|
||||
- Samples: 10 quantiles per type (`min/p01/p10/p25/p50/p75/p90/p95/p99/max`) → 50 files total
|
||||
|
||||
Metrics:
|
||||
|
||||
- **decode+validate**: `decode_der` (parse + profile validate) for each object file
|
||||
- **landing** (OURS only): `PackFile::from_bytes_compute_sha256` + CBOR encode + `RocksDB put_raw`
|
||||
- **compare**: ratio `ours_ns/op ÷ rout_ns/op` for decode+validate
|
||||
|
||||
## Default benchmark settings
|
||||
|
||||
Both OURS and Routinator baseline use the same run settings:
|
||||
|
||||
- warmup: `10` iterations
|
||||
- rounds: `3`
|
||||
- adaptive loop target: `min_round_ms=200` (with an internal max of `1_000_000` iters)
|
||||
- strict DER: `true` (baseline)
|
||||
- cert inspect: `false` (baseline)
|
||||
|
||||
You can override the settings via environment variables in the runner script:
|
||||
|
||||
- `BENCH_WARMUP_ITERS` (default `10`)
|
||||
- `BENCH_ROUNDS` (default `3`)
|
||||
- `BENCH_MIN_ROUND_MS` (default `200`)
|
||||
|
||||
## One-click run (OURS + Routinator compare)
|
||||
|
||||
From the `rpki/` crate directory:
|
||||
|
||||
```bash
|
||||
./scripts/benchmark/run_stage2_selected_der_v2_release.sh
|
||||
```
|
||||
|
||||
Outputs are written under:
|
||||
|
||||
- `rpki/target/bench/`
|
||||
- OURS decode+validate: `stage2_selected_der_v2_decode_release_<TS>.{md,csv}`
|
||||
- OURS landing: `stage2_selected_der_v2_landing_release_<TS>.{md,csv}`
|
||||
- Routinator: `stage2_selected_der_v2_routinator_decode_release_<TS>.{md,csv}`
|
||||
- Compare: `stage2_selected_der_v2_compare_ours_vs_routinator_decode_release_<TS>.{md,csv}`
|
||||
- Summary: `stage2_selected_der_v2_compare_summary_<TS>.md`
|
||||
|
||||
### Why decode and landing are separated
|
||||
|
||||
The underlying benchmark can run in `BENCH_MODE=both`, but the **landing** part writes to RocksDB
|
||||
and may trigger background work (e.g., compactions) that can **skew subsequent decode timings**.
|
||||
For a fair OURS-vs-Routinator comparison, the runner script:
|
||||
|
||||
- runs `BENCH_MODE=decode_validate` for comparison, and
|
||||
- runs `BENCH_MODE=landing` separately for landing-only numbers.
|
||||
|
||||
## Notes
|
||||
|
||||
- The Routinator baseline benchmark is implemented in-repo under:
|
||||
- `rpki/benchmark/routinator_object_bench/`
|
||||
- It pins `rpki = "=0.19.1"` in its `Cargo.toml`.
|
||||
- This benchmark is implemented as an `#[ignore]` integration test:
|
||||
- `rpki/tests/bench_stage2_decode_profile_selected_der_v2.rs`
|
||||
- The runner script invokes it with `cargo test --release ... -- --ignored --nocapture`.
|
||||
123
scripts/benchmark/run_stage2_selected_der_v2_release.sh
Executable file
123
scripts/benchmark/run_stage2_selected_der_v2_release.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Stage2 (selected_der_v2) decode+profile validate benchmark.
|
||||
# Runs:
|
||||
# 1) OURS decode+validate benchmark and writes MD/CSV.
|
||||
# 2) OURS landing benchmark and writes MD/CSV.
|
||||
# 3) Routinator baseline decode benchmark (rpki crate =0.19.1).
|
||||
# 4) Produces a joined compare CSV/MD and a short geomean summary.
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
OUT_DIR="$ROOT_DIR/target/bench"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
|
||||
WARMUP_ITERS="${BENCH_WARMUP_ITERS:-10}"
|
||||
ROUNDS="${BENCH_ROUNDS:-3}"
|
||||
MIN_ROUND_MS="${BENCH_MIN_ROUND_MS:-200}"
|
||||
|
||||
OURS_MD="$OUT_DIR/stage2_selected_der_v2_decode_release_${TS}.md"
|
||||
OURS_CSV="$OUT_DIR/stage2_selected_der_v2_decode_release_${TS}.csv"
|
||||
|
||||
OURS_LANDING_MD="$OUT_DIR/stage2_selected_der_v2_landing_release_${TS}.md"
|
||||
OURS_LANDING_CSV="$OUT_DIR/stage2_selected_der_v2_landing_release_${TS}.csv"
|
||||
|
||||
ROUT_MD="$OUT_DIR/stage2_selected_der_v2_routinator_decode_release_${TS}.md"
|
||||
ROUT_CSV="$OUT_DIR/stage2_selected_der_v2_routinator_decode_release_${TS}.csv"
|
||||
|
||||
COMPARE_MD="$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release_${TS}.md"
|
||||
COMPARE_CSV="$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release_${TS}.csv"
|
||||
|
||||
SUMMARY_MD="$OUT_DIR/stage2_selected_der_v2_compare_summary_${TS}.md"
|
||||
|
||||
echo "[1/4] OURS: decode+validate benchmark (release)..." >&2
|
||||
BENCH_MODE="decode_validate" \
|
||||
BENCH_WARMUP_ITERS="$WARMUP_ITERS" \
|
||||
BENCH_ROUNDS="$ROUNDS" \
|
||||
BENCH_MIN_ROUND_MS="$MIN_ROUND_MS" \
|
||||
BENCH_OUT_MD="$OURS_MD" \
|
||||
BENCH_OUT_CSV="$OURS_CSV" \
|
||||
cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture >/dev/null
|
||||
|
||||
echo "[2/4] OURS: landing benchmark (release)..." >&2
|
||||
BENCH_MODE="landing" \
|
||||
BENCH_WARMUP_ITERS="$WARMUP_ITERS" \
|
||||
BENCH_ROUNDS="$ROUNDS" \
|
||||
BENCH_MIN_ROUND_MS="$MIN_ROUND_MS" \
|
||||
BENCH_OUT_MD_LANDING="$OURS_LANDING_MD" \
|
||||
BENCH_OUT_CSV_LANDING="$OURS_LANDING_CSV" \
|
||||
cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture >/dev/null
|
||||
|
||||
echo "[3/4] Routinator baseline + compare join..." >&2
|
||||
OURS_CSV="$OURS_CSV" \
|
||||
ROUT_CSV="$ROUT_CSV" \
|
||||
ROUT_MD="$ROUT_MD" \
|
||||
COMPARE_CSV="$COMPARE_CSV" \
|
||||
COMPARE_MD="$COMPARE_MD" \
|
||||
WARMUP_ITERS="$WARMUP_ITERS" \
|
||||
ROUNDS="$ROUNDS" \
|
||||
MIN_ROUND_MS="$MIN_ROUND_MS" \
|
||||
scripts/stage2_perf_compare_m4.sh >/dev/null
|
||||
|
||||
echo "[4/4] Summary (geomean ratios)..." >&2
|
||||
python3 - "$COMPARE_CSV" "$SUMMARY_MD" <<'PY'
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
in_csv = Path(sys.argv[1])
|
||||
out_md = Path(sys.argv[2])
|
||||
|
||||
rows = list(csv.DictReader(in_csv.open(newline="")))
|
||||
ratios = {}
|
||||
for r in rows:
|
||||
ratios.setdefault(r["type"], []).append(float(r["ratio_ours_over_rout"]))
|
||||
|
||||
def geomean(vals):
|
||||
return math.exp(sum(math.log(v) for v in vals) / len(vals))
|
||||
|
||||
def p50(vals):
|
||||
v = sorted(vals)
|
||||
n = len(v)
|
||||
if n % 2 == 1:
|
||||
return v[n // 2]
|
||||
return (v[n // 2 - 1] + v[n // 2]) / 2.0
|
||||
|
||||
all_vals = [float(r["ratio_ours_over_rout"]) for r in rows]
|
||||
types = ["all"] + sorted(ratios.keys())
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
lines = []
|
||||
lines.append("# Stage2 selected_der_v2 compare summary (release)\n\n")
|
||||
lines.append(f"- recorded_at_utc: `{now}`\n")
|
||||
lines.append(f"- inputs_csv: `{in_csv}`\n\n")
|
||||
lines.append("| type | n | min | p50 | geomean | max | >1 count |\n")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|\n")
|
||||
|
||||
for t in types:
|
||||
vals = all_vals if t == "all" else ratios[t]
|
||||
vals_sorted = sorted(vals)
|
||||
lines.append(
|
||||
f"| {t} | {len(vals_sorted)} | {vals_sorted[0]:.4f} | {p50(vals_sorted):.4f} | "
|
||||
f"{geomean(vals_sorted):.4f} | {vals_sorted[-1]:.4f} | {sum(1 for v in vals_sorted if v>1.0)} |\n"
|
||||
)
|
||||
|
||||
out_md.write_text("".join(lines), encoding="utf-8")
|
||||
print(out_md)
|
||||
PY
|
||||
|
||||
echo "Done." >&2
|
||||
echo "- OURS decode MD: $OURS_MD" >&2
|
||||
echo "- OURS decode CSV: $OURS_CSV" >&2
|
||||
echo "- OURS landing MD: $OURS_LANDING_MD" >&2
|
||||
echo "- OURS landing CSV: $OURS_LANDING_CSV" >&2
|
||||
echo "- Routinator: $ROUT_MD" >&2
|
||||
echo "- Compare MD: $COMPARE_MD" >&2
|
||||
echo "- Compare CSV: $COMPARE_CSV" >&2
|
||||
echo "- Summary MD: $SUMMARY_MD" >&2
|
||||
80
scripts/coverage.sh
Executable file
80
scripts/coverage.sh
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Requires:
|
||||
# rustup component add llvm-tools-preview
|
||||
# cargo install cargo-llvm-cov --locked
|
||||
|
||||
run_out="$(mktemp)"
|
||||
text_out="$(mktemp)"
|
||||
html_out="$(mktemp)"
|
||||
|
||||
cleanup() {
|
||||
rm -f "$run_out" "$text_out" "$html_out"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Preserve colored output even though we post-process output by running under a pseudo-TTY.
|
||||
# We run tests only once, then generate both CLI text + HTML reports without rerunning tests.
|
||||
set +e
|
||||
|
||||
cargo llvm-cov clean --workspace >/dev/null 2>&1
|
||||
|
||||
# 1) Run tests once to collect coverage data (no report).
|
||||
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov --no-report" "$run_out" >/dev/null 2>&1
|
||||
run_status="$?"
|
||||
|
||||
# 2) CLI summary report + fail-under gate (no test rerun).
|
||||
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --fail-under-lines 90" "$text_out" >/dev/null 2>&1
|
||||
text_status="$?"
|
||||
|
||||
# 3) HTML report (no test rerun).
|
||||
script -q -e -c "CARGO_TERM_COLOR=always cargo llvm-cov report --html" "$html_out" >/dev/null 2>&1
|
||||
html_status="$?"
|
||||
|
||||
set -e
|
||||
|
||||
strip_script_noise() {
|
||||
tr -d '\r' | sed '/^Script \(started\|done\) on /d'
|
||||
}
|
||||
|
||||
strip_ansi_for_parse() {
|
||||
awk '
|
||||
{
|
||||
line = $0
|
||||
gsub(/\033\[[0-9;]*[A-Za-z]/, "", line) # CSI escapes
|
||||
gsub(/\033\([A-Za-z]/, "", line) # charset escapes (e.g., ESC(B)
|
||||
gsub(/\r/, "", line)
|
||||
print line
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
cat "$run_out" | strip_script_noise
|
||||
cat "$text_out" | strip_script_noise
|
||||
cat "$html_out" | strip_script_noise
|
||||
|
||||
cat "$run_out" | strip_ansi_for_parse | awk '
|
||||
BEGIN {
|
||||
passed=0; failed=0; ignored=0; measured=0; filtered=0;
|
||||
}
|
||||
/^test result: / {
|
||||
if (match($0, /([0-9]+) passed; ([0-9]+) failed; ([0-9]+) ignored; ([0-9]+) measured; ([0-9]+) filtered out;/, m)) {
|
||||
passed += m[1]; failed += m[2]; ignored += m[3]; measured += m[4]; filtered += m[5];
|
||||
}
|
||||
}
|
||||
END {
|
||||
executed = passed + failed;
|
||||
total = passed + failed + ignored + measured;
|
||||
printf("\nTEST SUMMARY (all suites): passed=%d failed=%d ignored=%d measured=%d filtered_out=%d executed=%d total=%d\n",
|
||||
passed, failed, ignored, measured, filtered, executed, total);
|
||||
}
|
||||
'
|
||||
|
||||
echo
|
||||
echo "HTML report: target/llvm-cov/html/index.html"
|
||||
|
||||
status="$text_status"
|
||||
if [ "$run_status" -ne 0 ]; then status="$run_status"; fi
|
||||
if [ "$html_status" -ne 0 ]; then status="$html_status"; fi
|
||||
exit "$status"
|
||||
195
scripts/manifest_perf_compare_m2.sh
Executable file
195
scripts/manifest_perf_compare_m2.sh
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# M2: Run per-sample decode+profile benchmark (Ours vs Routinator) on selected_der fixtures.
|
||||
#
|
||||
# Outputs:
|
||||
# - specs/develop/20260224/data/m2_manifest_decode_profile_compare.csv
|
||||
# - specs/develop/20260224/data/m2_raw.log
|
||||
#
|
||||
# Note: This script assumes Routinator benchmark repo exists at:
|
||||
# /home/yuyr/dev/rust_playground/routinator/benchmark
|
||||
#
|
||||
# It also assumes fixtures exist under:
|
||||
# rpki/tests/benchmark/selected_der/*.mft
|
||||
# routinator/benchmark/fixtures/selected_der/*.mft
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RPKI_DIR="$ROOT_DIR"
|
||||
OURS_BENCH_DIR="$RPKI_DIR/benchmark/ours_manifest_bench"
|
||||
|
||||
ROUT_BENCH_DIR="${ROUT_BENCH_DIR:-/home/yuyr/dev/rust_playground/routinator/benchmark}"
|
||||
ROUT_BIN="$ROUT_BENCH_DIR/target/release/routinator-manifest-benchmark"
|
||||
|
||||
DATE_TAG="${DATE_TAG:-20260224}"
|
||||
OUT_DIR="$RPKI_DIR/../specs/develop/${DATE_TAG}/data"
|
||||
OUT_CSV="${OUT_CSV:-$OUT_DIR/m2_manifest_decode_profile_compare.csv}"
|
||||
OUT_RAW="${OUT_RAW:-$OUT_DIR/m2_raw.log}"
|
||||
|
||||
REPEATS="${REPEATS:-3}"
|
||||
|
||||
# Iterations / warmups (kept moderate for interactive iteration).
|
||||
ITER_SMALL="${ITER_SMALL:-20000}"
|
||||
ITER_MEDIUM="${ITER_MEDIUM:-20000}"
|
||||
ITER_LARGE="${ITER_LARGE:-20000}"
|
||||
ITER_XLARGE="${ITER_XLARGE:-2000}"
|
||||
|
||||
WARM_SMALL="${WARM_SMALL:-2000}"
|
||||
WARM_MEDIUM="${WARM_MEDIUM:-2000}"
|
||||
WARM_LARGE="${WARM_LARGE:-2000}"
|
||||
WARM_XLARGE="${WARM_XLARGE:-200}"
|
||||
|
||||
SAMPLES=(
|
||||
small-01
|
||||
small-02
|
||||
medium-01
|
||||
medium-02
|
||||
large-01
|
||||
large-02
|
||||
xlarge-01
|
||||
xlarge-02
|
||||
)
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
: > "$OUT_RAW"
|
||||
|
||||
echo "sample,bucket,manifest_file_count,ours_avg_ns_per_op,ours_ops_per_s,rout_avg_ns_per_op,rout_ops_per_s,ratio_ours_over_rout,iterations,repeats,warmup" > "$OUT_CSV"
|
||||
|
||||
echo "[1/3] Build ours benchmark (release)..." | tee -a "$OUT_RAW"
|
||||
(cd "$OURS_BENCH_DIR" && cargo build --release -q)
|
||||
OURS_BIN="$OURS_BENCH_DIR/target/release/ours-manifest-bench"
|
||||
|
||||
echo "[2/3] Build routinator benchmark (release)..." | tee -a "$OUT_RAW"
|
||||
(cd "$ROUT_BENCH_DIR" && cargo build --release -q)
|
||||
|
||||
taskset_prefix=""
|
||||
if command -v taskset >/dev/null 2>&1; then
|
||||
if [[ -n "${TASKSET_CPU:-}" ]]; then
|
||||
taskset_prefix="taskset -c ${TASKSET_CPU}"
|
||||
fi
|
||||
fi
|
||||
|
||||
bucket_for() {
|
||||
local s="$1"
|
||||
case "$s" in
|
||||
small-*) echo "small" ;;
|
||||
medium-*) echo "medium" ;;
|
||||
large-*) echo "large" ;;
|
||||
xlarge-*) echo "xlarge" ;;
|
||||
*) echo "unknown" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
iters_for() {
|
||||
local b="$1"
|
||||
case "$b" in
|
||||
small) echo "$ITER_SMALL" ;;
|
||||
medium) echo "$ITER_MEDIUM" ;;
|
||||
large) echo "$ITER_LARGE" ;;
|
||||
xlarge) echo "$ITER_XLARGE" ;;
|
||||
*) echo "$ITER_MEDIUM" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
warm_for() {
|
||||
local b="$1"
|
||||
case "$b" in
|
||||
small) echo "$WARM_SMALL" ;;
|
||||
medium) echo "$WARM_MEDIUM" ;;
|
||||
large) echo "$WARM_LARGE" ;;
|
||||
xlarge) echo "$WARM_XLARGE" ;;
|
||||
*) echo "$WARM_MEDIUM" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_ours() {
|
||||
local sample="$1"
|
||||
local iters="$2"
|
||||
local warm="$3"
|
||||
local ours_fixture="$RPKI_DIR/tests/benchmark/selected_der/${sample}.mft"
|
||||
if [[ ! -f "$ours_fixture" ]]; then
|
||||
echo "ours fixture not found: $ours_fixture" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "### ours $sample" >> "$OUT_RAW"
|
||||
local out
|
||||
out=$($taskset_prefix "$OURS_BIN" --manifest "$ours_fixture" --iterations "$iters" --warmup-iterations "$warm" --repeats "$REPEATS")
|
||||
echo "$out" >> "$OUT_RAW"
|
||||
|
||||
local line
|
||||
line=$(echo "$out" | rg "^\\| ${sample} \\|" | tail -n 1)
|
||||
if [[ -z "${line:-}" ]]; then
|
||||
echo "failed to parse ours output for $sample" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Expected final row: | sample | avg ns/op | ops/s | file count |
|
||||
local avg ops cnt
|
||||
avg=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$3); print $3}')
|
||||
ops=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$4); print $4}')
|
||||
cnt=$(echo "$line" | awk -F'|' '{gsub(/^ +| +$/,"",$5); print $5}')
|
||||
echo "$avg,$ops,$cnt"
|
||||
}
|
||||
|
||||
run_rout() {
|
||||
local sample="$1"
|
||||
local iters="$2"
|
||||
local warm="$3"
|
||||
local rout_fixture="$ROUT_BENCH_DIR/fixtures/selected_der/${sample}.mft"
|
||||
if [[ ! -f "$rout_fixture" ]]; then
|
||||
echo "routinator fixture not found: $rout_fixture" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "### routinator $sample" >> "$OUT_RAW"
|
||||
local out
|
||||
out=$(
|
||||
$taskset_prefix "$ROUT_BIN" \
|
||||
--target decode_only \
|
||||
--manifest "$rout_fixture" \
|
||||
--issuer "$ROUT_BENCH_DIR/fixtures/ta.cer" \
|
||||
--iterations "$iters" \
|
||||
--repeats "$REPEATS" \
|
||||
--warmup-iterations "$warm" \
|
||||
--strict false
|
||||
)
|
||||
echo "$out" >> "$OUT_RAW"
|
||||
|
||||
local avg_line cnt_line
|
||||
avg_line=$(echo "$out" | rg "^ avg:")
|
||||
cnt_line=$(echo "$out" | rg "^ manifest_file_count:")
|
||||
|
||||
local avg_ns ops_s cnt
|
||||
avg_ns=$(echo "$avg_line" | awk '{print $2}')
|
||||
ops_s=$(echo "$avg_line" | awk '{gsub(/[()]/,"",$4); print $4}')
|
||||
cnt=$(echo "$cnt_line" | awk '{print $2}')
|
||||
echo "$avg_ns,$ops_s,$cnt"
|
||||
}
|
||||
|
||||
echo "[3/3] Run per-sample benchmarks..." | tee -a "$OUT_RAW"
|
||||
for s in "${SAMPLES[@]}"; do
|
||||
b=$(bucket_for "$s")
|
||||
it=$(iters_for "$b")
|
||||
warm=$(warm_for "$b")
|
||||
|
||||
IFS=, read -r ours_avg ours_ops ours_cnt < <(run_ours "$s" "$it" "$warm")
|
||||
IFS=, read -r rout_avg rout_ops rout_cnt < <(run_rout "$s" "$it" "$warm")
|
||||
|
||||
if [[ "$ours_cnt" != "$rout_cnt" ]]; then
|
||||
echo "WARNING: file count differs for $s (ours=$ours_cnt rout=$rout_cnt)" | tee -a "$OUT_RAW"
|
||||
fi
|
||||
|
||||
ratio=$(python3 - <<PY
|
||||
o=float("$ours_avg")
|
||||
r=float("$rout_avg")
|
||||
print(f"{(o/r):.4f}" if r != 0 else "inf")
|
||||
PY
|
||||
)
|
||||
|
||||
echo "$s,$b,$ours_cnt,$ours_avg,$ours_ops,$rout_avg,$rout_ops,$ratio,$it,$REPEATS,$warm" >> "$OUT_CSV"
|
||||
echo >> "$OUT_RAW"
|
||||
done
|
||||
|
||||
echo "Done."
|
||||
echo "- CSV: $OUT_CSV"
|
||||
echo "- Raw: $OUT_RAW"
|
||||
142
scripts/manifest_perf_profile_m3.sh
Executable file
142
scripts/manifest_perf_profile_m3.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# M3: Generate flamegraphs + top hotspots for Manifest decode+profile (Ours vs Routinator).
|
||||
#
|
||||
# Outputs under:
|
||||
# specs/develop/20260224/flamegraph/
|
||||
# specs/develop/20260224/hotspots/
|
||||
# specs/develop/20260224/perf/
|
||||
#
|
||||
# Notes:
|
||||
# - On WSL2, /usr/bin/perf is often a wrapper that fails. This script uses a real perf binary
|
||||
# from /usr/lib/linux-tools/*/perf (if present).
|
||||
# - Ours profiling uses perf + flamegraph --perfdata to avoid rebuilding the whole crate graph
|
||||
# with RocksDB.
|
||||
|
||||
ROOT_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
RPKI_DIR="$ROOT_REPO/rpki"
|
||||
|
||||
DATE_TAG="${DATE_TAG:-20260224}"
|
||||
OUT_BASE="$ROOT_REPO/specs/develop/${DATE_TAG}"
|
||||
OUT_FLAME="$OUT_BASE/flamegraph"
|
||||
OUT_HOT="$OUT_BASE/hotspots"
|
||||
OUT_PERF="$OUT_BASE/perf"
|
||||
|
||||
RUN_TAG="${RUN_TAG:-p2}"
|
||||
|
||||
OURS_BENCH_DIR="$RPKI_DIR/benchmark/ours_manifest_bench"
|
||||
OURS_BIN="$OURS_BENCH_DIR/target/release/ours-manifest-bench"
|
||||
|
||||
ROUT_BENCH_DIR="${ROUT_BENCH_DIR:-/home/yuyr/dev/rust_playground/routinator/benchmark}"
|
||||
ROUT_BIN="$ROUT_BENCH_DIR/target/release/routinator-manifest-benchmark"
|
||||
ROUT_ISSUER="$ROUT_BENCH_DIR/fixtures/ta.cer"
|
||||
|
||||
PROFILE_HZ="${PROFILE_HZ:-99}"
|
||||
|
||||
mkdir -p "$OUT_FLAME" "$OUT_HOT" "$OUT_PERF"
|
||||
|
||||
PERF_WRAPPER_OUT="$(perf --version 2>&1 || true)"
|
||||
PERF_REAL=""
|
||||
if echo "${PERF_WRAPPER_OUT}" | grep -q "WARNING: perf not found for kernel"; then
|
||||
PERF_REAL="$(ls -1 /usr/lib/linux-tools/*/perf 2>/dev/null | head -n 1 || true)"
|
||||
else
|
||||
PERF_REAL="$(command -v perf || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "${PERF_REAL}" ]]; then
|
||||
echo "ERROR: usable perf binary not found (wrapper detected and no /usr/lib/linux-tools/*/perf)." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SHIM_DIR="$RPKI_DIR/target/bench/tools"
|
||||
mkdir -p "$SHIM_DIR"
|
||||
cat > "$SHIM_DIR/perf" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
exec "${PERF_REAL}" "\$@"
|
||||
EOF
|
||||
chmod +x "$SHIM_DIR/perf"
|
||||
export PATH="$SHIM_DIR:$PATH"
|
||||
|
||||
echo "Using perf: $PERF_REAL"
|
||||
|
||||
echo "[1/3] Build ours benchmark with frame pointers..."
|
||||
(cd "$OURS_BENCH_DIR" && RUSTFLAGS="-C force-frame-pointers=yes" cargo build --release -q)
|
||||
|
||||
echo "[2/3] Build routinator benchmark (release)..."
|
||||
(cd "$ROUT_BENCH_DIR" && cargo build --release -q)
|
||||
|
||||
taskset_prefix=""
|
||||
if command -v taskset >/dev/null 2>&1; then
|
||||
taskset_prefix="taskset -c 0"
|
||||
fi
|
||||
|
||||
profile_ours() {
|
||||
local sample="$1"
|
||||
local iters="$2"
|
||||
local warm="$3"
|
||||
local fixture="$RPKI_DIR/tests/benchmark/selected_der/${sample}.mft"
|
||||
if [[ ! -f "$fixture" ]]; then
|
||||
echo "ERROR: ours fixture not found: $fixture" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local perfdata="$OUT_PERF/ours_${sample}_${RUN_TAG}.perf.data"
|
||||
local svg="$OUT_FLAME/ours_${sample}_${RUN_TAG}.svg"
|
||||
local tsv="$OUT_HOT/ours_${sample}_${RUN_TAG}.tsv"
|
||||
|
||||
echo "== ours $sample (iters=$iters warmup=$warm hz=$PROFILE_HZ)"
|
||||
$taskset_prefix perf record -o "$perfdata" -F "$PROFILE_HZ" -g -- \
|
||||
"$OURS_BIN" --manifest "$fixture" --iterations "$iters" --warmup-iterations "$warm" --repeats 1 >/dev/null
|
||||
|
||||
flamegraph --perfdata "$perfdata" --output "$svg" --title "ours ${sample} ManifestObject::decode_der" --deterministic >/dev/null
|
||||
|
||||
perf report -i "$perfdata" --stdio --no-children --sort symbol --percent-limit 0.5 \
|
||||
| awk '/^[[:space:]]*[0-9.]+%/ {pct=$1; sub(/%/,"",pct); $1=""; sub(/^[[:space:]]+/,""); print pct "\t" $0}' \
|
||||
> "$tsv"
|
||||
}
|
||||
|
||||
profile_routinator() {
|
||||
local sample="$1"
|
||||
local iters="$2"
|
||||
local warm="$3"
|
||||
local fixture="$ROUT_BENCH_DIR/fixtures/selected_der/${sample}.mft"
|
||||
if [[ ! -f "$fixture" ]]; then
|
||||
echo "ERROR: routinator fixture not found: $fixture" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local svg="$OUT_FLAME/routinator_${sample}_${RUN_TAG}.svg"
|
||||
local tsv="$OUT_HOT/routinator_${sample}_${RUN_TAG}.tsv"
|
||||
|
||||
echo "== routinator $sample (iters=$iters warmup=$warm hz=$PROFILE_HZ)"
|
||||
$taskset_prefix "$ROUT_BIN" \
|
||||
--target decode_only \
|
||||
--manifest "$fixture" \
|
||||
--issuer "$ROUT_ISSUER" \
|
||||
--iterations "$iters" \
|
||||
--repeats 1 \
|
||||
--warmup-iterations "$warm" \
|
||||
--strict false \
|
||||
--profile-hz "$PROFILE_HZ" \
|
||||
--flamegraph "$svg" \
|
||||
--hotspots "$tsv" \
|
||||
>/dev/null
|
||||
}
|
||||
|
||||
echo "[3/3] Profile samples..."
|
||||
|
||||
# Choose iterations so each capture runs ~10-20s serially.
|
||||
profile_ours small-01 200000 0
|
||||
profile_routinator small-01 200000 0
|
||||
|
||||
profile_ours large-02 50000 0
|
||||
profile_routinator large-02 50000 0
|
||||
|
||||
profile_ours xlarge-02 5000 0
|
||||
profile_routinator xlarge-02 5000 0
|
||||
|
||||
echo "Done."
|
||||
echo "- Flamegraphs: $OUT_FLAME/"
|
||||
echo "- Hotspots: $OUT_HOT/"
|
||||
echo "- Perf data: $OUT_PERF/"
|
||||
94
scripts/manual_sync/README.md
Normal file
94
scripts/manual_sync/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Manual RRDP sync (APNIC-focused)
|
||||
|
||||
This directory contains **manual, command-line** scripts to reproduce the workflow described in:
|
||||
|
||||
- `specs/develop/20260226/apnic_rrdp_delta_analysis_after_manifest_revalidation_fix_20260227T022606Z.md`
|
||||
|
||||
They are meant for **hands-on validation / acceptance runs**, not for CI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust toolchain (`cargo`)
|
||||
- `rsync` available on PATH (for rsync fallback/objects)
|
||||
- Network access (RRDP over HTTPS)
|
||||
|
||||
## What the scripts do
|
||||
|
||||
### `full_sync.sh`
|
||||
|
||||
- Creates a fresh RocksDB directory
|
||||
- Runs a **full serial** validation from a TAL URL (default: APNIC RFC7730 TAL)
|
||||
- Writes:
|
||||
- run log
|
||||
- audit report JSON
|
||||
- run meta JSON (includes durations + download_stats)
|
||||
- short summary Markdown (includes durations + download_stats)
|
||||
- RocksDB key statistics (`db_stats --exact`)
|
||||
- RRDP repo state dump (`rrdp_state_dump`)
|
||||
|
||||
### `delta_sync.sh`
|
||||
|
||||
- Copies an existing “baseline snapshot DB” to a new DB directory (so the baseline is not modified)
|
||||
- Runs another validation against the copied DB (RRDP will prefer **delta** when available)
|
||||
- Produces the same artifacts as `full_sync.sh`
|
||||
- Additionally generates a Markdown **delta analysis** report by comparing:
|
||||
- base vs delta report JSON
|
||||
- base vs delta `rrdp_state_dump` TSV
|
||||
- and includes a **duration comparison** (base vs delta) if the base meta JSON is available
|
||||
- delta meta JSON includes download_stats copied from delta report JSON
|
||||
|
||||
## Audit report fields (report.json)
|
||||
|
||||
The `rpki` binary writes an audit report JSON with:
|
||||
|
||||
- `format_version: 2`
|
||||
- `downloads`: per-download RRDP/rsync events (URI, timestamps, duration, ok/fail, error, bytes, objects stats)
|
||||
- `download_stats`: aggregate counters (by kind)
|
||||
|
||||
These are useful for diagnosing why a run is slow (e.g. RRDP snapshot vs delta vs rsync fallback).
|
||||
|
||||
## Meta fields (meta.json)
|
||||
|
||||
The scripts generate `*_meta.json` next to `*_report.json` and include:
|
||||
|
||||
- `durations_secs`: wall-clock duration breakdown for the script steps
|
||||
- `download_stats`: copied from `report_json.download_stats`
|
||||
|
||||
## Usage
|
||||
|
||||
Run from `rpki/`:
|
||||
|
||||
```bash
|
||||
./scripts/manual_sync/full_sync.sh
|
||||
```
|
||||
|
||||
After you have a baseline run, run delta against it:
|
||||
|
||||
```bash
|
||||
./scripts/manual_sync/delta_sync.sh target/live/manual_sync/apnic_full_db_YYYYMMDDTHHMMSSZ \
|
||||
target/live/manual_sync/apnic_full_report_YYYYMMDDTHHMMSSZ.json
|
||||
```
|
||||
|
||||
If the baseline was produced by `full_sync.sh`, the delta script will auto-discover the base meta JSON
|
||||
next to the base report (by replacing `_report.json` with `_meta.json`) and include base durations in
|
||||
the delta analysis report.
|
||||
|
||||
## Configuration (env vars)
|
||||
|
||||
Both scripts accept overrides via env vars:
|
||||
|
||||
- `TAL_URL` (default: APNIC TAL URL)
|
||||
- `HTTP_TIMEOUT_SECS` (default: 1800)
|
||||
- `RSYNC_TIMEOUT_SECS` (default: 1800)
|
||||
- `RSYNC_MIRROR_ROOT` (default: disabled; when set, passes `--rsync-mirror-root` to `rpki`)
|
||||
- `VALIDATION_TIME` (RFC3339; default: now UTC)
|
||||
- `OUT_DIR` (default: `rpki/target/live/manual_sync`)
|
||||
- `RUN_NAME` (default: auto timestamped)
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
TAL_URL="https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal" \
|
||||
HTTP_TIMEOUT_SECS=1800 RSYNC_TIMEOUT_SECS=1800 \
|
||||
./scripts/manual_sync/full_sync.sh
|
||||
```
|
||||
515
scripts/manual_sync/delta_sync.sh
Executable file
515
scripts/manual_sync/delta_sync.sh
Executable file
@@ -0,0 +1,515 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Delta sync + validation starting from a baseline snapshot DB.
|
||||
#
|
||||
# This script:
|
||||
# 1) Copies BASE_DB_DIR -> DELTA_DB_DIR (so baseline is not modified)
|
||||
# 2) Runs rpki validation again (RRDP will prefer delta if available)
|
||||
# 3) Writes artifacts + a markdown delta analysis report
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/manual_sync/delta_sync.sh <base_db_dir> <base_report_json>
|
||||
#
|
||||
# Outputs under OUT_DIR (default: target/live/manual_sync):
|
||||
# - *_delta_db_* copied RocksDB directory
|
||||
# - *_delta_report_*.json audit report
|
||||
# - *_delta_run_*.log stdout/stderr log (includes summary)
|
||||
# - *_delta_db_stats_*.txt db_stats --exact output
|
||||
# - *_delta_rrdp_state_*.tsv rrdp_state_dump output
|
||||
# - *_delta_analysis_*.md base vs delta comparison report
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
BASE_DB_DIR="${1:-}"
|
||||
BASE_REPORT_JSON="${2:-}"
|
||||
if [[ -z "${BASE_DB_DIR}" || -z "${BASE_REPORT_JSON}" ]]; then
|
||||
echo "Usage: $0 <base_db_dir> <base_report_json>" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -d "${BASE_DB_DIR}" ]]; then
|
||||
echo "ERROR: base_db_dir is not a directory: ${BASE_DB_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "${BASE_REPORT_JSON}" ]]; then
|
||||
echo "ERROR: base_report_json does not exist: ${BASE_REPORT_JSON}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TAL_URL="${TAL_URL:-https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal}"
|
||||
HTTP_TIMEOUT_SECS="${HTTP_TIMEOUT_SECS:-1800}"
|
||||
RSYNC_TIMEOUT_SECS="${RSYNC_TIMEOUT_SECS:-1800}"
|
||||
RSYNC_MIRROR_ROOT="${RSYNC_MIRROR_ROOT:-}"
|
||||
VALIDATION_TIME="${VALIDATION_TIME:-}"
|
||||
|
||||
OUT_DIR="${OUT_DIR:-$ROOT_DIR/target/live/manual_sync}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
RUN_NAME="${RUN_NAME:-apnic_delta_${TS}}"
|
||||
|
||||
DELTA_DB_DIR="${DELTA_DB_DIR:-$OUT_DIR/${RUN_NAME}_db}"
|
||||
DELTA_REPORT_JSON="${DELTA_REPORT_JSON:-$OUT_DIR/${RUN_NAME}_report.json}"
|
||||
DELTA_RUN_LOG="${DELTA_RUN_LOG:-$OUT_DIR/${RUN_NAME}_run.log}"
|
||||
|
||||
BASE_DB_STATS_TXT="${BASE_DB_STATS_TXT:-$OUT_DIR/${RUN_NAME}_base_db_stats.txt}"
|
||||
DELTA_DB_STATS_TXT="${DELTA_DB_STATS_TXT:-$OUT_DIR/${RUN_NAME}_delta_db_stats.txt}"
|
||||
|
||||
BASE_RRDP_STATE_TSV="${BASE_RRDP_STATE_TSV:-$OUT_DIR/${RUN_NAME}_base_rrdp_state.tsv}"
|
||||
DELTA_RRDP_STATE_TSV="${DELTA_RRDP_STATE_TSV:-$OUT_DIR/${RUN_NAME}_delta_rrdp_state.tsv}"
|
||||
|
||||
DELTA_ANALYSIS_MD="${DELTA_ANALYSIS_MD:-$OUT_DIR/${RUN_NAME}_delta_analysis.md}"
|
||||
DELTA_META_JSON="${DELTA_META_JSON:-$OUT_DIR/${RUN_NAME}_meta.json}"
|
||||
|
||||
# Best-effort base meta discovery (produced by `full_sync.sh`).
|
||||
BASE_META_JSON="${BASE_META_JSON:-}"
|
||||
if [[ -z "${BASE_META_JSON}" ]]; then
|
||||
guess="${BASE_REPORT_JSON%_report.json}_meta.json"
|
||||
if [[ -f "${guess}" ]]; then
|
||||
BASE_META_JSON="${guess}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "== rpki manual delta sync ==" >&2
|
||||
echo "tal_url=$TAL_URL" >&2
|
||||
echo "base_db=$BASE_DB_DIR" >&2
|
||||
echo "base_report=$BASE_REPORT_JSON" >&2
|
||||
echo "delta_db=$DELTA_DB_DIR" >&2
|
||||
echo "delta_report=$DELTA_REPORT_JSON" >&2
|
||||
|
||||
echo "== copying base DB (baseline is not modified) ==" >&2
|
||||
cp -a "$BASE_DB_DIR" "$DELTA_DB_DIR"
|
||||
|
||||
script_start_s="$(date +%s)"
|
||||
run_start_s="$(date +%s)"
|
||||
cmd=(cargo run --release --bin rpki -- \
|
||||
--db "$DELTA_DB_DIR" \
|
||||
--tal-url "$TAL_URL" \
|
||||
--http-timeout-secs "$HTTP_TIMEOUT_SECS" \
|
||||
--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS" \
|
||||
--report-json "$DELTA_REPORT_JSON")
|
||||
if [[ -n "${RSYNC_MIRROR_ROOT}" ]]; then
|
||||
cmd+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||
fi
|
||||
if [[ -n "${VALIDATION_TIME}" ]]; then
|
||||
cmd+=(--validation-time "$VALIDATION_TIME")
|
||||
fi
|
||||
|
||||
(
|
||||
echo "# command:"
|
||||
printf '%q ' "${cmd[@]}"
|
||||
echo
|
||||
echo
|
||||
"${cmd[@]}"
|
||||
) 2>&1 | tee "$DELTA_RUN_LOG" >/dev/null
|
||||
run_end_s="$(date +%s)"
|
||||
run_duration_s="$((run_end_s - run_start_s))"
|
||||
|
||||
echo "== db_stats (exact) ==" >&2
|
||||
db_stats_start_s="$(date +%s)"
|
||||
cargo run --release --bin db_stats -- --db "$BASE_DB_DIR" --exact 2>&1 | tee "$BASE_DB_STATS_TXT" >/dev/null
|
||||
cargo run --release --bin db_stats -- --db "$DELTA_DB_DIR" --exact 2>&1 | tee "$DELTA_DB_STATS_TXT" >/dev/null
|
||||
db_stats_end_s="$(date +%s)"
|
||||
db_stats_duration_s="$((db_stats_end_s - db_stats_start_s))"
|
||||
|
||||
echo "== rrdp_state_dump ==" >&2
|
||||
state_start_s="$(date +%s)"
|
||||
cargo run --release --bin rrdp_state_dump -- --db "$BASE_DB_DIR" >"$BASE_RRDP_STATE_TSV"
|
||||
cargo run --release --bin rrdp_state_dump -- --db "$DELTA_DB_DIR" >"$DELTA_RRDP_STATE_TSV"
|
||||
state_end_s="$(date +%s)"
|
||||
state_duration_s="$((state_end_s - state_start_s))"
|
||||
|
||||
script_end_s="$(date +%s)"
|
||||
total_duration_s="$((script_end_s - script_start_s))"
|
||||
|
||||
echo "== delta analysis report ==" >&2
|
||||
TAL_URL="$TAL_URL" \
|
||||
BASE_DB_DIR="$BASE_DB_DIR" \
|
||||
DELTA_DB_DIR="$DELTA_DB_DIR" \
|
||||
DELTA_RUN_LOG="$DELTA_RUN_LOG" \
|
||||
VALIDATION_TIME_ARG="$VALIDATION_TIME" \
|
||||
HTTP_TIMEOUT_SECS="$HTTP_TIMEOUT_SECS" \
|
||||
RSYNC_TIMEOUT_SECS="$RSYNC_TIMEOUT_SECS" \
|
||||
RUN_DURATION_S="$run_duration_s" \
|
||||
DB_STATS_DURATION_S="$db_stats_duration_s" \
|
||||
STATE_DURATION_S="$state_duration_s" \
|
||||
TOTAL_DURATION_S="$total_duration_s" \
|
||||
python3 - "$BASE_REPORT_JSON" "$DELTA_REPORT_JSON" "$BASE_RRDP_STATE_TSV" "$DELTA_RRDP_STATE_TSV" \
|
||||
"$BASE_DB_STATS_TXT" "$DELTA_DB_STATS_TXT" "$BASE_META_JSON" "$DELTA_META_JSON" "$DELTA_ANALYSIS_MD" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
base_report_path = Path(sys.argv[1])
|
||||
delta_report_path = Path(sys.argv[2])
|
||||
base_state_path = Path(sys.argv[3])
|
||||
delta_state_path = Path(sys.argv[4])
|
||||
base_db_stats_path = Path(sys.argv[5])
|
||||
delta_db_stats_path = Path(sys.argv[6])
|
||||
base_meta_path_s = sys.argv[7]
|
||||
delta_meta_path = Path(sys.argv[8])
|
||||
out_md_path = Path(sys.argv[9])
|
||||
|
||||
def load_json(p: Path):
|
||||
s = p.read_text(encoding="utf-8")
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
# Backwards-compat / robustness: tolerate accidental literal trailing "\\n".
|
||||
s2 = s.strip()
|
||||
if s2.endswith("\\n"):
|
||||
s2 = s2[:-2].rstrip()
|
||||
return json.loads(s2)
|
||||
|
||||
def load_optional_json(path_s: str):
|
||||
if not path_s:
|
||||
return None
|
||||
p = Path(path_s)
|
||||
if not p.exists():
|
||||
return None
|
||||
return load_json(p)
|
||||
|
||||
def parse_rrdp_state_tsv(p: Path):
|
||||
# format: "<notify_uri>\t<serial>\t<session_id>"
|
||||
out = {}
|
||||
for line in p.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) != 3:
|
||||
raise SystemExit(f"invalid rrdp_state_dump line in {p}: {line!r}")
|
||||
uri, serial, session = parts
|
||||
out[uri] = (int(serial), session)
|
||||
return out
|
||||
|
||||
def parse_db_stats(p: Path):
|
||||
# lines: key=value
|
||||
out = {}
|
||||
for line in p.read_text(encoding="utf-8").splitlines():
|
||||
if "=" not in line:
|
||||
continue
|
||||
k, v = line.split("=", 1)
|
||||
k = k.strip()
|
||||
v = v.strip()
|
||||
if v.isdigit():
|
||||
out[k] = int(v)
|
||||
else:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
def warnings_total(rep: dict) -> int:
|
||||
return len(rep["tree"]["warnings"]) + sum(len(pp["warnings"]) for pp in rep["publication_points"])
|
||||
|
||||
def report_summary(rep: dict) -> dict:
|
||||
return {
|
||||
"validation_time": rep["meta"]["validation_time_rfc3339_utc"],
|
||||
"publication_points_processed": rep["tree"]["instances_processed"],
|
||||
"publication_points_failed": rep["tree"]["instances_failed"],
|
||||
"rrdp_repos_unique": len({pp.get("rrdp_notification_uri") for pp in rep["publication_points"] if pp.get("rrdp_notification_uri")}),
|
||||
"vrps": len(rep["vrps"]),
|
||||
"aspas": len(rep["aspas"]),
|
||||
"audit_publication_points": len(rep["publication_points"]),
|
||||
"warnings_total": warnings_total(rep),
|
||||
}
|
||||
|
||||
def count_repo_sync_failed(rep: dict) -> int:
|
||||
# Best-effort heuristic (we don't currently expose a structured counter in the audit report).
|
||||
# Keep the match conservative to avoid false positives.
|
||||
def is_repo_sync_failed(msg: str) -> bool:
|
||||
m = msg.lower()
|
||||
return "repo sync failed" in m or "rrdp fetch failed" in m or "rsync fetch failed" in m
|
||||
|
||||
n = 0
|
||||
for w in rep["tree"]["warnings"]:
|
||||
if is_repo_sync_failed(w.get("message", "")):
|
||||
n += 1
|
||||
for pp in rep["publication_points"]:
|
||||
for w in pp.get("warnings", []):
|
||||
if is_repo_sync_failed(w.get("message", "")):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
def pp_manifest_sha(pp: dict) -> str:
|
||||
# In our audit format, the first object is the manifest (synthetic entry) with sha256 of manifest_bytes.
|
||||
for o in pp["objects"]:
|
||||
if o["kind"] == "manifest":
|
||||
return o["sha256_hex"]
|
||||
return ""
|
||||
|
||||
def pp_objects_by_uri(rep: dict):
|
||||
m = {}
|
||||
for pp in rep["publication_points"]:
|
||||
for o in pp["objects"]:
|
||||
m[o["rsync_uri"]] = (o["sha256_hex"], o["kind"])
|
||||
return m
|
||||
|
||||
def vrp_set(rep: dict):
|
||||
return {(v["asn"], v["prefix"], v["max_length"]) for v in rep["vrps"]}
|
||||
|
||||
def rfc_refs_str(w: dict) -> str:
|
||||
refs = w.get("rfc_refs") or []
|
||||
return ", ".join(refs) if refs else ""
|
||||
|
||||
base = load_json(base_report_path)
|
||||
delta = load_json(delta_report_path)
|
||||
base_sum = report_summary(base)
|
||||
delta_sum = report_summary(delta)
|
||||
|
||||
base_db = parse_db_stats(base_db_stats_path)
|
||||
delta_db = parse_db_stats(delta_db_stats_path)
|
||||
|
||||
base_state = parse_rrdp_state_tsv(base_state_path)
|
||||
delta_state = parse_rrdp_state_tsv(delta_state_path)
|
||||
|
||||
base_meta = load_optional_json(base_meta_path_s)
|
||||
download_stats = delta.get("download_stats") or {}
|
||||
delta_meta = {
|
||||
"recorded_at_utc": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"tal_url": os.environ["TAL_URL"],
|
||||
"base_db_dir": os.environ["BASE_DB_DIR"],
|
||||
"delta_db_dir": os.environ["DELTA_DB_DIR"],
|
||||
"base_report_json": str(base_report_path),
|
||||
"delta_report_json": str(delta_report_path),
|
||||
"delta_run_log": os.environ["DELTA_RUN_LOG"],
|
||||
"validation_time_arg": os.environ.get("VALIDATION_TIME_ARG",""),
|
||||
"http_timeout_secs": int(os.environ["HTTP_TIMEOUT_SECS"]),
|
||||
"rsync_timeout_secs": int(os.environ["RSYNC_TIMEOUT_SECS"]),
|
||||
"durations_secs": {
|
||||
"rpki_run": int(os.environ["RUN_DURATION_S"]),
|
||||
"db_stats_exact": int(os.environ["DB_STATS_DURATION_S"]),
|
||||
"rrdp_state_dump": int(os.environ["STATE_DURATION_S"]),
|
||||
"total_script": int(os.environ["TOTAL_DURATION_S"]),
|
||||
},
|
||||
"download_stats": download_stats,
|
||||
}
|
||||
delta_meta_path.write_text(json.dumps(delta_meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
# RRDP state changes
|
||||
serial_changed = 0
|
||||
session_changed = 0
|
||||
serial_deltas = []
|
||||
for uri, (old_serial, old_sess) in base_state.items():
|
||||
if uri not in delta_state:
|
||||
continue
|
||||
new_serial, new_sess = delta_state[uri]
|
||||
if new_serial != old_serial:
|
||||
serial_changed += 1
|
||||
serial_deltas.append((uri, old_serial, new_serial, new_serial - old_serial))
|
||||
if new_sess != old_sess:
|
||||
session_changed += 1
|
||||
|
||||
serial_deltas.sort(key=lambda x: x[3], reverse=True)
|
||||
|
||||
# Publication point diffs
|
||||
base_pp = {pp["manifest_rsync_uri"]: pp for pp in base["publication_points"]}
|
||||
delta_pp = {pp["manifest_rsync_uri"]: pp for pp in delta["publication_points"]}
|
||||
|
||||
base_keys = set(base_pp.keys())
|
||||
delta_keys = set(delta_pp.keys())
|
||||
|
||||
new_pp = sorted(delta_keys - base_keys)
|
||||
missing_pp = sorted(base_keys - delta_keys)
|
||||
|
||||
updated_pp = 0
|
||||
unchanged_pp = 0
|
||||
for k in sorted(base_keys & delta_keys):
|
||||
if pp_manifest_sha(base_pp[k]) != pp_manifest_sha(delta_pp[k]):
|
||||
updated_pp += 1
|
||||
else:
|
||||
unchanged_pp += 1
|
||||
|
||||
# Cache usage + repo sync failure hints
|
||||
def source_counts(rep: dict) -> Counter:
|
||||
c = Counter()
|
||||
for pp in rep["publication_points"]:
|
||||
c[pp.get("source","")] += 1
|
||||
return c
|
||||
|
||||
base_sources = source_counts(base)
|
||||
delta_sources = source_counts(delta)
|
||||
base_repo_sync_failed = count_repo_sync_failed(base)
|
||||
delta_repo_sync_failed = count_repo_sync_failed(delta)
|
||||
|
||||
def cache_reason_counts(rep: dict) -> Counter:
|
||||
c = Counter()
|
||||
for pp in rep.get("publication_points", []):
|
||||
if pp.get("source") != "fetch_cache_pp":
|
||||
continue
|
||||
# Use warning messages as "reason". If missing, emit a fallback bucket.
|
||||
ws = pp.get("warnings", [])
|
||||
if not ws:
|
||||
c["(no warnings recorded)"] += 1
|
||||
continue
|
||||
for w in ws:
|
||||
msg = w.get("message", "").strip() or "(empty warning message)"
|
||||
c[msg] += 1
|
||||
return c
|
||||
|
||||
base_cache_reasons = cache_reason_counts(base)
|
||||
delta_cache_reasons = cache_reason_counts(delta)
|
||||
|
||||
# Object change stats (by rsync URI, sha256)
|
||||
base_obj = pp_objects_by_uri(base)
|
||||
delta_obj = pp_objects_by_uri(delta)
|
||||
|
||||
kind_stats = {k: {"added": 0, "changed": 0, "removed": 0} for k in ["manifest","crl","certificate","roa","aspa","other"]}
|
||||
all_uris = set(base_obj.keys()) | set(delta_obj.keys())
|
||||
for uri in all_uris:
|
||||
b = base_obj.get(uri)
|
||||
d = delta_obj.get(uri)
|
||||
if b is None and d is not None:
|
||||
kind_stats[d[1]]["added"] += 1
|
||||
elif b is not None and d is None:
|
||||
kind_stats[b[1]]["removed"] += 1
|
||||
else:
|
||||
if b[0] != d[0]:
|
||||
kind_stats[d[1]]["changed"] += 1
|
||||
|
||||
# VRP diff
|
||||
base_v = vrp_set(base)
|
||||
delta_v = vrp_set(delta)
|
||||
added_v = delta_v - base_v
|
||||
removed_v = base_v - delta_v
|
||||
|
||||
def fmt_db_stats(db: dict) -> str:
|
||||
keys = ["raw_objects","rrdp_object_index","fetch_cache_pp","rrdp_state","total"]
|
||||
out = []
|
||||
for k in keys:
|
||||
if k in db:
|
||||
out.append(f"- `{k}={db[k]}`")
|
||||
return "\n".join(out) if out else "_(missing db_stats keys)_"
|
||||
|
||||
lines = []
|
||||
lines.append("# APNIC RRDP 增量同步验收(manual_sync)\n\n")
|
||||
lines.append(f"时间戳:`{now}`(UTC)\n\n")
|
||||
|
||||
lines.append("## 复现信息\n\n")
|
||||
lines.append(f"- base_report:`{base_report_path}`\n")
|
||||
lines.append(f"- delta_report:`{delta_report_path}`\n")
|
||||
lines.append(f"- base_db_stats:`{base_db_stats_path}`\n")
|
||||
lines.append(f"- delta_db_stats:`{delta_db_stats_path}`\n")
|
||||
lines.append(f"- base_rrdp_state:`{base_state_path}`\n")
|
||||
lines.append(f"- delta_rrdp_state:`{delta_state_path}`\n\n")
|
||||
|
||||
lines.append("## 运行结果概览\n\n")
|
||||
lines.append("| metric | base | delta |\n")
|
||||
lines.append("|---|---:|---:|\n")
|
||||
for k in [
|
||||
"validation_time",
|
||||
"publication_points_processed",
|
||||
"publication_points_failed",
|
||||
"rrdp_repos_unique",
|
||||
"vrps",
|
||||
"aspas",
|
||||
"audit_publication_points",
|
||||
"warnings_total",
|
||||
]:
|
||||
lines.append(f"| {k} | {base_sum[k]} | {delta_sum[k]} |\n")
|
||||
lines.append("\n")
|
||||
|
||||
def dur(meta: dict | None, key: str):
|
||||
if not meta:
|
||||
return None
|
||||
return (meta.get("durations_secs") or {}).get(key)
|
||||
|
||||
base_rpki_run = dur(base_meta, "rpki_run")
|
||||
delta_rpki_run = delta_meta["durations_secs"]["rpki_run"]
|
||||
base_total = dur(base_meta, "total_script")
|
||||
delta_total = delta_meta["durations_secs"]["total_script"]
|
||||
|
||||
lines.append("## 持续时间(seconds)\n\n")
|
||||
lines.append("| step | base | delta |\n")
|
||||
lines.append("|---|---:|---:|\n")
|
||||
lines.append(f"| rpki_run | {base_rpki_run if base_rpki_run is not None else 'unknown'} | {delta_rpki_run} |\n")
|
||||
lines.append(f"| total_script | {base_total if base_total is not None else 'unknown'} | {delta_total} |\n")
|
||||
lines.append("\n")
|
||||
if base_meta is None and base_meta_path_s:
|
||||
lines.append(f"> 注:未能读取 base meta:`{base_meta_path_s}`(文件不存在或不可读)。建议用 `full_sync.sh` 生成 baseline 以获得 base 时长对比。\n\n")
|
||||
|
||||
lines.append("RocksDB KV(`db_stats --exact`):\n\n")
|
||||
lines.append("### 基线(base)\n\n")
|
||||
lines.append(fmt_db_stats(base_db) + "\n\n")
|
||||
lines.append("### 增量(delta)\n\n")
|
||||
lines.append(fmt_db_stats(delta_db) + "\n\n")
|
||||
|
||||
lines.append("## RRDP 增量是否发生(基于 `rrdp_state` 变化)\n\n")
|
||||
lines.append(f"- repo_total(base)={len(base_state)}\n")
|
||||
lines.append(f"- repo_total(delta)={len(delta_state)}\n")
|
||||
lines.append(f"- serial_changed={serial_changed}\n")
|
||||
lines.append(f"- session_changed={session_changed}\n\n")
|
||||
if serial_deltas:
|
||||
lines.append("serial 增长最大的 10 个 RRDP repo(old -> new):\n\n")
|
||||
for uri, old, new, diff in serial_deltas[:10]:
|
||||
lines.append(f"- `{uri}`:`{old} -> {new}`(+{diff})\n")
|
||||
lines.append("\n")
|
||||
|
||||
lines.append("## 发布点(Publication Point)变化统计\n\n")
|
||||
lines.append("以 `manifest_rsync_uri` 作为发布点 key,对比 base vs delta:\n\n")
|
||||
lines.append(f"- base PP:`{len(base_keys)}`\n")
|
||||
lines.append(f"- delta PP:`{len(delta_keys)}`\n")
|
||||
lines.append(f"- `new_pp={len(new_pp)}`\n")
|
||||
lines.append(f"- `missing_pp={len(missing_pp)}`\n")
|
||||
lines.append(f"- `updated_pp={updated_pp}`\n")
|
||||
lines.append(f"- `unchanged_pp={unchanged_pp}`\n\n")
|
||||
lines.append("> 注:`new_pp/missing_pp/updated_pp` 会混入“遍历范围变化”的影响(例如 validation_time 不同、或 base 中存在更多失败 PP)。\n\n")
|
||||
|
||||
lines.append("## fail fetch / cache 使用情况\n\n")
|
||||
lines.append(f"- repo sync failed(启发式:warning contains 'repo sync failed'/'rrdp fetch failed'/'rsync fetch failed')\n")
|
||||
lines.append(f" - base:`{base_repo_sync_failed}`\n")
|
||||
lines.append(f" - delta:`{delta_repo_sync_failed}`\n\n")
|
||||
|
||||
lines.append("- source 计数(按 `PublicationPointAudit.source`):\n\n")
|
||||
lines.append(f" - base:`{dict(base_sources)}`\n")
|
||||
lines.append(f" - delta:`{dict(delta_sources)}`\n\n")
|
||||
|
||||
def render_cache_reasons(title: str, c: Counter) -> str:
|
||||
if not c:
|
||||
return f"{title}:`0`(未使用 fetch_cache_pp)\n\n"
|
||||
lines = []
|
||||
total = sum(c.values())
|
||||
lines.append(f"{title}:`{total}`\n\n")
|
||||
lines.append("Top reasons(按 warning message 聚合,可能一条 PP 有多条 warning):\n\n")
|
||||
for msg, n in c.most_common(10):
|
||||
lines.append(f"- `{n}` × {msg}\n")
|
||||
lines.append("\n")
|
||||
return "".join(lines)
|
||||
|
||||
lines.append(render_cache_reasons("- base `source=fetch_cache_pp`", base_cache_reasons))
|
||||
lines.append(render_cache_reasons("- delta `source=fetch_cache_pp`", delta_cache_reasons))
|
||||
|
||||
lines.append("## 文件变更统计(按对象类型)\n\n")
|
||||
lines.append("按 `ObjectAuditEntry.sha256_hex` 对比(同一 rsync URI 前后 hash 变化记为 `~changed`):\n\n")
|
||||
lines.append("| kind | added | changed | removed |\n")
|
||||
lines.append("|---|---:|---:|---:|\n")
|
||||
for kind in ["manifest","crl","certificate","roa","aspa","other"]:
|
||||
st = kind_stats[kind]
|
||||
lines.append(f"| {kind} | {st['added']} | {st['changed']} | {st['removed']} |\n")
|
||||
lines.append("\n")
|
||||
|
||||
lines.append("## VRP 影响(去重后集合 diff)\n\n")
|
||||
lines.append("以 `(asn, prefix, max_length)` 为 key:\n\n")
|
||||
lines.append(f"- base unique VRP:`{len(base_v)}`\n")
|
||||
lines.append(f"- delta unique VRP:`{len(delta_v)}`\n")
|
||||
lines.append(f"- `added={len(added_v)}`\n")
|
||||
lines.append(f"- `removed={len(removed_v)}`\n")
|
||||
lines.append(f"- `net={len(added_v) - len(removed_v)}`\n\n")
|
||||
|
||||
out_md_path.write_text("".join(lines), encoding="utf-8")
|
||||
print(out_md_path)
|
||||
PY
|
||||
|
||||
echo "== done ==" >&2
|
||||
echo "artifacts:" >&2
|
||||
echo "- delta db: $DELTA_DB_DIR" >&2
|
||||
echo "- delta report: $DELTA_REPORT_JSON" >&2
|
||||
echo "- delta run log: $DELTA_RUN_LOG" >&2
|
||||
echo "- delta meta json: $DELTA_META_JSON" >&2
|
||||
echo "- analysis md: $DELTA_ANALYSIS_MD" >&2
|
||||
echo "- base state tsv: $BASE_RRDP_STATE_TSV" >&2
|
||||
echo "- delta state tsv: $DELTA_RRDP_STATE_TSV" >&2
|
||||
189
scripts/manual_sync/full_sync.sh
Executable file
189
scripts/manual_sync/full_sync.sh
Executable file
@@ -0,0 +1,189 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Full sync + validation from a TAL URL (default: APNIC).
|
||||
#
|
||||
# Produces artifacts under OUT_DIR (default: target/live/manual_sync):
|
||||
# - *_db_* RocksDB directory
|
||||
# - *_report_*.json audit report
|
||||
# - *_run_*.log stdout/stderr log (includes summary)
|
||||
# - *_db_stats_*.txt db_stats --exact output
|
||||
# - *_rrdp_state_*.tsv rrdp_state_dump output
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
TAL_URL="${TAL_URL:-https://tal.apnic.net/tal-archive/apnic-rfc7730-https.tal}"
|
||||
HTTP_TIMEOUT_SECS="${HTTP_TIMEOUT_SECS:-1800}"
|
||||
RSYNC_TIMEOUT_SECS="${RSYNC_TIMEOUT_SECS:-1800}"
|
||||
RSYNC_MIRROR_ROOT="${RSYNC_MIRROR_ROOT:-}"
|
||||
VALIDATION_TIME="${VALIDATION_TIME:-}"
|
||||
|
||||
OUT_DIR="${OUT_DIR:-$ROOT_DIR/target/live/manual_sync}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
TS="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||
RUN_NAME="${RUN_NAME:-apnic_full_${TS}}"
|
||||
|
||||
DB_DIR="${DB_DIR:-$OUT_DIR/${RUN_NAME}_db}"
|
||||
REPORT_JSON="${REPORT_JSON:-$OUT_DIR/${RUN_NAME}_report.json}"
|
||||
RUN_LOG="${RUN_LOG:-$OUT_DIR/${RUN_NAME}_run.log}"
|
||||
DB_STATS_TXT="${DB_STATS_TXT:-$OUT_DIR/${RUN_NAME}_db_stats.txt}"
|
||||
RRDP_STATE_TSV="${RRDP_STATE_TSV:-$OUT_DIR/${RUN_NAME}_rrdp_state.tsv}"
|
||||
RUN_META_JSON="${RUN_META_JSON:-$OUT_DIR/${RUN_NAME}_meta.json}"
|
||||
SUMMARY_MD="${SUMMARY_MD:-$OUT_DIR/${RUN_NAME}_summary.md}"
|
||||
|
||||
echo "== rpki manual full sync ==" >&2
|
||||
echo "tal_url=$TAL_URL" >&2
|
||||
echo "db=$DB_DIR" >&2
|
||||
echo "report_json=$REPORT_JSON" >&2
|
||||
echo "out_dir=$OUT_DIR" >&2
|
||||
|
||||
cmd=(cargo run --release --bin rpki -- \
|
||||
--db "$DB_DIR" \
|
||||
--tal-url "$TAL_URL" \
|
||||
--http-timeout-secs "$HTTP_TIMEOUT_SECS" \
|
||||
--rsync-timeout-secs "$RSYNC_TIMEOUT_SECS" \
|
||||
--report-json "$REPORT_JSON")
|
||||
|
||||
if [[ -n "${RSYNC_MIRROR_ROOT}" ]]; then
|
||||
cmd+=(--rsync-mirror-root "$RSYNC_MIRROR_ROOT")
|
||||
fi
|
||||
if [[ -n "${VALIDATION_TIME}" ]]; then
|
||||
cmd+=(--validation-time "$VALIDATION_TIME")
|
||||
fi
|
||||
|
||||
script_start_s="$(date +%s)"
|
||||
run_start_s="$(date +%s)"
|
||||
(
|
||||
echo "# command:"
|
||||
printf '%q ' "${cmd[@]}"
|
||||
echo
|
||||
echo
|
||||
"${cmd[@]}"
|
||||
) 2>&1 | tee "$RUN_LOG" >/dev/null
|
||||
run_end_s="$(date +%s)"
|
||||
run_duration_s="$((run_end_s - run_start_s))"
|
||||
|
||||
echo "== db_stats (exact) ==" >&2
|
||||
db_stats_start_s="$(date +%s)"
|
||||
cargo run --release --bin db_stats -- --db "$DB_DIR" --exact 2>&1 | tee "$DB_STATS_TXT" >/dev/null
|
||||
db_stats_end_s="$(date +%s)"
|
||||
db_stats_duration_s="$((db_stats_end_s - db_stats_start_s))"
|
||||
|
||||
echo "== rrdp_state_dump ==" >&2
|
||||
state_start_s="$(date +%s)"
|
||||
cargo run --release --bin rrdp_state_dump -- --db "$DB_DIR" >"$RRDP_STATE_TSV"
|
||||
state_end_s="$(date +%s)"
|
||||
state_duration_s="$((state_end_s - state_start_s))"
|
||||
|
||||
script_end_s="$(date +%s)"
|
||||
total_duration_s="$((script_end_s - script_start_s))"
|
||||
|
||||
echo "== write run meta + summary ==" >&2
|
||||
TAL_URL="$TAL_URL" \
|
||||
DB_DIR="$DB_DIR" \
|
||||
REPORT_JSON="$REPORT_JSON" \
|
||||
RUN_LOG="$RUN_LOG" \
|
||||
HTTP_TIMEOUT_SECS="$HTTP_TIMEOUT_SECS" \
|
||||
RSYNC_TIMEOUT_SECS="$RSYNC_TIMEOUT_SECS" \
|
||||
VALIDATION_TIME_ARG="$VALIDATION_TIME" \
|
||||
RUN_DURATION_S="$run_duration_s" \
|
||||
DB_STATS_DURATION_S="$db_stats_duration_s" \
|
||||
STATE_DURATION_S="$state_duration_s" \
|
||||
TOTAL_DURATION_S="$total_duration_s" \
|
||||
python3 - "$REPORT_JSON" "$RUN_META_JSON" "$SUMMARY_MD" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
report_path = Path(sys.argv[1])
|
||||
meta_path = Path(sys.argv[2])
|
||||
summary_path = Path(sys.argv[3])
|
||||
|
||||
rep = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
download_stats = rep.get("download_stats") or {}
|
||||
meta = {
|
||||
"recorded_at_utc": now,
|
||||
"tal_url": os.environ["TAL_URL"],
|
||||
"db_dir": os.environ["DB_DIR"],
|
||||
"report_json": os.environ["REPORT_JSON"],
|
||||
"run_log": os.environ["RUN_LOG"],
|
||||
"validation_time_rfc3339_utc": rep["meta"]["validation_time_rfc3339_utc"],
|
||||
"http_timeout_secs": int(os.environ["HTTP_TIMEOUT_SECS"]),
|
||||
"rsync_timeout_secs": int(os.environ["RSYNC_TIMEOUT_SECS"]),
|
||||
"validation_time_arg": os.environ.get("VALIDATION_TIME_ARG",""),
|
||||
"durations_secs": {
|
||||
"rpki_run": int(os.environ["RUN_DURATION_S"]),
|
||||
"db_stats_exact": int(os.environ["DB_STATS_DURATION_S"]),
|
||||
"rrdp_state_dump": int(os.environ["STATE_DURATION_S"]),
|
||||
"total_script": int(os.environ["TOTAL_DURATION_S"]),
|
||||
},
|
||||
"counts": {
|
||||
"publication_points_processed": rep["tree"]["instances_processed"],
|
||||
"publication_points_failed": rep["tree"]["instances_failed"],
|
||||
"vrps": len(rep["vrps"]),
|
||||
"aspas": len(rep["aspas"]),
|
||||
"audit_publication_points": len(rep["publication_points"]),
|
||||
},
|
||||
"download_stats": download_stats,
|
||||
}
|
||||
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
lines = []
|
||||
lines.append("# Manual full sync summary\\n\\n")
|
||||
lines.append(f"- recorded_at_utc: `{now}`\\n")
|
||||
lines.append(f"- tal_url: `{meta['tal_url']}`\\n")
|
||||
lines.append(f"- db: `{meta['db_dir']}`\\n")
|
||||
lines.append(f"- report_json: `{meta['report_json']}`\\n")
|
||||
lines.append(f"- validation_time: `{meta['validation_time_rfc3339_utc']}`\\n\\n")
|
||||
lines.append("## Results\\n\\n")
|
||||
lines.append("| metric | value |\\n")
|
||||
lines.append("|---|---:|\\n")
|
||||
for k in ["publication_points_processed","publication_points_failed","vrps","aspas","audit_publication_points"]:
|
||||
lines.append(f"| {k} | {meta['counts'][k]} |\\n")
|
||||
lines.append("\\n")
|
||||
lines.append("## Durations (seconds)\\n\\n")
|
||||
lines.append("| step | seconds |\\n")
|
||||
lines.append("|---|---:|\\n")
|
||||
for k,v in meta["durations_secs"].items():
|
||||
lines.append(f"| {k} | {v} |\\n")
|
||||
lines.append("\\n")
|
||||
|
||||
lines.append("## Download Stats\\n\\n")
|
||||
lines.append("- raw events: `report_json.downloads`\\n")
|
||||
lines.append("- aggregated: `report_json.download_stats` (copied into meta.json)\\n\\n")
|
||||
|
||||
def fmt_u(v):
|
||||
if v is None:
|
||||
return ""
|
||||
return str(v)
|
||||
|
||||
by_kind = download_stats.get("by_kind") or {}
|
||||
lines.append("| kind | ok | fail | duration_ms_total | bytes_total | objects_count_total | objects_bytes_total |\\n")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|\\n")
|
||||
for kind in sorted(by_kind.keys()):
|
||||
st = by_kind[kind] or {}
|
||||
lines.append(
|
||||
f"| {kind} | {st.get('ok_total',0)} | {st.get('fail_total',0)} | {st.get('duration_ms_total',0)} | {fmt_u(st.get('bytes_total'))} | {fmt_u(st.get('objects_count_total'))} | {fmt_u(st.get('objects_bytes_total'))} |\\n"
|
||||
)
|
||||
lines.append("\\n")
|
||||
|
||||
summary_path.write_text("".join(lines), encoding="utf-8")
|
||||
print(summary_path)
|
||||
PY
|
||||
|
||||
echo "== done ==" >&2
|
||||
echo "artifacts:" >&2
|
||||
echo "- db: $DB_DIR" >&2
|
||||
echo "- report: $REPORT_JSON" >&2
|
||||
echo "- run log: $RUN_LOG" >&2
|
||||
echo "- db stats: $DB_STATS_TXT" >&2
|
||||
echo "- rrdp state: $RRDP_STATE_TSV" >&2
|
||||
echo "- meta json: $RUN_META_JSON" >&2
|
||||
echo "- summary md: $SUMMARY_MD" >&2
|
||||
123
scripts/stage2_perf_compare_m4.sh
Executable file
123
scripts/stage2_perf_compare_m4.sh
Executable file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# M4: Compare decode+profile (OURS) vs routinator baseline (rpki crate 0.19.1)
|
||||
# on selected_der_v2 fixtures (cer/crl/manifest/roa/aspa).
|
||||
#
|
||||
# Outputs under:
|
||||
# - rpki/target/bench/stage2_selected_der_v2_routinator_decode_release.{csv,md}
|
||||
# - rpki/target/bench/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.{csv,md}
|
||||
#
|
||||
# Notes:
|
||||
# - OURS decode benchmark is produced by:
|
||||
# `cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture`
|
||||
# and writes `stage2_selected_der_v2_decode_release.csv` when BENCH_OUT_CSV is set.
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
RPKI_DIR="$ROOT_DIR"
|
||||
|
||||
OUT_DIR="$RPKI_DIR/target/bench"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
OURS_CSV="${OURS_CSV:-$OUT_DIR/stage2_selected_der_v2_decode_release.csv}"
|
||||
|
||||
ROUT_CSV="${ROUT_CSV:-$OUT_DIR/stage2_selected_der_v2_routinator_decode_release.csv}"
|
||||
ROUT_MD="${ROUT_MD:-$OUT_DIR/stage2_selected_der_v2_routinator_decode_release.md}"
|
||||
|
||||
COMPARE_CSV="${COMPARE_CSV:-$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.csv}"
|
||||
COMPARE_MD="${COMPARE_MD:-$OUT_DIR/stage2_selected_der_v2_compare_ours_vs_routinator_decode_release.md}"
|
||||
|
||||
WARMUP_ITERS="${WARMUP_ITERS:-10}"
|
||||
ROUNDS="${ROUNDS:-3}"
|
||||
MIN_ROUND_MS="${MIN_ROUND_MS:-200}"
|
||||
|
||||
if [[ ! -f "$OURS_CSV" ]]; then
|
||||
echo "ERROR: missing OURS CSV: $OURS_CSV" >&2
|
||||
echo "Hint: run:" >&2
|
||||
echo " cd rpki && BENCH_WARMUP_ITERS=$WARMUP_ITERS BENCH_ROUNDS=$ROUNDS BENCH_MIN_ROUND_MS=$MIN_ROUND_MS \\" >&2
|
||||
echo " BENCH_OUT_CSV=target/bench/stage2_selected_der_v2_decode_release.csv \\" >&2
|
||||
echo " cargo test --release --test bench_stage2_decode_profile_selected_der_v2 -- --ignored --nocapture" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "[1/2] Run routinator baseline bench (release)..." >&2
|
||||
(cd "$RPKI_DIR/benchmark/routinator_object_bench" && cargo run --release -q -- \
|
||||
--dir "$RPKI_DIR/tests/benchmark/selected_der_v2" \
|
||||
--warmup-iters "$WARMUP_ITERS" \
|
||||
--rounds "$ROUNDS" \
|
||||
--min-round-ms "$MIN_ROUND_MS" \
|
||||
--out-csv "$ROUT_CSV" \
|
||||
--out-md "$ROUT_MD")
|
||||
|
||||
echo "[2/2] Join CSVs + compute ratios..." >&2
|
||||
python3 - "$OURS_CSV" "$ROUT_CSV" "$COMPARE_CSV" "$COMPARE_MD" <<'PY'
|
||||
import csv
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ours_path = Path(sys.argv[1])
|
||||
rout_path = Path(sys.argv[2])
|
||||
out_csv_path = Path(sys.argv[3])
|
||||
out_md_path = Path(sys.argv[4])
|
||||
|
||||
def read_csv(path: Path):
|
||||
with path.open(newline="") as f:
|
||||
return list(csv.DictReader(f))
|
||||
|
||||
ours_rows = read_csv(ours_path)
|
||||
rout_rows = read_csv(rout_path)
|
||||
|
||||
rout_by_key = {(r["type"], r["sample"]): r for r in rout_rows}
|
||||
|
||||
out_rows = []
|
||||
for r in ours_rows:
|
||||
key = (r["type"], r["sample"])
|
||||
rr = rout_by_key.get(key)
|
||||
if rr is None:
|
||||
raise SystemExit(f"missing routinator row for {key}")
|
||||
|
||||
ours_ns = float(r["avg_ns_per_op"])
|
||||
rout_ns = float(rr["avg_ns_per_op"])
|
||||
ratio = (ours_ns / rout_ns) if rout_ns != 0.0 else float("inf")
|
||||
|
||||
out_rows.append({
|
||||
"type": r["type"],
|
||||
"sample": r["sample"],
|
||||
"size_bytes": r["size_bytes"],
|
||||
"complexity": r["complexity"],
|
||||
"ours_avg_ns_per_op": f"{ours_ns:.2f}",
|
||||
"ours_ops_per_sec": f"{float(r['ops_per_sec']):.2f}",
|
||||
"rout_avg_ns_per_op": f"{rout_ns:.2f}",
|
||||
"rout_ops_per_sec": f"{float(rr['ops_per_sec']):.2f}",
|
||||
"ratio_ours_over_rout": f"{ratio:.4f}",
|
||||
})
|
||||
|
||||
out_rows.sort(key=lambda x: (x["type"], x["sample"]))
|
||||
|
||||
out_csv_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with out_csv_path.open("w", newline="") as f:
|
||||
w = csv.DictWriter(f, fieldnames=list(out_rows[0].keys()))
|
||||
w.writeheader()
|
||||
w.writerows(out_rows)
|
||||
|
||||
lines = []
|
||||
lines.append("# Stage2 ours vs routinator (decode+profile, selected_der_v2)\n")
|
||||
lines.append(f"- ours_csv: `{ours_path}`\n")
|
||||
lines.append(f"- rout_csv: `{rout_path}`\n")
|
||||
lines.append("\n")
|
||||
lines.append("| type | sample | size_bytes | complexity | ours ns/op | rout ns/op | ratio |\n")
|
||||
lines.append("|---|---|---:|---:|---:|---:|---:|\n")
|
||||
for r in out_rows:
|
||||
lines.append(
|
||||
f"| {r['type']} | {r['sample']} | {r['size_bytes']} | {r['complexity']} | "
|
||||
f"{r['ours_avg_ns_per_op']} | {r['rout_avg_ns_per_op']} | {r['ratio_ours_over_rout']} |\n"
|
||||
)
|
||||
|
||||
out_md_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_md_path.write_text("".join(lines), encoding="utf-8")
|
||||
PY
|
||||
|
||||
echo "Done." >&2
|
||||
echo "- routinator CSV: $ROUT_CSV" >&2
|
||||
echo "- compare CSV: $COMPARE_CSV" >&2
|
||||
echo "- compare MD: $COMPARE_MD" >&2
|
||||
@@ -182,6 +182,8 @@ RFC 引用:RFC 5280 §4.2.2.1;RFC 5280 §4.2.2.2;RFC 5280 §4.2.1.6。
|
||||
| `1.2.840.113549.1.9.5` | CMS signedAttrs: signing-time | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) |
|
||||
| `1.2.840.113549.1.9.16.1.24` | ROA eContentType: id-ct-routeOriginAuthz | RFC 9582 §3 |
|
||||
| `1.2.840.113549.1.9.16.1.26` | Manifest eContentType: id-ct-rpkiManifest | RFC 9286 §4.1 |
|
||||
| `1.2.840.113549.1.9.16.1.35` | Ghostbusters eContentType: id-ct-rpkiGhostbusters | RFC 6493 §6;RFC 6493 §9.1 |
|
||||
| `1.2.840.113549.1.9.16.1.49` | ASPA eContentType: id-ct-ASPA | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||
| `1.3.6.1.5.5.7.1.1` | X.509 v3 扩展:authorityInfoAccess | RFC 5280 §4.2.2.1 |
|
||||
| `1.3.6.1.5.5.7.1.11` | X.509 v3 扩展:subjectInfoAccess | RFC 5280 §4.2.2.2;RPKI 约束见 RFC 6487 §4.8.8 |
|
||||
| `1.3.6.1.5.5.7.48.2` | AIA accessMethod: id-ad-caIssuers | RFC 5280 §4.2.2.1 |
|
||||
|
||||
108
specs/01_tal.md
108
specs/01_tal.md
@@ -1,36 +1,94 @@
|
||||
# 01. Trust Anchor Locator (TAL)
|
||||
# 01. TAL(Trust Anchor Locator)
|
||||
|
||||
## 1.1 对象定位
|
||||
TAL是一个数据格式/配置文件,目的是告诉RP信任锚的公钥是什么,以及相关对象可以从哪里获取。
|
||||
|
||||
## 1.2 数据格式 (RFC 8630 §2.2)
|
||||
TAL是一个配置文件,格式定义如下:
|
||||
```
|
||||
The TAL is an ordered sequence of:
|
||||
1. an optional comment section consisting of one or more lines each starting with the "#" character, followed by human-readable informational UTF-8 text, conforming to the restrictions defined
|
||||
in Section 2 of [RFC5198], and ending with a line break,
|
||||
2. a URI section that is comprised of one or more ordered lines, each containing a TA URI, and ending with a line break,
|
||||
3. a line break, and
|
||||
4. a subjectPublicKeyInfo [RFC5280] in DER format [X.509], encoded in base64 (see Section 4 of [RFC4648]). To avoid long lines,
|
||||
line breaks MAY be inserted into the base64-encoded string.
|
||||
Note that line breaks in this file can use either "<CRLF>" or "<LF>".
|
||||
TAL(Trust Anchor Locator)用于向 RP 提供:
|
||||
|
||||
1) 可检索“当前 TA 证书”的一个或多个 URI;以及
|
||||
2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。
|
||||
|
||||
RFC 8630 §2;RFC 8630 §2.3。
|
||||
|
||||
## 1.2 原始载体与编码
|
||||
|
||||
- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。
|
||||
- 行结束:允许 `CRLF` 或 `LF`。RFC 8630 §2.2。
|
||||
- 结构:`[可选注释区] + URI 区 + 空行 + Base64(SPKI DER)`。RFC 8630 §2.2。
|
||||
|
||||
### 1.2.1 注释区
|
||||
|
||||
- 一行或多行,以 `#` 开头,后随人类可读 UTF-8 文本。RFC 8630 §2.2。
|
||||
- 注释行文本需符合 RFC 5198 §2 的限制(RFC 8630 §2.2 引用)。
|
||||
|
||||
### 1.2.2 URI 区
|
||||
|
||||
- 一行或多行,每行一个 TA URI,按序排列。RFC 8630 §2.2。
|
||||
- TA URI **MUST** 是 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||
|
||||
### 1.2.3 空行分隔
|
||||
|
||||
- URI 区后必须有一个额外的换行(即空行),用于与 Base64 区分隔。RFC 8630 §2.2(第 3 点)。
|
||||
|
||||
### 1.2.4 SPKI(Base64)
|
||||
|
||||
- `subjectPublicKeyInfo` 以 DER 编码(ASN.1)后,再 Base64 编码表示。RFC 8630 §2.2(第 4 点)。
|
||||
- 为避免长行,Base64 字符串中 **MAY** 插入换行。RFC 8630 §2.2。
|
||||
- SPKI ASN.1 类型来自 X.509 / RFC 5280。RFC 8630 §2.2(第 4 点);RFC 5280 §4.1.2.7。
|
||||
|
||||
#### 1.2.4.1 `SubjectPublicKeyInfo` 的 ASN.1 定义(RFC 5280 §4.1)
|
||||
|
||||
TAL 中携带的是一个 X.509 `SubjectPublicKeyInfo` 的 DER 字节串(再 Base64)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||
|
||||
```asn1
|
||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
algorithm AlgorithmIdentifier,
|
||||
subjectPublicKey BIT STRING }
|
||||
```
|
||||
|
||||
## 1.3 抽象数据模型
|
||||
其中 `algorithm`/`subjectPublicKey` 的取值受 RPKI 算法 profile 约束(例如 RSA 2048 + SHA-256 等;SKI/AKI 计算仍用 SHA-1)。RFC 5280 §4.1.2.7;RFC 7935 §2-§3.1;RFC 6487 §4.8.2-§4.8.3。
|
||||
|
||||
### 1.3.1 TAL
|
||||
## 1.3 解析规则(语义层)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|----------|-------------|-------------------------|--------------------------------------------|---------------|
|
||||
| uris | Vec<TalUri> | 指向TA的URI列表 | 允许rsync和https协议。 | RFC 8630 §2.1 |
|
||||
| comment | Vec<String> | 注释(可选) | | RFC 8630 §2.2 |
|
||||
| spki_der | Vec<u8> | 原始的subjectPublicKeyInfo | x.509 SubjectPublicKeyInfo DER编码,再base64编码 | RFC 8630 §2.2 |
|
||||
输入:`TalFileBytes: bytes`。
|
||||
|
||||
解析步骤:
|
||||
|
||||
### 1.3.2 TalUri
|
||||
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
|
||||
2) 从文件开头读取所有以 `#` 开头的行,作为 `comments`(保留去掉 `#` 后的 UTF-8 文本或保留原始行均可,但需保持 UTF-8)。RFC 8630 §2.2。
|
||||
3) 继续读取一行或多行非空行,作为 `ta_uris`(保持顺序)。RFC 8630 §2.2(第 2 点)。
|
||||
4) 读取一个空行(必须存在)。RFC 8630 §2.2(第 3 点)。
|
||||
5) 将剩余行拼接为 Base64 文本(移除行分隔),Base64 解码得到 `subject_public_key_info_der`。RFC 8630 §2.2(第 4 点)。
|
||||
6) 可选:将 `subject_public_key_info_der` 解析为 X.509 `SubjectPublicKeyInfo` 结构(用于与 TA 证书比对)。RFC 8630 §2.3;RFC 5280 §4.1.2.7。
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|-------|--------|---------|---------|---------------|
|
||||
| Rsync | String | rsync地址 | | RFC 8630 §2.1 |
|
||||
| Https | String | https地址 | | RFC 8630 §2.1 |
|
||||
URI 解析与约束:
|
||||
|
||||
- `ta_uris[*]` 的 scheme **MUST** 为 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||
- 每个 `ta_uri` **MUST** 指向“单个对象”,且 **MUST NOT** 指向目录或集合。RFC 8630 §2.3。
|
||||
|
||||
## 1.4 抽象数据模型(接口)
|
||||
|
||||
### 1.4.1 `Tal`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw` | `bytes` | TAL 原始文件字节 | 原样保留(可选但建议) | RFC 8630 §2.2 |
|
||||
| `comments` | `list[Utf8Text]` | 注释行(按出现顺序) | 每行以 `#` 开头;文本为 UTF-8;内容限制见 RFC 5198 §2 | RFC 8630 §2.2 |
|
||||
| `ta_uris` | `list[Uri]` | TA 证书位置列表 | 至少 1 个;按序;每个 scheme 必须是 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||
| `subject_public_key_info_der` | `DerBytes` | TA 证书 SPKI 的期望 DER | Base64 解码所得 DER;Base64 中可有换行 | RFC 8630 §2.2 |
|
||||
|
||||
### 1.4.2 `TaUri`(可选细化)
|
||||
|
||||
> 若你的实现希望对 URI 做更强类型化,可在 `Tal.ta_uris` 上进一步拆分为 `TaUri` 结构。
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `uri` | `Uri` | 完整 URI 文本 | scheme 为 `rsync` 或 `https` | RFC 8630 §2.2 |
|
||||
| `scheme` | `enum` | `rsync` / `https` | 从 `uri` 解析 | RFC 8630 §2.2 |
|
||||
|
||||
## 1.5 字段级约束清单(实现对照)
|
||||
|
||||
- TAL 由(可选)注释区 + URI 区 + 空行 + Base64(SPKI DER) 组成。RFC 8630 §2.2。
|
||||
- URI 区至少 1 行,每行一个 TA URI,顺序有意义。RFC 8630 §2.2。
|
||||
- TA URI 仅允许 `rsync` 或 `https`。RFC 8630 §2.2。
|
||||
- Base64 区允许插入换行。RFC 8630 §2.2。
|
||||
- 每个 TA URI 必须引用单个对象,不能指向目录/集合。RFC 8630 §2.3。
|
||||
|
||||
121
specs/02_ta.md
121
specs/02_ta.md
@@ -1,121 +0,0 @@
|
||||
# 02. Trust Anchor (TA)
|
||||
|
||||
## 2.1 对象定位
|
||||
TA是一个自签名的CA证书。
|
||||
|
||||
## 2.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 certificates.
|
||||
- 编码:DER(遵循 RFC 5280 的 certificate 结构与字段语义,但受限于RFC 8630 §2.3)
|
||||
|
||||
|
||||
## 2.3 抽象数据类型
|
||||
|
||||
### 2.3.1 TA
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|-------------------|------------------|---------------|---------|---------------|
|
||||
| name | String | 标识该TA,如apnic等 | | |
|
||||
| cert_der | Vec<u8> | 原始DER内容 | | |
|
||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
||||
| resource | ResourceSet | 资源集合 | | |
|
||||
| publication_point | Uri | 获取该TA的URI | | |
|
||||
|
||||
### 2.3.2 ResourceSet
|
||||
资源集合是来自RFC 3779的IP地址块(§2)和AS号段(§3),受约束于RFC 8630 §2.3
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|------|----------------|--------|-------------|---------------------------|
|
||||
| ips | IpResourceSet | IP地址集合 | 不能是inherit | RFC 3779 §2和RFC 8630 §2.3 |
|
||||
| asns | AsnResourceSet | ASN集合 | 不能是inherit | RFC 3779 §3和RFC 8630 §2.3 |
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.3 IpResourceSet)
|
||||
|
||||
[//]: # (包括IPv4和IPv6的前缀表示)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|----|------------------------|----------|-------------|--------------|)
|
||||
|
||||
[//]: # (| v4 | PrefixSet<Ipv4Prefix> | IPv4前缀集合 | | RFC 3779 §2 |)
|
||||
|
||||
[//]: # (| v6 | PrefixSet<Ipv6Prefix> | IPv6前缀集合 | | RFC 3779 §2 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.4 AsnResourceSet)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|-------|--------------------|-------|-------------|-------------|)
|
||||
|
||||
[//]: # (| range | RangeSet<AsnBlock> | ASN集合 | | RFC 3779 §3 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.5 Ipv4Prefix)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|------|-----|-----|---------|-------------|)
|
||||
|
||||
[//]: # (| addr | u32 | 地址 | | RFC 3779 §2 |)
|
||||
|
||||
[//]: # (| len | u8 | 长度 | 0-32 | RFC 3779 §2 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.6 Ipv6Prefix)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|------|------|-----|---------|-------------|)
|
||||
|
||||
[//]: # (| addr | u128 | 地址 | | RFC 3779 §2 |)
|
||||
|
||||
[//]: # (| len | u8 | 长度 | 0-128 | RFC 3779 §2 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.7 AsnBlock)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|----------|----------|-------|---------|--------------|)
|
||||
|
||||
[//]: # (| asn | Asn | ASN | | RFC 3779 §3 |)
|
||||
|
||||
[//]: # (| asnRange | AsnRange | ASN范围 | | RFC 3779 §3 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.8 Asn)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|-----|-----|-----|---------|-------------|)
|
||||
|
||||
[//]: # (| asn | u32 | ASN | | RFC 3779 §3 |)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (### 2.3.8 AsnRange)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |)
|
||||
|
||||
[//]: # (|-----|-----|-------|---------|--------------|)
|
||||
|
||||
[//]: # (| min | Asn | 最小ASN | | RFC 3779 §3 |)
|
||||
|
||||
[//]: # (| max | Asn | 最大ASN | | RFC 3779 §3 |)
|
||||
|
||||
# 2.4 TA校验流程(RFC 8630 §3)
|
||||
1. 从TAL的URI列表中获取证书对象。(顺序访问,若前面失效,再访问后面的)
|
||||
2. 验证证书格式,必须是当前、有效的自签名RPKI证书。
|
||||
3. 验证公钥匹配。TAL中的SubjectPublicKeyInfo与下载证书的公钥一致。
|
||||
4. 其他检查。
|
||||
5. 更新本地存储库缓存。
|
||||
88
specs/02_ta_certificate.md
Normal file
88
specs/02_ta_certificate.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# 02. TA(Trust Anchor)自签名证书
|
||||
|
||||
## 2.1 对象定位
|
||||
|
||||
在 RP 侧,“信任锚(Trust Anchor, TA)”以一个**自签名 CA 资源证书**体现,其可获取位置与期望公钥由 TAL 提供。RFC 8630 §2.3。
|
||||
|
||||
本文件描述两个紧密相关的数据对象:
|
||||
|
||||
1) `TaCertificate`:TA 自签名资源证书本体(X.509 DER)
|
||||
2) `TrustAnchor`:语义组合对象(`TAL` + `TaCertificate` 的绑定语义)
|
||||
|
||||
## 2.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 证书(通常以 `.cer` 存放于仓库,但文件扩展名不作为语义依据)。
|
||||
- 编码:DER。TA 证书必须符合 RPKI 资源证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
|
||||
### 2.2.1 X.509 Certificate 的 ASN.1 定义(RFC 5280 §4.1;TA 与 RC 共享)
|
||||
|
||||
TA 证书与普通资源证书(RC)在编码层面都是 X.509 `Certificate`(DER)。其 ASN.1 定义如下:RFC 5280 §4.1。
|
||||
|
||||
```asn1
|
||||
Certificate ::= SEQUENCE {
|
||||
tbsCertificate TBSCertificate,
|
||||
signatureAlgorithm AlgorithmIdentifier,
|
||||
signatureValue BIT STRING }
|
||||
```
|
||||
|
||||
其中 `tbsCertificate.extensions`(v3 扩展)是 RPKI 语义的主要承载处(IP/AS 资源扩展、SIA/AIA/CRLDP 等)。RFC 5280 §4.1;RPKI 对字段/扩展存在性与关键性约束见 RFC 6487 §4。
|
||||
|
||||
> 说明:更完整的 RC 编码层结构(包括 Extension 外层“extnValue 二次 DER 解码”的套娃方式)在 `03_resource_certificate_rc.md` 与 `00_common_types.md` 中给出。
|
||||
|
||||
## 2.3 TA 证书的 RPKI 语义约束(在 RC profile 基础上额外强调)
|
||||
|
||||
### 2.3.1 自签名与 profile
|
||||
|
||||
- TA URI 指向的对象 **MUST** 是一个**自签名 CA 证书**,并且 **MUST** 符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
- 自签名证书在 RC profile 下的通用差异(例如 CRLDP/AIA 的省略规则、AKI 的规则)见 RFC 6487。RFC 6487 §4.8.3;RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||
|
||||
### 2.3.2 INR(IP/AS 资源扩展)在 TA 上的额外约束
|
||||
|
||||
- TA 的 INR 扩展(IP/AS 资源扩展,RFC 3779)**MUST** 是非空资源集合。RFC 8630 §2.3。
|
||||
- TA 的 INR 扩展 **MUST NOT** 使用 `inherit` 形式。RFC 8630 §2.3。
|
||||
- 说明:一般 RC profile 允许 `inherit`。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3.5;RFC 3779 §3.2.3.3。
|
||||
|
||||
### 2.3.3 TAL ↔ TA 公钥绑定
|
||||
|
||||
- 用于验证 TA 的公钥(来自 TAL 中的 SPKI)**MUST** 与 TA 证书中的 `subjectPublicKeyInfo` 相同。RFC 8630 §2.3。
|
||||
|
||||
### 2.3.4 TA 稳定性语义(实现需建模为“约束/假设”,但不属于验证结果态)
|
||||
|
||||
- TA 公钥与 TAL 中公钥必须保持稳定(用于 RP 侧长期信任锚)。RFC 8630 §2.3。
|
||||
|
||||
### 2.3.5 TA 与 CRL/Manifest 的关系(语义)
|
||||
|
||||
- RFC 8630 指出:TA 为自签名证书,没有对应 CRL,且不会被 manifest 列出;TA 的获取/轮换由 TAL 控制。RFC 8630 §2.3。
|
||||
|
||||
> 注:这条更偏“发布/运维语义”,但对数据对象建模有影响:`TrustAnchor` 组合对象不应依赖 CRL/MFT 的存在。
|
||||
|
||||
## 2.4 抽象数据模型(接口)
|
||||
|
||||
### 2.4.1 `TaCertificate`
|
||||
|
||||
> 该对象在字段层面复用 `RC(CA)` 的语义模型(见 `03_resource_certificate_rc.md`),但增加 TA 特有约束。
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_der` | `DerBytes` | TA 证书 DER | X.509 DER;证书 profile 约束见 RC 文档 | RFC 8630 §2.3;RFC 6487 §4 |
|
||||
| `rc_ca` | `ResourceCaCertificate` | 以 RC(CA) 语义解析出的字段集合 | 必须满足“自签名 CA”分支约束;且 INR 必须非空且不允许 inherit | RFC 8630 §2.3;RFC 6487 §4;RFC 3779 §2/§3 |
|
||||
|
||||
### 2.4.2 `TrustAnchor`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `tal` | `Tal` | TAL 文件语义对象 | 见 `01_tal.md` | RFC 8630 §2.2 |
|
||||
| `ta_certificate` | `TaCertificate` | TA 证书语义对象 | TA URI 指向的对象 | RFC 8630 §2.3 |
|
||||
| `tal_spki_der` | `DerBytes` | 从 TAL 解析出的 SPKI DER | `tal.subject_public_key_info_der` | RFC 8630 §2.2 |
|
||||
| `ta_spki_der` | `DerBytes` | 从 TA 证书抽取的 SPKI DER | `ta_certificate` 的 `subjectPublicKeyInfo` | RFC 8630 §2.3;RFC 5280 §4.1.2.7 |
|
||||
|
||||
**绑定约束(字段级)**
|
||||
|
||||
- `tal_spki_der` 必须与 `ta_spki_der` 完全相等(字节层面的 DER 等价)。RFC 8630 §2.3。
|
||||
|
||||
## 2.5 字段级约束清单(实现对照)
|
||||
|
||||
- TA URI 指向的对象必须是自签名 CA 证书,且符合 RPKI 证书 profile。RFC 8630 §2.3;RFC 6487 §4。
|
||||
- TA 的 INR 扩展必须非空,且不得使用 inherit。RFC 8630 §2.3。
|
||||
- TAL 中 SPKI 必须与 TA 证书的 `subjectPublicKeyInfo` 匹配。RFC 8630 §2.3。
|
||||
- TA 不依赖 CRL/MFT(无对应 CRL,且不被 manifest 列出)。RFC 8630 §2.3。
|
||||
314
specs/03_rc.md
314
specs/03_rc.md
@@ -1,314 +0,0 @@
|
||||
# 03. RC (Resource Certifications)
|
||||
|
||||
## 3.1 对象定位
|
||||
RC是资源证书,包括CA和EE
|
||||
|
||||
## 3.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 certificates.
|
||||
- 编码:DER(遵循 RFC 5280 的 Certificate 结构与字段语义,但受 RPKI profile 限制)RFC 6487 §4
|
||||
|
||||
### 3.2.1 基本语法(RFC 5280 §4,RFC 6487 )
|
||||
|
||||
RC是遵循RFC5280定义的X.509Certificate语法(RFC 5280 §4),并且符合RFC 6487 §4的约束。只选取RFC 6487 §4章节列出来的字段。(Unless specifically noted as being OPTIONAL, all the fields listed
|
||||
here MUST be present, and any other fields MUST NOT appear in a
|
||||
conforming resource certificate.)
|
||||
|
||||
```
|
||||
Certificate ::= SEQUENCE {
|
||||
tbsCertificate TBSCertificate,
|
||||
signatureAlgorithm AlgorithmIdentifier,
|
||||
signatureValue BIT STRING
|
||||
}
|
||||
|
||||
TBSCertificate ::= SEQUENCE {
|
||||
version [0] EXPLICIT Version MUST be v3,
|
||||
serialNumber CertificateSerialNumber,
|
||||
signature AlgorithmIdentifier,
|
||||
issuer Name,
|
||||
subject Name,
|
||||
validity Validity,
|
||||
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||
extensions [3] EXPLICIT Extensions OPTIONAL
|
||||
-- If present, version MUST be v3
|
||||
}
|
||||
|
||||
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
||||
|
||||
CertificateSerialNumber ::= INTEGER
|
||||
|
||||
Validity ::= SEQUENCE {
|
||||
notBefore Time,
|
||||
notAfter Time }
|
||||
|
||||
Time ::= CHOICE {
|
||||
utcTime UTCTime,
|
||||
generalTime GeneralizedTime }
|
||||
|
||||
UniqueIdentifier ::= BIT STRING
|
||||
|
||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
algorithm AlgorithmIdentifier,
|
||||
subjectPublicKey BIT STRING }
|
||||
|
||||
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||
|
||||
Extension ::= SEQUENCE {
|
||||
extnID OBJECT IDENTIFIER,
|
||||
critical BOOLEAN DEFAULT FALSE,
|
||||
extnValue OCTET STRING
|
||||
-- contains the DER encoding of an ASN.1 value
|
||||
-- corresponding to the extension type identified
|
||||
-- by extnID
|
||||
}
|
||||
```
|
||||
|
||||
> 其中`Name` "a valid X.501 distinguished name"(RFC 6487 §4.4)
|
||||
|
||||
### 3.2.2 证书扩展字段 (RFC 6487 §4.8)
|
||||
|
||||
RC的证书扩展字段按照RFC 6487 §4.8的规定,有以下几个扩展:
|
||||
|
||||
- Basic Constraints
|
||||
- Subject Key Identifier
|
||||
- Authority Key Identifier
|
||||
- Key Usage
|
||||
- Extended Key Usage(CA证书,以及验证RPKI对象的EE证书不能出现该字段。非RPKI对象的EE可以出现EKU,但必须为non-critical)
|
||||
- CRL Distribution Points
|
||||
- Authority Information Access
|
||||
- Subject Information Access
|
||||
- SIA for CA Certificates
|
||||
- SIA for EE Certificates
|
||||
- Certificate Policies
|
||||
- IP Resources
|
||||
- AS Resources
|
||||
|
||||
```
|
||||
# Basic Constraints
|
||||
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
||||
|
||||
BasicConstraints ::= SEQUENCE {
|
||||
cA BOOLEAN DEFAULT FALSE }
|
||||
|
||||
|
||||
# Subject Key Identifier
|
||||
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
||||
|
||||
SubjectKeyIdentifier ::= KeyIdentifier
|
||||
|
||||
KeyIdentifier ::= OCTET STRING
|
||||
|
||||
|
||||
# Authority Key Identifier
|
||||
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
||||
|
||||
AuthorityKeyIdentifier ::= SEQUENCE {
|
||||
keyIdentifier [0] KeyIdentifier OPTIONAL }
|
||||
|
||||
|
||||
# Key Usage
|
||||
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
||||
|
||||
KeyUsage ::= BIT STRING {
|
||||
digitalSignature (0),
|
||||
nonRepudiation (1), -- recent editions of X.509 have
|
||||
-- renamed this bit to contentCommitment
|
||||
keyEncipherment (2),
|
||||
dataEncipherment (3),
|
||||
keyAgreement (4),
|
||||
keyCertSign (5),
|
||||
cRLSign (6),
|
||||
encipherOnly (7),
|
||||
decipherOnly (8) }
|
||||
|
||||
|
||||
# Extended Key Usage
|
||||
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
||||
|
||||
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
||||
|
||||
KeyPurposeId ::= OBJECT IDENTIFIER
|
||||
|
||||
|
||||
# CRL Distribution Points
|
||||
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
||||
|
||||
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
||||
|
||||
DistributionPoint ::= SEQUENCE {
|
||||
distributionPoint [0] DistributionPointName OPTIONAL }
|
||||
|
||||
DistributionPointName ::= CHOICE {
|
||||
fullName [0] GeneralNames }
|
||||
|
||||
|
||||
## Authority Information Access
|
||||
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
||||
|
||||
AuthorityInfoAccessSyntax ::=
|
||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
# AccessDescription
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
# CA 证书发布位置
|
||||
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
||||
# OCSP 服务地址
|
||||
id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 }
|
||||
|
||||
|
||||
# Subject Information Access
|
||||
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
||||
|
||||
SubjectInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
## Subject Information Access for CA (RFC 6487 §4.8.8.1)
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 }
|
||||
|
||||
必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。
|
||||
必须存在一个accessMethod=id-ad-repiManifest, accessLocation=rsync URI,指向该CA的mft对象。
|
||||
|
||||
## Subject Information Access for EE (RFC 6487 §4.8.8.2)
|
||||
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 }
|
||||
|
||||
必须存在一个accessMethod=id-ad-signedObject, accessLocation=rsyncURI
|
||||
不允许其他的accessMethod
|
||||
|
||||
|
||||
# Certificate Policies
|
||||
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
||||
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
||||
|
||||
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
||||
|
||||
PolicyInformation ::= SEQUENCE {
|
||||
policyIdentifier CertPolicyId,
|
||||
policyQualifiers SEQUENCE SIZE (1..MAX) OF PolicyQualifierInfo OPTIONAL }
|
||||
|
||||
CertPolicyId ::= OBJECT IDENTIFIER
|
||||
|
||||
PolicyQualifierInfo ::= SEQUENCE {
|
||||
policyQualifierId PolicyQualifierId,
|
||||
qualifier ANY DEFINED BY policyQualifierId }
|
||||
|
||||
-- policyQualifierIds for Internet policy qualifiers
|
||||
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
||||
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
||||
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
||||
|
||||
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
||||
|
||||
Qualifier ::= CHOICE {
|
||||
cPSuri CPSuri,
|
||||
userNotice UserNotice }
|
||||
|
||||
CPSuri ::= IA5String
|
||||
|
||||
UserNotice ::= SEQUENCE {
|
||||
noticeRef NoticeReference OPTIONAL,
|
||||
explicitText DisplayText OPTIONAL }
|
||||
|
||||
NoticeReference ::= SEQUENCE {
|
||||
organization DisplayText,
|
||||
noticeNumbers SEQUENCE OF INTEGER }
|
||||
|
||||
DisplayText ::= CHOICE {
|
||||
ia5String IA5String (SIZE (1..200)),
|
||||
visibleString VisibleString (SIZE (1..200)),
|
||||
bmpString BMPString (SIZE (1..200)),
|
||||
utf8String UTF8String (SIZE (1..200)) }
|
||||
|
||||
|
||||
# IP Resources
|
||||
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
||||
|
||||
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
||||
|
||||
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
||||
addressFamily OCTET STRING (SIZE (2..3)),
|
||||
ipAddressChoice IPAddressChoice }
|
||||
|
||||
IPAddressChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
||||
|
||||
IPAddressOrRange ::= CHOICE {
|
||||
addressPrefix IPAddress,
|
||||
addressRange IPAddressRange }
|
||||
|
||||
IPAddressRange ::= SEQUENCE {
|
||||
min IPAddress,
|
||||
max IPAddress }
|
||||
|
||||
IPAddress ::= BIT STRING
|
||||
|
||||
|
||||
# AS Resources
|
||||
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
||||
ASIdentifiers ::= SEQUENCE {
|
||||
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
||||
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
||||
|
||||
ASIdentifierChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
||||
|
||||
ASIdOrRange ::= CHOICE {
|
||||
id ASId,
|
||||
range ASRange }
|
||||
|
||||
ASRange ::= SEQUENCE {
|
||||
min ASId,
|
||||
max ASId }
|
||||
|
||||
ASId ::= INTEGER
|
||||
```
|
||||
|
||||
# 3.3 抽象数据结构
|
||||
采用X509 Certificate + Resource + 约束校验的方式组合
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|----------|---------------------|----------|---------|---------------|
|
||||
| cert_der | Vec<u8> | 证书原始数据 | | |
|
||||
| cert | X509Certificate | 基础X509证书 | | RFC 5280 §4.1 |
|
||||
| resource | ResourceSet | 资源集合 | | |
|
||||
|
||||
|
||||
# 3.4 约束规则
|
||||
|
||||
## 3.4.1 Cert约束校验规则
|
||||
RFC 6487中规定的证书的字段参见[3.2.1 ](#321-基本语法rfc-5280-4rfc-6487-)
|
||||
-
|
||||
|
||||
| 字段 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|-----------|-------|----------------------------------------------|--------------|
|
||||
| version | 证书版本 | 必须是v3(值为2) | RFC6487 §4.1 |
|
||||
| serial | 证书编号 | 同一个CA签发的证书编号必须唯一 | RFC6487 §4.2 |
|
||||
| validity | 证书有效期 | notBefore:时间不能早于证书的生成时间。若时间段大于上级证书的有效期,也是有效的 | RFC6487 §4.6 |
|
||||
|
||||
|
||||
## 3.4.2 Cert Extentions中字段的约束校验规则
|
||||
RFC 6487中规定的扩展字段参见[3.2.2 ](#322-证书扩展字段-rfc-6487-48)
|
||||
|
||||
| 字段 | critical | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|----------------------------|----------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------|
|
||||
| basicConstraints | Y | 证书类型 | CA证书:cA=TRUE; EE证书:cA=FALSE | RFC6487 §4.8.1 |
|
||||
| subjectKeyIdentifier | N | 证书公钥 | SKI = SHA-1(DER-encoded SPKI bit string) | RFC6487 §4.8.2 |
|
||||
| authorityKeyIdentifier | N | 父证书的公钥 | 字段只包含keyIdentifier,不能包含authorityCertIssuer和authorityCertSerialNumber;除了自签名CA外,其余证书必须出现。自签名CA若出现该字段,则等于SKI | RFC6487 §4.8.3 |
|
||||
| keyUsage | Y | 证书公钥的用途权限 | CA证书:keyCertSign = TRUE, cRLSign = TRUE 其他都是FALSE。EE证书:digitalSignature = TRUE 其他都是FALSE | RFC6487 §4.8.4 |
|
||||
| extendedKeyUsage | N | 扩展证书公钥的用途权限 | CA证书:不能出现EKU;验证 RPKI 对象的 EE 证书:不能出现EKU;非 RPKI 对象的 EE:可以出现EKU,但必须为non-critical. | RFC6487 §4.8.5 |
|
||||
| cRLDistributionPoints | N | CRL的发布点位置 | 字段:distributionPoint,不能包含reasons、cRLIssuer。其中distributionPoint字段包含:fullName,不能包含nameRelativeToCRLIssuer。fullName的格式必须是URI。自签名证书禁止出现该字段。非自签名证书必须出现。一个CA只能有一个CRL。一个CRLDP只能包含一个distributionPoint。但一个distributionPoint字段中可以包含多于1个的URI,但必须包含rsync URI且必须是最新的。 | RFC6487 §4.8.6 |
|
||||
| authorityInformationAccess | N | 签发者的发布点位置 | 除了自签名的CA,必须出现。自签名CA,禁止出现。推荐的URI访问方式是rsync,并且rsyncURI的话,必须指定accessMethod=id-ad-caIssuers | RFC6487 §4.8.7 |
|
||||
| subjectInformationAccess | N | 发布点位置 | CA证书:必须存在。必须存在一个accessMethod=id-ad-caRepository,accessLocation=rsyncURI。必须存在一个accessMethod=id-ad-repiManifest,accessLocation=rsync URI,指向该CA的mft对象。 EE证书:必须存在。必须存在一个accessMethod=id-ad-signedObject,accessLocation=rsyncURI。不允许其他的accessMethod | RFC6487 §4.8.8 |
|
||||
| certificatePolicies | Y | 证书策略 | 必须存在,并且只能存在一种策略:RFC 6484 — RPKI Certificate Policy (CP) | RFC6487 §4.8.9 |
|
||||
| iPResources | Y | IP地址集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.10 |
|
||||
| aSResources | Y | ASN集合 | 所有的RPKI证书中必须包含IP Resources或者ASResources,或者两者都包含。 | RFC6487 §4.8.11 |
|
||||
|
||||
|
||||
460
specs/03_resource_certificate_rc.md
Normal file
460
specs/03_resource_certificate_rc.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# 03. RC(Resource Certificate:资源证书,CA/EE)
|
||||
|
||||
## 3.1 对象定位
|
||||
|
||||
资源证书(RC)是 X.509 v3 证书,遵循 PKIX profile(RFC 5280),并受 RPKI profile 进一步约束。RFC 6487 §4。
|
||||
|
||||
RC 在 RPKI 中至少分为两类语义用途:
|
||||
|
||||
- `CA 证书`:签发下级证书/CRL,并在 SIA 中声明发布点与 manifest。RFC 6487 §4.8.8.1。
|
||||
- `EE 证书`:用于验证某个 RPKI Signed Object(如 ROA/MFT),在 SIA 中指向被验证对象。RFC 6487 §4.8.8.2。
|
||||
|
||||
## 3.2 原始载体与编码
|
||||
|
||||
- 载体:X.509 证书。
|
||||
- 编码:DER。RFC 6487 §4(“valid X.509 public key certificate consistent with RFC 5280” + RPKI 限制)。
|
||||
|
||||
### 3.2.1 X.509 v3 证书基本语法(ASN.1;RFC 5280 §4.1)
|
||||
|
||||
资源证书在编码层面是 RFC 5280 定义的 X.509 v3 `Certificate`(DER),其中 `tbsCertificate` 携带主体字段与扩展集合(`Extensions`)。RFC 5280 §4.1。
|
||||
|
||||
```asn1
|
||||
Certificate ::= SEQUENCE {
|
||||
tbsCertificate TBSCertificate,
|
||||
signatureAlgorithm AlgorithmIdentifier,
|
||||
signatureValue BIT STRING }
|
||||
|
||||
TBSCertificate ::= SEQUENCE {
|
||||
version [0] EXPLICIT Version DEFAULT v1,
|
||||
serialNumber CertificateSerialNumber,
|
||||
signature AlgorithmIdentifier,
|
||||
issuer Name,
|
||||
validity Validity,
|
||||
subject Name,
|
||||
subjectPublicKeyInfo SubjectPublicKeyInfo,
|
||||
issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
-- If present, version MUST be v2 or v3
|
||||
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
||||
-- If present, version MUST be v2 or v3
|
||||
extensions [3] EXPLICIT Extensions OPTIONAL
|
||||
-- If present, version MUST be v3
|
||||
}
|
||||
|
||||
Version ::= INTEGER { v1(0), v2(1), v3(2) }
|
||||
|
||||
CertificateSerialNumber ::= INTEGER
|
||||
|
||||
Validity ::= SEQUENCE {
|
||||
notBefore Time,
|
||||
notAfter Time }
|
||||
|
||||
Time ::= CHOICE {
|
||||
utcTime UTCTime,
|
||||
generalTime GeneralizedTime }
|
||||
|
||||
UniqueIdentifier ::= BIT STRING
|
||||
|
||||
SubjectPublicKeyInfo ::= SEQUENCE {
|
||||
algorithm AlgorithmIdentifier,
|
||||
subjectPublicKey BIT STRING }
|
||||
|
||||
Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
||||
|
||||
Extension ::= SEQUENCE {
|
||||
extnID OBJECT IDENTIFIER,
|
||||
critical BOOLEAN DEFAULT FALSE,
|
||||
extnValue OCTET STRING
|
||||
-- contains the DER encoding of an ASN.1 value
|
||||
-- corresponding to the extension type identified
|
||||
-- by extnID
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2.2 AlgorithmIdentifier(ASN.1;RFC 5280 §4.1.1.2)
|
||||
|
||||
```asn1
|
||||
AlgorithmIdentifier ::= SEQUENCE {
|
||||
algorithm OBJECT IDENTIFIER,
|
||||
parameters ANY DEFINED BY algorithm OPTIONAL }
|
||||
```
|
||||
|
||||
### 3.2.3 Name / DN 结构(ASN.1;RFC 5280 §4.1.2.4)
|
||||
|
||||
```asn1
|
||||
Name ::= CHOICE { -- only one possibility for now --
|
||||
rdnSequence RDNSequence }
|
||||
|
||||
RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
|
||||
|
||||
RelativeDistinguishedName ::=
|
||||
SET SIZE (1..MAX) OF AttributeTypeAndValue
|
||||
|
||||
AttributeTypeAndValue ::= SEQUENCE {
|
||||
type AttributeType,
|
||||
value AttributeValue }
|
||||
|
||||
AttributeType ::= OBJECT IDENTIFIER
|
||||
|
||||
AttributeValue ::= ANY -- DEFINED BY AttributeType
|
||||
|
||||
DirectoryString ::= CHOICE {
|
||||
teletexString TeletexString (SIZE (1..MAX)),
|
||||
printableString PrintableString (SIZE (1..MAX)),
|
||||
universalString UniversalString (SIZE (1..MAX)),
|
||||
utf8String UTF8String (SIZE (1..MAX)),
|
||||
bmpString BMPString (SIZE (1..MAX)) }
|
||||
```
|
||||
|
||||
### 3.2.4 GeneralNames / GeneralName(ASN.1;RFC 5280 §4.2.1.6)
|
||||
|
||||
> 说明:RPKI 的 AIA/SIA/CRLDP 等扩展通常把 URI 编码在 `uniformResourceIdentifier [6] IA5String` 分支中。RFC 5280 §4.2.1.6。
|
||||
|
||||
```asn1
|
||||
GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||
|
||||
GeneralName ::= CHOICE {
|
||||
otherName [0] OtherName,
|
||||
rfc822Name [1] IA5String,
|
||||
dNSName [2] IA5String,
|
||||
x400Address [3] ORAddress,
|
||||
directoryName [4] Name,
|
||||
ediPartyName [5] EDIPartyName,
|
||||
uniformResourceIdentifier [6] IA5String,
|
||||
iPAddress [7] OCTET STRING,
|
||||
registeredID [8] OBJECT IDENTIFIER }
|
||||
|
||||
OtherName ::= SEQUENCE {
|
||||
type-id OBJECT IDENTIFIER,
|
||||
value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||
|
||||
EDIPartyName ::= SEQUENCE {
|
||||
nameAssigner [0] DirectoryString OPTIONAL,
|
||||
partyName [1] DirectoryString }
|
||||
```
|
||||
|
||||
### 3.2.5 AIA(Authority Information Access;ASN.1;RFC 5280 §4.2.2.1)
|
||||
|
||||
```asn1
|
||||
id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 }
|
||||
|
||||
AuthorityInfoAccessSyntax ::=
|
||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 }
|
||||
```
|
||||
|
||||
### 3.2.6 SIA(Subject Information Access;ASN.1;RFC 5280 §4.2.2.2)
|
||||
|
||||
```asn1
|
||||
id-pe-subjectInfoAccess OBJECT IDENTIFIER ::= { id-pe 11 }
|
||||
|
||||
SubjectInfoAccessSyntax ::=
|
||||
SEQUENCE SIZE (1..MAX) OF AccessDescription
|
||||
|
||||
AccessDescription ::= SEQUENCE {
|
||||
accessMethod OBJECT IDENTIFIER,
|
||||
accessLocation GeneralName }
|
||||
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-caRepository OBJECT IDENTIFIER ::= { id-ad 5 }
|
||||
```
|
||||
|
||||
### 3.2.7 RPKI 在 SIA 中新增/使用的 accessMethod OID(RFC 6487 §4.8.8.1 / §4.8.8.2;RFC 8182 §3.2)
|
||||
|
||||
> 说明:下列 OID 用于 `AccessDescription.accessMethod`,并放在 SIA 的 `extnValue` 内层结构中(其外层 extnID 仍为 SIA:`id-pe-subjectInfoAccess`)。RFC 6487 §4.8.8;RFC 8182 §3.2。
|
||||
|
||||
```asn1
|
||||
id-ad OBJECT IDENTIFIER ::= { id-pkix 48 }
|
||||
|
||||
id-ad-rpkiManifest OBJECT IDENTIFIER ::= { id-ad 10 } -- 1.3.6.1.5.5.7.48.10
|
||||
|
||||
id-ad-signedObject OBJECT IDENTIFIER ::= { id-ad 11 } -- 1.3.6.1.5.5.7.48.11
|
||||
|
||||
id-ad-rpkiNotify OBJECT IDENTIFIER ::= { id-ad 13 } -- 1.3.6.1.5.5.7.48.13
|
||||
```
|
||||
|
||||
### 3.2.8 CRLDistributionPoints(CRLDP;ASN.1;RFC 5280 §4.2.1.13)
|
||||
|
||||
```asn1
|
||||
id-ce-cRLDistributionPoints OBJECT IDENTIFIER ::= { id-ce 31 }
|
||||
|
||||
CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
|
||||
|
||||
DistributionPoint ::= SEQUENCE {
|
||||
distributionPoint [0] DistributionPointName OPTIONAL,
|
||||
reasons [1] ReasonFlags OPTIONAL,
|
||||
cRLIssuer [2] GeneralNames OPTIONAL }
|
||||
|
||||
DistributionPointName ::= CHOICE {
|
||||
fullName [0] GeneralNames,
|
||||
nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
|
||||
|
||||
ReasonFlags ::= BIT STRING {
|
||||
unused (0),
|
||||
keyCompromise (1),
|
||||
cACompromise (2),
|
||||
affiliationChanged (3),
|
||||
superseded (4),
|
||||
cessationOfOperation (5),
|
||||
certificateHold (6),
|
||||
privilegeWithdrawn (7),
|
||||
aACompromise (8) }
|
||||
```
|
||||
|
||||
### 3.2.9 Certificate Policies(ASN.1;RFC 5280 §4.2.1.4)
|
||||
|
||||
```asn1
|
||||
id-ce-certificatePolicies OBJECT IDENTIFIER ::= { id-ce 32 }
|
||||
|
||||
anyPolicy OBJECT IDENTIFIER ::= { id-ce-certificatePolicies 0 }
|
||||
|
||||
certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
|
||||
|
||||
PolicyInformation ::= SEQUENCE {
|
||||
policyIdentifier CertPolicyId,
|
||||
policyQualifiers SEQUENCE SIZE (1..MAX) OF
|
||||
PolicyQualifierInfo OPTIONAL }
|
||||
|
||||
CertPolicyId ::= OBJECT IDENTIFIER
|
||||
|
||||
PolicyQualifierInfo ::= SEQUENCE {
|
||||
policyQualifierId PolicyQualifierId,
|
||||
qualifier ANY DEFINED BY policyQualifierId }
|
||||
|
||||
-- policyQualifierIds for Internet policy qualifiers
|
||||
|
||||
id-qt OBJECT IDENTIFIER ::= { id-pkix 2 }
|
||||
id-qt-cps OBJECT IDENTIFIER ::= { id-qt 1 }
|
||||
id-qt-unotice OBJECT IDENTIFIER ::= { id-qt 2 }
|
||||
|
||||
PolicyQualifierId ::= OBJECT IDENTIFIER ( id-qt-cps | id-qt-unotice )
|
||||
|
||||
Qualifier ::= CHOICE {
|
||||
cPSuri CPSuri,
|
||||
userNotice UserNotice }
|
||||
|
||||
CPSuri ::= IA5String
|
||||
```
|
||||
|
||||
### 3.2.10 RFC 3779 IP/AS 资源扩展(ASN.1;RFC 3779 §2.2.1-§2.2.3;RFC 3779 §3.2.1-§3.2.3)
|
||||
|
||||
> 说明:RFC 3779 给出两个扩展的 OID 与 ASN.1 语法;它们作为 X.509 v3 扩展出现在 `extensions` 中(外层 extnID 为下列 OID)。RPKI profile 进一步约束 criticality/SAFI/RDI 等,见 RFC 6487 §4.8.10-§4.8.11。
|
||||
|
||||
```asn1
|
||||
-- IP Address Delegation Extension
|
||||
id-pe-ipAddrBlocks OBJECT IDENTIFIER ::= { id-pe 7 }
|
||||
|
||||
IPAddrBlocks ::= SEQUENCE OF IPAddressFamily
|
||||
|
||||
IPAddressFamily ::= SEQUENCE { -- AFI & optional SAFI --
|
||||
addressFamily OCTET STRING (SIZE (2..3)),
|
||||
ipAddressChoice IPAddressChoice }
|
||||
|
||||
IPAddressChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
addressesOrRanges SEQUENCE OF IPAddressOrRange }
|
||||
|
||||
IPAddressOrRange ::= CHOICE {
|
||||
addressPrefix IPAddress,
|
||||
addressRange IPAddressRange }
|
||||
|
||||
IPAddressRange ::= SEQUENCE {
|
||||
min IPAddress,
|
||||
max IPAddress }
|
||||
|
||||
IPAddress ::= BIT STRING
|
||||
|
||||
-- Autonomous System Identifier Delegation Extension
|
||||
id-pe-autonomousSysIds OBJECT IDENTIFIER ::= { id-pe 8 }
|
||||
|
||||
ASIdentifiers ::= SEQUENCE {
|
||||
asnum [0] EXPLICIT ASIdentifierChoice OPTIONAL,
|
||||
rdi [1] EXPLICIT ASIdentifierChoice OPTIONAL}
|
||||
|
||||
ASIdentifierChoice ::= CHOICE {
|
||||
inherit NULL, -- inherit from issuer --
|
||||
asIdsOrRanges SEQUENCE OF ASIdOrRange }
|
||||
|
||||
ASIdOrRange ::= CHOICE {
|
||||
id ASId,
|
||||
range ASRange }
|
||||
|
||||
ASRange ::= SEQUENCE {
|
||||
min ASId,
|
||||
max ASId }
|
||||
|
||||
ASId ::= INTEGER
|
||||
```
|
||||
|
||||
### 3.2.11 其它 RPKI profile 相关扩展的 ASN.1 定义(RFC 5280 §4.2.1.1-§4.2.1.3;RFC 5280 §4.2.1.9;RFC 5280 §4.2.1.12)
|
||||
|
||||
> 说明:这些是 RPKI 资源证书 profile(RFC 6487 §4.8)所引用的通用 PKIX 扩展语法。RPKI 对其“必须/禁止/criticality/字段允许性”有额外限制(见本文件 3.3/3.4),但编码层的 ASN.1 类型来自 RFC 5280。
|
||||
|
||||
```asn1
|
||||
id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 }
|
||||
|
||||
AuthorityKeyIdentifier ::= SEQUENCE {
|
||||
keyIdentifier [0] KeyIdentifier OPTIONAL,
|
||||
authorityCertIssuer [1] GeneralNames OPTIONAL,
|
||||
authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL }
|
||||
|
||||
KeyIdentifier ::= OCTET STRING
|
||||
|
||||
id-ce-subjectKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 14 }
|
||||
|
||||
SubjectKeyIdentifier ::= KeyIdentifier
|
||||
|
||||
id-ce-keyUsage OBJECT IDENTIFIER ::= { id-ce 15 }
|
||||
|
||||
KeyUsage ::= BIT STRING {
|
||||
digitalSignature (0),
|
||||
nonRepudiation (1), -- recent editions of X.509 have
|
||||
-- renamed this bit to contentCommitment
|
||||
keyEncipherment (2),
|
||||
dataEncipherment (3),
|
||||
keyAgreement (4),
|
||||
keyCertSign (5),
|
||||
cRLSign (6),
|
||||
encipherOnly (7),
|
||||
decipherOnly (8) }
|
||||
|
||||
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
|
||||
|
||||
BasicConstraints ::= SEQUENCE {
|
||||
cA BOOLEAN DEFAULT FALSE,
|
||||
pathLenConstraint INTEGER (0..MAX) OPTIONAL }
|
||||
|
||||
id-ce-extKeyUsage OBJECT IDENTIFIER ::= { id-ce 37 }
|
||||
|
||||
ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
|
||||
|
||||
KeyPurposeId ::= OBJECT IDENTIFIER
|
||||
```
|
||||
|
||||
## 3.3 抽象数据模型(接口)
|
||||
|
||||
> 说明:本模型面向“语义化解析产物”。实现可保留 `raw_der` 作为可追溯入口。
|
||||
|
||||
### 3.3.1 顶层联合类型:`ResourceCertificate`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_der` | `DerBytes` | 证书 DER | 原样保留(建议) | RFC 6487 §4 |
|
||||
| `tbs` | `RpkixTbsCertificate` | 证书语义字段(见下) | 仅允许 RFC 6487 允许的字段/扩展;其他字段 MUST NOT 出现 | RFC 6487 §4 |
|
||||
| `kind` | `enum { ca, ee }` | 语义分类 | 来自 BasicConstraints + 用途约束 | RFC 6487 §4.8.1;RFC 6487 §4.8.8 |
|
||||
|
||||
### 3.3.1.1 派生类型(用于字段类型标注)
|
||||
|
||||
为避免在其它对象文档里反复写“`ResourceCertificate` 且 `kind==...`”,这里定义两个派生/别名类型:
|
||||
|
||||
- `ResourceCaCertificate`:`ResourceCertificate` 且 `kind == ca`
|
||||
- `ResourceEeCertificate`:`ResourceCertificate` 且 `kind == ee`
|
||||
|
||||
这些派生类型不引入新字段,只是对 `ResourceCertificate.kind` 的约束化视图。
|
||||
|
||||
### 3.3.2 `RpkixTbsCertificate`(语义字段集合)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | X.509 版本 | MUST 为 v3(字段值为 2) | RFC 6487 §4.1 |
|
||||
| `serial_number` | `int` | 序列号 | 正整数;对每 CA 签发唯一 | RFC 6487 §4.2 |
|
||||
| `signature_algorithm` | `Oid` | 证书签名算法 | 必须为 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6487 §4.3;RFC 7935 §2(引用 RFC 4055) |
|
||||
| `issuer_dn` | `RpkixDistinguishedName` | 颁发者 DN | 必含 1 个 CommonName;可含 1 个 serialNumber;CN 必须 PrintableString | RFC 6487 §4.4 |
|
||||
| `subject_dn` | `RpkixDistinguishedName` | 主体 DN | 同 issuer 约束;且对同一 issuer 下“实体+公钥”唯一 | RFC 6487 §4.5 |
|
||||
| `validity_not_before` | `UtcTime` | 有效期起 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.1;RFC 5280 §4.1.2.5 |
|
||||
| `validity_not_after` | `UtcTime` | 有效期止 | X.509 `Time`(UTCTime/GeneralizedTime)解析为 UTC 时间点 | RFC 6487 §4.6.2;RFC 5280 §4.1.2.5 |
|
||||
| `subject_public_key_info` | `DerBytes` | SPKI DER | 算法 profile 指定 | RFC 6487 §4.7;RFC 7935 §3.1 |
|
||||
| `extensions` | `RpkixExtensions` | 扩展集合 | 见下表;criticality/存在性/内容受约束 | RFC 6487 §4.8 |
|
||||
|
||||
### 3.3.3 `RpkixDistinguishedName`(RPKI profile 下的 DN 语义)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `common_name` | `string` | CommonName (CN) | MUST 存在且仅 1 个;类型为 PrintableString | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||
| `serial_number` | `optional[string]` | serialNumber | MAY 存在且仅 1 个 | RFC 6487 §4.4;RFC 6487 §4.5 |
|
||||
| `rfc4514` | `string` | DN 的 RFC4514 字符串表示 | 便于日志/索引(实现自选) | RFC 6487 §4.5(引用 RFC4514) |
|
||||
|
||||
### 3.3.4 `RpkixExtensions`(核心扩展语义)
|
||||
|
||||
> 表中 “存在性/criticality” 指 RPKI profile 下对该扩展的要求;实现应能区分 “字段缺失” 与 “字段存在但不符合约束”。
|
||||
|
||||
| 字段 | 类型 | 语义 | 存在性/criticality 与内容约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `basic_constraints` | `optional[BasicConstraints]` | CA 标志 | **extnID=`2.5.29.19`**;CA 证书:MUST present & critical;EE:MUST NOT present;pathLen MUST NOT present | RFC 6487 §4.8.1;RFC 5280 §4.2.1.9 |
|
||||
| `subject_key_identifier` | `bytes` | SKI | **extnID=`2.5.29.14`**;MUST present & non-critical;值为 subjectPublicKey 的 DER bit string 的 SHA-1 哈希 | RFC 6487 §4.8.2(引用 RFC 5280 §4.2.1.2) |
|
||||
| `authority_key_identifier` | `optional[AuthorityKeyIdentifier]` | AKI | **extnID=`2.5.29.35`**;自签名:MAY present 且可等于 SKI;非自签名:MUST present;authorityCertIssuer/authorityCertSerialNumber MUST NOT present;non-critical | RFC 6487 §4.8.3;RFC 5280 §4.2.1.1 |
|
||||
| `key_usage` | `KeyUsage` | KeyUsage | **extnID=`2.5.29.15`**;MUST present & critical;CA:仅 `keyCertSign` 与 `cRLSign` 为 TRUE;EE:仅 `digitalSignature` 为 TRUE | RFC 6487 §4.8.4;RFC 5280 §4.2.1.3 |
|
||||
| `extended_key_usage` | `optional[OidSet]` | EKU | **extnID=`2.5.29.37`**;CA:MUST NOT appear;用于验证 RPKI 对象的 EE:MUST NOT appear;若出现不得标 critical | RFC 6487 §4.8.5;RFC 5280 §4.2.1.12 |
|
||||
| `crl_distribution_points` | `optional[CrlDistributionPoints]` | CRLDP | **extnID=`2.5.29.31`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;仅 1 个 DistributionPoint;fullName URI;必须包含至少 1 个 `rsync://` | RFC 6487 §4.8.6;RFC 5280 §4.2.1.13 |
|
||||
| `authority_info_access` | `optional[AuthorityInfoAccess]` | AIA | **extnID=`1.3.6.1.5.5.7.1.1`**;自签名:MUST be omitted;非自签名:MUST present & non-critical;必须含 accessMethod `id-ad-caIssuers`(**`1.3.6.1.5.5.7.48.2`**) 的 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||
| `subject_info_access_ca` | `optional[SubjectInfoAccessCa]` | SIA(CA) | **extnID=`1.3.6.1.5.5.7.1.11`**;CA:MUST present & non-critical;必须含 accessMethod `id-ad-caRepository`(**`1.3.6.1.5.5.7.48.5`**)(`rsync://` 目录 URI)与 `id-ad-rpkiManifest`(**`1.3.6.1.5.5.7.48.10`**)(`rsync://` 对象 URI);若 CA 使用 RRDP,还会包含 `id-ad-rpkiNotify`(**`1.3.6.1.5.5.7.48.13`**)(HTTPS Notification URI) | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2;RFC 8182 §3.2 |
|
||||
| `subject_info_access_ee` | `optional[SubjectInfoAccessEe]` | SIA(EE) | **extnID=`1.3.6.1.5.5.7.1.11`**;EE:MUST present & non-critical;必须含 accessMethod `id-ad-signedObject`(**`1.3.6.1.5.5.7.48.11`**);URI **MUST include** `rsync://`;EE 的 SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||
| `certificate_policies` | `CertificatePolicies` | 证书策略 | **extnID=`2.5.29.32`**;MUST present & critical;恰好 1 个 policy;并允许 0 或 1 个 CPS qualifier(若存在其 id 必为 `id-qt-cps`(**`1.3.6.1.5.5.7.2.1`**)) | RFC 6487 §4.8.9;RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||
| `ip_resources` | `optional[IpResourceSet]` | IP 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.7`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;在公用互联网场景 SAFI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.10;RFC 3779 §2.2.1;RFC 3779 §2.2.2 |
|
||||
| `as_resources` | `optional[AsResourceSet]` | AS 资源扩展 | **extnID=`1.3.6.1.5.5.7.1.8`**;IP/AS 两者至少其一 MUST present;若 present MUST be critical;内容为 RFC 3779 语义;RDI MUST NOT 使用;且必须为非空或 inherit | RFC 6487 §4.8.11;RFC 3779 §3.2.1;RFC 3779 §3.2.2 |
|
||||
|
||||
### 3.3.5 结构化子类型(建议)
|
||||
|
||||
#### `BasicConstraints`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca` | `bool` | 是否 CA | 由 issuer 决定;在 CA 证书中该扩展必须存在 | RFC 6487 §4.8.1 |
|
||||
| `path_len_constraint` | `None` | pathLenConstraint | MUST NOT present(RPKI profile 不使用) | RFC 6487 §4.8.1 |
|
||||
|
||||
#### `AuthorityKeyIdentifier`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `key_identifier` | `bytes` | AKI.keyIdentifier | 使用 issuer 公钥的 SHA-1 哈希(按 RFC 5280 的定义) | RFC 6487 §4.8.3(引用 RFC 5280 §4.2.1.1) |
|
||||
| `authority_cert_issuer` | `None` | authorityCertIssuer | MUST NOT present | RFC 6487 §4.8.3 |
|
||||
| `authority_cert_serial_number` | `None` | authorityCertSerialNumber | MUST NOT present | RFC 6487 §4.8.3 |
|
||||
|
||||
#### `CrlDistributionPoints`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `distribution_point_uris` | `list[Uri]` | CRL 位置列表 | 仅 1 个 DistributionPoint;必须包含至少 1 个 `rsync://` URI 指向该 issuer 最新 CRL;可含其它 URI | RFC 6487 §4.8.6 |
|
||||
|
||||
#### `AuthorityInfoAccess`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca_issuers_uris` | `list[Uri]` | 上级 CA 证书位置 | accessMethod=`id-ad-caIssuers`(`1.3.6.1.5.5.7.48.2`);必含 `rsync://` URI;可含同对象其它 URI | RFC 6487 §4.8.7;RFC 5280 §4.2.2.1 |
|
||||
|
||||
#### `SubjectInfoAccessCa`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `ca_repository_uris` | `list[Uri]` | CA 发布点目录(repository publication point) | accessMethod=`id-ad-caRepository`(`1.3.6.1.5.5.7.48.5`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一目录”的替代访问方式;顺序表示 CA 偏好 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||
| `rpki_manifest_uris` | `list[Uri]` | 当前 manifest 对象 URI | accessMethod=`id-ad-rpkiManifest`(`1.3.6.1.5.5.7.48.10`);至少 1 个;必须包含 `rsync://`;也可包含其它机制(例如 `https://`)作为“同一对象”的替代访问方式 | RFC 6487 §4.8.8.1;RFC 5280 §4.2.2.2 |
|
||||
| `rpki_notify_uris` | `optional[list[Uri]]` | RRDP Notification(Update Notification File)URI | accessMethod=`id-ad-rpkiNotify`(`1.3.6.1.5.5.7.48.13`);若存在则 accessLocation MUST 为 `https://` URI,指向 RRDP Notification 文件 | RFC 8182 §3.2;RFC 5280 §4.2.2.2 |
|
||||
|
||||
#### `SubjectInfoAccessEe`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object_uris` | `list[Uri]` | 被 EE 证书验证的签名对象位置 | accessMethod=`id-ad-signedObject`(`1.3.6.1.5.5.7.48.11`);必须包含 `rsync://`;其它 URI 可作为同对象替代机制;EE SIA 不允许其它 AccessMethods | RFC 6487 §4.8.8.2;RFC 5280 §4.2.2.2 |
|
||||
|
||||
#### `CertificatePolicies`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `policy_oid` | `Oid` | 唯一 policy OID | 恰好 1 个 policy;RPKI CP 分配的 OID 为 `id-cp-ipAddr-asNumber`(`1.3.6.1.5.5.7.14.2`) | RFC 6487 §4.8.9;RFC 6484 §1.2 |
|
||||
| `cps_uri` | `optional[Uri]` | CPS policy qualifier URI | MAY 存在且最多 1 个;若存在其 `policyQualifierId` 必为 `id-qt-cps`;对该 URI 不施加处理要求 | RFC 7318 §2;RFC 5280 §4.2.1.4 |
|
||||
|
||||
## 3.4 字段级约束清单(实现对照)
|
||||
|
||||
- 仅允许 RFC 6487 §4 指定的字段/扩展;未列出字段 MUST NOT 出现。RFC 6487 §4。
|
||||
- 证书版本必须为 v3。RFC 6487 §4.1。
|
||||
- CA/EE 在 BasicConstraints 与 SIA 的约束不同。RFC 6487 §4.8.1;RFC 6487 §4.8.8.1;RFC 6487 §4.8.8.2。
|
||||
- KeyUsage:CA 仅 `keyCertSign`/`cRLSign`;EE 仅 `digitalSignature`。RFC 6487 §4.8.4。
|
||||
- CRLDP/AIA:自签名必须省略;非自签名必须存在并包含 `rsync://`。RFC 6487 §4.8.6;RFC 6487 §4.8.7。
|
||||
- IP/AS 资源扩展:两者至少其一存在;若存在必须 critical;语义来自 RFC 3779;在公用互联网场景 SAFI 与 RDI 均不得使用。RFC 6487 §4.8.10;RFC 6487 §4.8.11;RFC 3779 §2.2.3;RFC 3779 §3.2.3。
|
||||
158
specs/05_signed_object_cms.md
Normal file
158
specs/05_signed_object_cms.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# 05. RPKI Signed Object(CMS SignedData 外壳)
|
||||
|
||||
## 5.1 对象定位
|
||||
|
||||
ROA、Manifest 等都属于 “RPKI Signed Object”,其外壳是 CMS SignedData,并受 RFC 6488 的 profile 约束;RFC 9589 进一步更新了 `signedAttrs` 的要求。RFC 6488 §2-§4;RFC 9589 §4。
|
||||
|
||||
本文件描述**通用外壳模型**(eContentType/eContent 由具体对象文档给出)。
|
||||
|
||||
## 5.2 原始载体与编码
|
||||
|
||||
- 载体:CMS `ContentInfo`,其中 `contentType` 为 SignedData。RFC 6488 §2;RFC 6488 §3(1a)。
|
||||
- 编码:DER。RFC 6488 §2;RFC 6488 §3(1l)。
|
||||
|
||||
### 5.2.1 CMS 外壳:ContentInfo(ASN.1;RFC 5652 §3)
|
||||
|
||||
```asn1
|
||||
ContentInfo ::= SEQUENCE {
|
||||
contentType ContentType,
|
||||
content [0] EXPLICIT ANY DEFINED BY contentType }
|
||||
|
||||
ContentType ::= OBJECT IDENTIFIER
|
||||
```
|
||||
|
||||
### 5.2.2 CMS 外壳:SignedData(ASN.1;RFC 5652 §5.1)
|
||||
|
||||
```asn1
|
||||
id-signedData OBJECT IDENTIFIER ::= { iso(1) member-body(2)
|
||||
us(840) rsadsi(113549) pkcs(1) pkcs7(7) 2 }
|
||||
|
||||
SignedData ::= SEQUENCE {
|
||||
version CMSVersion,
|
||||
digestAlgorithms DigestAlgorithmIdentifiers,
|
||||
encapContentInfo EncapsulatedContentInfo,
|
||||
certificates [0] IMPLICIT CertificateSet OPTIONAL,
|
||||
crls [1] IMPLICIT RevocationInfoChoices OPTIONAL,
|
||||
signerInfos SignerInfos }
|
||||
|
||||
DigestAlgorithmIdentifiers ::= SET OF DigestAlgorithmIdentifier
|
||||
|
||||
SignerInfos ::= SET OF SignerInfo
|
||||
```
|
||||
|
||||
### 5.2.3 CMS 外壳:EncapsulatedContentInfo(ASN.1;RFC 5652 §5.2)
|
||||
|
||||
```asn1
|
||||
EncapsulatedContentInfo ::= SEQUENCE {
|
||||
eContentType ContentType,
|
||||
eContent [0] EXPLICIT OCTET STRING OPTIONAL }
|
||||
|
||||
ContentType ::= OBJECT IDENTIFIER
|
||||
```
|
||||
|
||||
> 注:CMS 允许 `eContent` 不一定 DER 编码(RFC 5652 §5.2);但 RPKI signed object profile 要求**整个对象 DER 编码**(RFC 6488 §2;RFC 6488 §3(1l)),且 eContent(payload)由对象规范定义并通常为 DER(如 ROA:RFC 9582 §4;Manifest:RFC 9286 §4.2)。
|
||||
|
||||
### 5.2.4 CMS 外壳:SignerInfo 与 Attribute(ASN.1;RFC 5652 §5.3)
|
||||
|
||||
```asn1
|
||||
SignerInfo ::= SEQUENCE {
|
||||
version CMSVersion,
|
||||
sid SignerIdentifier,
|
||||
digestAlgorithm DigestAlgorithmIdentifier,
|
||||
signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
|
||||
signatureAlgorithm SignatureAlgorithmIdentifier,
|
||||
signature SignatureValue,
|
||||
unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
|
||||
|
||||
SignerIdentifier ::= CHOICE {
|
||||
issuerAndSerialNumber IssuerAndSerialNumber,
|
||||
subjectKeyIdentifier [0] SubjectKeyIdentifier }
|
||||
|
||||
SignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||
|
||||
UnsignedAttributes ::= SET SIZE (1..MAX) OF Attribute
|
||||
|
||||
Attribute ::= SEQUENCE {
|
||||
attrType OBJECT IDENTIFIER,
|
||||
attrValues SET OF AttributeValue }
|
||||
|
||||
AttributeValue ::= ANY
|
||||
|
||||
SignatureValue ::= OCTET STRING
|
||||
```
|
||||
|
||||
### 5.2.5 RPKI 对 CMS 外壳字段的 profile 约束(RFC 6488 §2.1;RFC 6488 §3;更新:RFC 9589 §4)
|
||||
|
||||
> 说明:上面是 CMS 的通用 ASN.1;RPKI 进一步约束取值与允许出现的字段(例如 SignedData.version 必须为 3、crls 必须省略、signedAttrs 的内容限制等)。RFC 6488 §2-§3;RFC 9589 §4。
|
||||
|
||||
### 5.2.6 signedAttrs 中允许的属性与 attrType OID(RFC 6488 §2.1.6.4.1-§2.1.6.4.2;更新:RFC 9589 §4)
|
||||
|
||||
RPKI signed object profile 对 `SignerInfo.signedAttrs` 的 Attribute 集合施加限制(除 ASN.1 结构外,还包含“只允许哪些 attrType”的编码约束):
|
||||
|
||||
- `content-type`:attrType OID `1.2.840.113549.1.9.3`。RFC 6488 §2.1.6.4.1。
|
||||
- `message-digest`:attrType OID `1.2.840.113549.1.9.4`。RFC 6488 §2.1.6.4.2。
|
||||
- `signing-time`:attrType OID `1.2.840.113549.1.9.5`。RFC 9589 §4(更新 RFC 6488 的相关要求)。
|
||||
|
||||
并且:
|
||||
|
||||
- 每种属性在集合中只能出现一次;且 `attrValues` 虽然语法是 `SET OF`,但在 RPKI 中必须只含一个值。RFC 6488 §2.1.6.4。
|
||||
|
||||
## 5.3 抽象数据模型(接口)
|
||||
|
||||
### 5.3.1 `RpkiSignedObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_der` | `DerBytes` | CMS DER | 原样保留(建议) | RFC 6488 §2;RFC 6488 §3(1l) |
|
||||
| `content_info_content_type` | `Oid` | ContentInfo.contentType | MUST 为 SignedData:`1.2.840.113549.1.7.2` | RFC 6488 §3(1a) |
|
||||
| `signed_data` | `SignedDataProfiled` | SignedData 语义字段 | 见下 | RFC 6488 §2.1;RFC 6488 §3 |
|
||||
|
||||
### 5.3.2 `SignedDataProfiled`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | SignedData.version | MUST 为 3 | RFC 6488 §3(1b);RFC 6488 §2.1.1 |
|
||||
| `digest_algorithms` | `list[Oid]` | SignedData.digestAlgorithms | MUST contain exactly one digest algorithm,且必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 6488 §2.1.2;RFC 7935 §2(引用 RFC 5754) |
|
||||
| `encap_content_info` | `EncapsulatedContentInfo` | EncapsulatedContentInfo | 见下;eContentType 由具体对象定义 | RFC 6488 §2.1.3 |
|
||||
| `certificates` | `list[ResourceEeCertificate]` | SignedData.certificates | MUST present;且仅包含 1 个 EE 证书;该 EE 的 SKI 必须匹配 SignerInfo.sid | RFC 6488 §3(1c) |
|
||||
| `crls` | `None` | SignedData.crls | MUST be omitted | RFC 6488 §3(1d) |
|
||||
| `signer_infos` | `list[SignerInfoProfiled]` | SignedData.signerInfos | MUST contain exactly one SignerInfo | RFC 6488 §2.1;RFC 6488 §2.1(SignerInfos 约束段落) |
|
||||
|
||||
### 5.3.3 `EncapsulatedContentInfo`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `econtent_type` | `Oid` | eContentType | MUST 与 signedAttrs.content-type 的 attrValues 一致;具体值由对象定义(如 ROA/MFT) | RFC 6488 §3(1h);RFC 6488 §2.1.3.1 |
|
||||
| `econtent_der` | `DerBytes` | eContent(对象 payload) | DER 编码的对象特定 ASN.1(ROA/MFT 文档定义);在 CMS 中以 OCTET STRING 承载 | RFC 6488 §2.1.3;RFC 9286 §4.2;RFC 9582 §4 |
|
||||
|
||||
### 5.3.4 `SignerInfoProfiled`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | SignerInfo.version | MUST 为 3 | RFC 6488 §3(1e) |
|
||||
| `sid_ski` | `bytes` | sid(SubjectKeyIdentifier) | 必须与 EE 证书的 SKI 匹配 | RFC 6488 §3(1c) |
|
||||
| `digest_algorithm` | `Oid` | SignerInfo.digestAlgorithm | 必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 6488 §3(1j);RFC 7935 §2(引用 RFC 5754) |
|
||||
| `signature_algorithm` | `Oid` | SignerInfo.signatureAlgorithm | 生成时 MUST 为 `rsaEncryption`(`1.2.840.113549.1.1.1`);验证时实现必须接受 `rsaEncryption` 或 `sha256WithRSAEncryption`(`1.2.840.113549.1.1.11`) | RFC 6488 §3(1k);RFC 7935 §2 |
|
||||
| `signed_attrs` | `SignedAttrsProfiled` | signedAttrs | MUST present;仅允许特定 3 个属性 | RFC 9589 §4(更新 RFC 6488 §3(1f)/(1g)) |
|
||||
| `unsigned_attrs` | `None` | unsignedAttrs | MUST be omitted | RFC 6488 §3(1i) |
|
||||
|
||||
### 5.3.5 `SignedAttrsProfiled`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `content_type` | `Oid` | signedAttrs.content-type | attrType=`1.2.840.113549.1.9.3`;MUST present;attrValues 等于 eContentType | RFC 9589 §4;RFC 6488 §3(1h) |
|
||||
| `message_digest` | `bytes` | signedAttrs.message-digest | attrType=`1.2.840.113549.1.9.4`;MUST present | RFC 9589 §4(更新 RFC 6488 §3(1f)) |
|
||||
| `signing_time` | `UtcTime` | signedAttrs.signing-time | attrType=`1.2.840.113549.1.9.5`;MUST present(时间值正确性不用于安全假设) | RFC 9589 §4;RFC 9589 §5 |
|
||||
| `other_attrs` | `None` | 其它 signed attributes | MUST NOT be included(binary-signing-time 也不允许) | RFC 9589 §4 |
|
||||
|
||||
## 5.4 字段级约束清单(实现对照)
|
||||
|
||||
- ContentInfo.contentType 必须为 SignedData(OID `1.2.840.113549.1.7.2`)。RFC 6488 §3(1a)。
|
||||
- SignedData.version 必须为 3,且 SignerInfos 仅允许 1 个 SignerInfo。RFC 6488 §3(1b);RFC 6488 §2.1。
|
||||
- SignedData.certificates 必须存在且仅含 1 个 EE 证书;该证书 SKI 必须匹配 SignerInfo.sid。RFC 6488 §3(1c)。
|
||||
- SignedData.crls 必须省略。RFC 6488 §3(1d)。
|
||||
- signedAttrs 必须存在,且仅允许 content-type/message-digest/signing-time;其它全部禁止。RFC 9589 §4。
|
||||
- eContentType 必须与 content-type attribute 一致。RFC 6488 §3(1h)。
|
||||
- unsignedAttrs 必须省略。RFC 6488 §3(1i)。
|
||||
- digest/signature 算法必须符合算法 profile。RFC 6488 §3(1j)/(1k);RFC 7935 §2。
|
||||
- 整个对象必须 DER 编码。RFC 6488 §3(1l)。
|
||||
110
specs/06_manifest_mft.md
Normal file
110
specs/06_manifest_mft.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# 06. Manifest(MFT)
|
||||
|
||||
## 6.1 对象定位
|
||||
|
||||
Manifest 是 CA 发布点内对象的“清单”(文件名 + hash),用于 RP 侧检测删除/替换/回放等不一致情况。RFC 9286 §1;RFC 9286 §6。
|
||||
|
||||
Manifest 是一种 RPKI Signed Object:CMS 外壳遵循 RFC 6488/9589,eContent 遵循 RFC 9286。RFC 9286 §4;RFC 6488 §4;RFC 9589 §4。
|
||||
|
||||
## 6.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 9286 §4;RFC 6488 §2。
|
||||
- eContentType:`id-ct-rpkiManifest`,OID `1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1。
|
||||
- eContent:DER 编码 ASN.1 `Manifest`。RFC 9286 §4.2。
|
||||
|
||||
### 6.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9286 §4.1;RFC 9286 §4.2)
|
||||
|
||||
Manifest 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent` 的 ASN.1 由 RFC 9286 明确定义。RFC 9286 §4。
|
||||
|
||||
**eContentType(OID)**:RFC 9286 §4.1。
|
||||
|
||||
```asn1
|
||||
id-smime OBJECT IDENTIFIER ::= { iso(1) member-body(2) us(840)
|
||||
rsadsi(113549) pkcs(1) pkcs9(9) 16 }
|
||||
|
||||
id-ct OBJECT IDENTIFIER ::= { id-smime 1 }
|
||||
|
||||
id-ct-rpkiManifest OBJECT IDENTIFIER ::= { id-ct 26 }
|
||||
```
|
||||
|
||||
**eContent(Manifest 结构)**:RFC 9286 §4.2。
|
||||
|
||||
```asn1
|
||||
Manifest ::= SEQUENCE {
|
||||
version [0] INTEGER DEFAULT 0,
|
||||
manifestNumber INTEGER (0..MAX),
|
||||
thisUpdate GeneralizedTime,
|
||||
nextUpdate GeneralizedTime,
|
||||
fileHashAlg OBJECT IDENTIFIER,
|
||||
fileList SEQUENCE SIZE (0..MAX) OF FileAndHash
|
||||
}
|
||||
|
||||
FileAndHash ::= SEQUENCE {
|
||||
file IA5String,
|
||||
hash BIT STRING
|
||||
}
|
||||
```
|
||||
|
||||
解码要点:
|
||||
|
||||
- `fileHashAlg` 决定 `FileAndHash.hash` 的算法与输出长度(RPKI profile 要求 SHA-256)。RFC 9286 §4.2.1;RFC 7935 §2。
|
||||
- `hash` 在 ASN.1 中是 BIT STRING,但 hash 输出是按字节的比特串,DER 编码时应为 “unused bits = 0” 的 octet-aligned BIT STRING(实现可据此做一致性检查)。RFC 9286 §4.2。
|
||||
|
||||
## 6.3 解析规则(eContent 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 先按通用 Signed Object 外壳解析得到 `encap_content_info.econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.26`。RFC 9286 §4.1;RFC 9286 §4.4(1)。
|
||||
3) 将 `econtent_der` 以 DER 解析为 `Manifest` ASN.1。RFC 9286 §4.2。
|
||||
4) 将 `fileList` 映射为语义字段 `files: list[FileAndHash]`,其中 `hash` 为 `fileHashAlg` 对应算法的输出字节序列。RFC 9286 §4.2.1(fileHashAlg/fileList 定义)。
|
||||
|
||||
## 6.4 抽象数据模型(接口)
|
||||
|
||||
### 6.4.1 `ManifestObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9286 §4;RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.26` | RFC 9286 §4.1 |
|
||||
| `manifest` | `ManifestEContent` | eContent 语义对象 | 见下 | RFC 9286 §4.2 |
|
||||
|
||||
### 6.4.2 `ManifestEContent`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | Manifest.version | MUST 为 0 | RFC 9286 §4.2.1(version) |
|
||||
| `manifest_number` | `int` | manifestNumber | 0..MAX;可达 20 octets;issuer 必须单调递增;RP 必须可处理至 20 octets | RFC 9286 §4.2;RFC 9286 §4.2.1(manifestNumber) |
|
||||
| `this_update` | `UtcTime` | thisUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须比先前生成的 manifest 更新 | RFC 9286 §4.2;RFC 9286 §4.2.1(thisUpdate) |
|
||||
| `next_update` | `UtcTime` | nextUpdate | 由 ASN.1 `GeneralizedTime` 解析为 UTC 时间点;且必须晚于 thisUpdate | RFC 9286 §4.2;RFC 9286 §4.2.1(nextUpdate) |
|
||||
| `file_hash_alg` | `Oid` | fileHashAlg | 必须为 `id-sha256`(`2.16.840.1.101.3.4.2.1`) | RFC 9286 §4.2.1(fileHashAlg);RFC 7935 §2(引用 RFC 5754) |
|
||||
| `files` | `list[FileAndHash]` | fileList | `SEQUENCE SIZE (0..MAX)`;每项含文件名与 hash | RFC 9286 §4.2;RFC 9286 §4.2.1(fileList) |
|
||||
|
||||
### 6.4.3 `FileAndHash`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `file_name` | `string` | 文件名(不含路径) | 字符集限制:`[a-zA-Z0-9-_]+` + `.` + 三字母扩展;扩展必须在 IANA “RPKI Repository Name Schemes” 注册表中 | RFC 9286 §4.2.2 |
|
||||
| `hash_bytes` | `bytes` | 文件内容 hash | 由 `file_hash_alg` 指定算法计算 | RFC 9286 §4.2.1(fileHashAlg/fileList) |
|
||||
|
||||
## 6.5 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-rpkiManifest`(OID `1.2.840.113549.1.9.16.1.26`)。RFC 9286 §4.1。
|
||||
- eContent 必须 DER 编码且符合 `Manifest` ASN.1。RFC 9286 §4.2。
|
||||
- `version` 必须为 0。RFC 9286 §4.2.1。
|
||||
- `manifestNumber` 由 issuer 单调递增;RP 必须能处理至 20 octets;issuer 不得超过 20 octets。RFC 9286 §4.2.1。
|
||||
- `nextUpdate` 必须晚于 `thisUpdate`。RFC 9286 §4.2.1。
|
||||
- `fileHashAlg` 必须符合算法 profile(SHA-256)。RFC 9286 §4.2.1;RFC 7935 §2。
|
||||
- `fileList` 中 `file` 名称字符集与扩展名受限;实现需按 RFC 限制解析并保留大小写语义。RFC 9286 §4.2.2。
|
||||
|
||||
## 6.6 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
Manifest 使用“one-time-use EE certificate”进行签名验证,规范对该 EE 证书的使用方式给出约束:
|
||||
|
||||
- Manifest 相关 EE 证书应为 one-time-use(每次新 manifest 生成新密钥对/新 EE)。RFC 9286 §4(Section 4 前导段落)。
|
||||
- 用于签名/验证 manifest 的 EE 证书在 RFC 3779 的资源扩展中描述 INRs 时,**MUST** 使用 `inherit`(而不是显式列出资源集合)。RFC 9286 §5.1(生成步骤 2)。
|
||||
- 若证书包含 **IP Address Delegation Extension**(IP prefix delegation):内容必须为 `inherit`(不得显式列 prefix/range)。RFC 9286 §5.1;RFC 3779。
|
||||
- 若证书包含 **AS Identifier Delegation Extension**(ASN delegation):内容必须为 `inherit`(不得显式列 ASID/range)。RFC 9286 §5.1;RFC 3779。
|
||||
- 另外按资源证书 profile:资源证书 **MUST** 至少包含上述两类扩展之一(也可两者都有),且这些扩展 **MUST** 标记为 critical。RFC 6487 §2。
|
||||
- 用于验证 manifest 的 EE 证书 **MUST** 具有与 `thisUpdate..nextUpdate` 区间一致的有效期,以避免 CRL 无谓增长。RFC 9286 §4.2.1(manifestNumber 段落前的说明)。
|
||||
- 替换 manifest 时,CA 必须撤销旧 manifest 对应 EE 证书;且若新 manifest 早于旧 manifest 的 nextUpdate 发行,则 CA **MUST** 同时发行新 CRL 撤销旧 manifest EE。RFC 9286 §4.2.1(nextUpdate 段落末);RFC 9286 §5.1(生成步骤)。
|
||||
159
specs/07_roa.md
Normal file
159
specs/07_roa.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# 07. ROA(Route Origin Authorization)
|
||||
|
||||
## 7.1 对象定位
|
||||
|
||||
ROA 是一种 RPKI Signed Object,用于声明“某 AS 被授权起源某些前缀”。RFC 9582 §1;RFC 9582 §4。
|
||||
|
||||
ROA 由 CMS 外壳 + ROA eContent 组成:
|
||||
|
||||
- 外壳:RFC 6488(更新:RFC 9589)
|
||||
- eContent:RFC 9582
|
||||
|
||||
## 7.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 9582 §1(引用 RFC 6488)。
|
||||
- eContentType:`id-ct-routeOriginAuthz`,OID `1.2.840.113549.1.9.16.1.24`。RFC 9582 §3。
|
||||
- eContent:DER 编码 ASN.1 `RouteOriginAttestation`。RFC 9582 §4。
|
||||
|
||||
### 7.2.1 eContentType 与 eContent 的 ASN.1 定义(RFC 9582 §3;RFC 9582 §4)
|
||||
|
||||
ROA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。RFC 9582 定义了其 `eContentType` 以及 `eContent`(payload)的 ASN.1。RFC 9582 §3-§4。
|
||||
|
||||
**eContentType(OID)**:RFC 9582 §3。
|
||||
|
||||
```asn1
|
||||
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
|
||||
```
|
||||
|
||||
**eContent(ROA ASN.1 模块)**:RFC 9582 §4。
|
||||
|
||||
```asn1
|
||||
RPKI-ROA-2023
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549)
|
||||
pkcs(1) pkcs9(9) smime(16) mod(0)
|
||||
id-mod-rpkiROA-2023(75) }
|
||||
|
||||
DEFINITIONS EXPLICIT TAGS ::=
|
||||
BEGIN
|
||||
|
||||
IMPORTS
|
||||
CONTENT-TYPE
|
||||
FROM CryptographicMessageSyntax-2010 -- in [RFC6268]
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||
pkcs-9(9) smime(16) modules(0) id-mod-cms-2009(58) } ;
|
||||
|
||||
ct-routeOriginAttestation CONTENT-TYPE ::=
|
||||
{ TYPE RouteOriginAttestation
|
||||
IDENTIFIED BY id-ct-routeOriginAuthz }
|
||||
|
||||
id-ct-routeOriginAuthz OBJECT IDENTIFIER ::=
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||
pkcs-9(9) id-smime(16) id-ct(1) routeOriginAuthz(24) }
|
||||
|
||||
RouteOriginAttestation ::= SEQUENCE {
|
||||
version [0] INTEGER DEFAULT 0,
|
||||
asID ASID,
|
||||
ipAddrBlocks SEQUENCE (SIZE(1..2)) OF ROAIPAddressFamily }
|
||||
|
||||
ASID ::= INTEGER (0..4294967295)
|
||||
|
||||
ROAIPAddressFamily ::= SEQUENCE {
|
||||
addressFamily ADDRESS-FAMILY.&afi ({AddressFamilySet}),
|
||||
addresses ADDRESS-FAMILY.&Addresses
|
||||
({AddressFamilySet}{@addressFamily}) }
|
||||
|
||||
ADDRESS-FAMILY ::= CLASS {
|
||||
&afi OCTET STRING (SIZE(2)) UNIQUE,
|
||||
&Addresses
|
||||
} WITH SYNTAX { AFI &afi ADDRESSES &Addresses }
|
||||
|
||||
AddressFamilySet ADDRESS-FAMILY ::=
|
||||
{ addressFamilyIPv4 | addressFamilyIPv6 }
|
||||
|
||||
addressFamilyIPv4 ADDRESS-FAMILY ::=
|
||||
{ AFI afi-IPv4 ADDRESSES ROAAddressesIPv4 }
|
||||
addressFamilyIPv6 ADDRESS-FAMILY ::=
|
||||
{ AFI afi-IPv6 ADDRESSES ROAAddressesIPv6 }
|
||||
|
||||
afi-IPv4 OCTET STRING ::= '0001'H
|
||||
afi-IPv6 OCTET STRING ::= '0002'H
|
||||
|
||||
ROAAddressesIPv4 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv4}
|
||||
ROAAddressesIPv6 ::= SEQUENCE (SIZE(1..MAX)) OF ROAIPAddress{ub-IPv6}
|
||||
|
||||
ub-IPv4 INTEGER ::= 32
|
||||
ub-IPv6 INTEGER ::= 128
|
||||
|
||||
ROAIPAddress {INTEGER: ub} ::= SEQUENCE {
|
||||
address BIT STRING (SIZE(0..ub)),
|
||||
maxLength INTEGER (0..ub) OPTIONAL }
|
||||
|
||||
END
|
||||
```
|
||||
|
||||
编码/解码要点(与上面 ASN.1 结构直接对应):
|
||||
|
||||
- `addressFamily` 仅允许 IPv4/IPv6 两种 AFI,并且每个 AFI 最多出现一次。RFC 9582 §4.3.1。
|
||||
- `address` 是 BIT STRING 表示的前缀,语义与 RFC 3779 的 `IPAddress` 一致(按前缀长度截断,DER unused bits 置零)。RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8)。
|
||||
- `maxLength` 为可选字段,出现与否会影响语义与编码规范约束(例如等于前缀长时不建议编码)。RFC 9582 §4.3.2.2。
|
||||
|
||||
## 7.3 解析规则(eContent 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.24`。RFC 9582 §3。
|
||||
3) 将 `econtent_der` 以 DER 解析为 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
|
||||
4) 将 `ipAddrBlocks` 解析为“前缀集合”的语义结构,并建议按 RFC 9582 给出的 canonicalization 过程做去重/排序/归一化(以便后续处理一致)。RFC 9582 §4.3.3。
|
||||
|
||||
## 7.4 抽象数据模型(接口)
|
||||
|
||||
### 7.4.1 `RoaObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 9582 §1;RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.24` | RFC 9582 §3 |
|
||||
| `roa` | `RoaEContent` | eContent 语义对象 | 见下 | RFC 9582 §4 |
|
||||
|
||||
### 7.4.2 `RoaEContent`(RouteOriginAttestation)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | version | MUST 为 0 | RFC 9582 §4.1 |
|
||||
| `as_id` | `int` | asID | 0..4294967295 | RFC 9582 §4(ASID 定义);RFC 9582 §4.2 |
|
||||
| `ip_addr_blocks` | `list[RoaIpAddressFamily]` | ipAddrBlocks | `SIZE(1..2)`;最多 IPv4/IPv6 各一个;建议 canonicalize | RFC 9582 §4;RFC 9582 §4.3.1;RFC 9582 §4.3.3 |
|
||||
|
||||
### 7.4.3 `RoaIpAddressFamily`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `afi` | `enum { ipv4, ipv6 }` | Address Family | MUST 为 IPv4(0001) 或 IPv6(0002) | RFC 9582 §4.3.1 |
|
||||
| `addresses` | `list[RoaIpAddress]` | 前缀列表 | `SIZE(1..MAX)`;每项为前缀 + 可选 maxLength | RFC 9582 §4(ROAAddressesIPv4/IPv6);RFC 9582 §4.3.2 |
|
||||
|
||||
### 7.4.4 `RoaIpAddress`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `prefix` | `IpPrefix` | 前缀 | address 以 BIT STRING 表示前缀(同 RFC 3779 IPAddress 表示) | RFC 9582 §4.3.2.1(引用 RFC 3779 §2.2.3.8) |
|
||||
| `max_length` | `optional[int]` | 最大允许前缀长 | 若存在:必须 `>= prefix_len` 且 `<= 32/128`;并且 `maxLength == prefix_len` 时 **SHOULD NOT** 编码(未来 RP 可能视为编码错误) | RFC 9582 §4.3.2.2 |
|
||||
|
||||
## 7.5 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-routeOriginAuthz`(OID `1.2.840.113549.1.9.16.1.24`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 9582 §3(引用 RFC 6488)。
|
||||
- eContent 必须 DER 编码并符合 `RouteOriginAttestation` ASN.1。RFC 9582 §4。
|
||||
- `version` 必须为 0。RFC 9582 §4.1。
|
||||
- `ipAddrBlocks` 长度为 1..2;每种 AFI 最多出现一次;仅支持 IPv4/IPv6。RFC 9582 §4;RFC 9582 §4.3.1。
|
||||
- `maxLength` 若存在必须在范围内,且不应出现“等于前缀长”的冗余编码。RFC 9582 §4.3.2.2。
|
||||
- 建议按 canonical form 归一化/排序以利一致处理。RFC 9582 §4.3.3。
|
||||
|
||||
## 7.6 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
ROA 的外壳包含一个 EE 证书,用于验证 ROA 签名;RFC 对该 EE 证书与 ROA payload 的匹配关系提出要求:
|
||||
|
||||
- ROA 的 EE 证书必须是有效的 RPKI EE 证书(路径从 TA 到 EE 可建立),并用于验证 CMS 签名。RFC 9582 §1(引用 RFC 6488);RFC 6488 §3(2)-(3)。
|
||||
- ROA EE 证书中的 IP 资源扩展必须存在且不得使用 inherit。RFC 9582 §5。
|
||||
- ROA EE 证书中 AS 资源扩展不得出现。RFC 9582 §5。
|
||||
- ROA payload 中每个前缀必须包含在 EE 证书的 IP 资源集合内(资源包含语义来自 RFC 3779)。RFC 9582 §5;RFC 3779 §2.3。
|
||||
100
specs/08_aspa.md
Normal file
100
specs/08_aspa.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 08. ASPA(Autonomous System Provider Authorization)
|
||||
|
||||
## 8.1 对象定位
|
||||
|
||||
ASPA(Autonomous System Provider Authorization)是一种 RPKI Signed Object,用于由“客户 AS”(Customer AS, CAS)签名声明其上游“提供者 AS”(Provider AS, PAS)集合,以支持路由泄漏(route leak)检测/缓解。`draft-ietf-sidrops-aspa-profile-21`;`draft-ietf-sidrops-aspa-verification`。
|
||||
|
||||
ASPA 由 CMS 外壳 + ASPA eContent 组成:
|
||||
|
||||
- 外壳:RFC 6488(更新:RFC 9589)
|
||||
- eContent:`draft-ietf-sidrops-aspa-profile-21`
|
||||
|
||||
## 8.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6488 §2-§3;RFC 9589 §4。
|
||||
- eContentType:`id-ct-ASPA`,OID `1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
- eContent:DER 编码 ASN.1 `ASProviderAttestation`。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
|
||||
### 8.2.1 eContentType 与 eContent 的 ASN.1 定义(`draft-ietf-sidrops-aspa-profile-21` §2-§3)
|
||||
|
||||
ASPA 是一种 RPKI signed object(CMS 外壳见 `05_signed_object_cms.md`)。其 `eContentType` 与 `eContent`(payload)的 ASN.1 定义见 `draft-ietf-sidrops-aspa-profile-21` §2-§3。
|
||||
|
||||
**eContentType(OID)**:`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
|
||||
```asn1
|
||||
id-ct-ASPA OBJECT IDENTIFIER ::=
|
||||
{ iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
|
||||
pkcs-9(9) id-smime(16) id-ct(1) aspa(49) }
|
||||
```
|
||||
|
||||
**eContent(ASPA ASN.1 模块)**:`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
|
||||
```asn1
|
||||
ASProviderAttestation ::= SEQUENCE {
|
||||
version [0] INTEGER DEFAULT 0,
|
||||
customerASID ASID,
|
||||
providers ProviderASSet }
|
||||
|
||||
ProviderASSet ::= SEQUENCE (SIZE(1..MAX)) OF ASID
|
||||
|
||||
ASID ::= INTEGER (0..4294967295)
|
||||
```
|
||||
|
||||
编码/解码要点(与上面 ASN.1 结构直接对应):
|
||||
|
||||
- `version`:规范要求 **MUST 为 1** 且 **MUST 显式编码**(不得依赖 DEFAULT 省略)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||
- `customerASID`:客户 AS 号(CAS)。`draft-ietf-sidrops-aspa-profile-21` §3.2。
|
||||
- `providers`:授权的提供者 AS 集合(SPAS)。并对“自包含/排序/去重”施加额外约束(见下)。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.3 解析规则(eContent 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_der`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.49`。`draft-ietf-sidrops-aspa-profile-21` §2。
|
||||
3) 将 `econtent_der` 以 DER 解析为 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
4) 将 `providers` 映射为语义字段 `provider_as_ids: list[int]`,并对其执行“约束检查/(可选)归一化”。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.4 抽象数据模型(接口)
|
||||
|
||||
### 8.4.1 `AspaObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.49` | `draft-ietf-sidrops-aspa-profile-21` §2 |
|
||||
| `aspa` | `AspaEContent` | eContent 语义对象 | 见下 | `draft-ietf-sidrops-aspa-profile-21` §3 |
|
||||
|
||||
### 8.4.2 `AspaEContent`(ASProviderAttestation)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | 规范引用 |
|
||||
|---|---|---|---|---|
|
||||
| `version` | `int` | ASPA 版本 | MUST 为 `1` 且 MUST 显式编码(字段不可省略) | `draft-ietf-sidrops-aspa-profile-21` §3.1 |
|
||||
| `customer_as_id` | `int` | Customer ASID | 0..4294967295 | `draft-ietf-sidrops-aspa-profile-21` §3.2(ASID 定义) |
|
||||
| `provider_as_ids` | `list[int]` | Provider ASID 列表(SPAS) | 长度 `>= 1`;且满足“不得包含 customer、升序、去重” | `draft-ietf-sidrops-aspa-profile-21` §3.3 |
|
||||
|
||||
## 8.5 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-ASPA`(OID `1.2.840.113549.1.9.16.1.49`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。`draft-ietf-sidrops-aspa-profile-21` §2(引用 RFC 6488)。
|
||||
- eContent 必须 DER 编码并符合 `ASProviderAttestation` ASN.1。`draft-ietf-sidrops-aspa-profile-21` §3。
|
||||
- `version` 必须为 1,且必须显式编码(缺失视为不合规)。`draft-ietf-sidrops-aspa-profile-21` §3.1。
|
||||
- `providers`(`provider_as_ids`)必须满足:
|
||||
- `customer_as_id` **MUST NOT** 出现在 `provider_as_ids` 中;
|
||||
- `provider_as_ids` 必须按数值 **升序排序**;
|
||||
- `provider_as_ids` 中每个 ASID 必须 **唯一**。`draft-ietf-sidrops-aspa-profile-21` §3.3。
|
||||
|
||||
## 8.6 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
ASPA 的外壳包含一个 EE 证书,用于验证 ASPA 签名;规范对该 EE 证书与 ASPA payload 的匹配关系提出要求:
|
||||
|
||||
- EE 证书必须包含 AS 资源扩展(Autonomous System Identifier Delegation Extension),且 `customer_as_id` 必须与该扩展中的 ASId 匹配。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||
- EE 证书的 AS 资源扩展 **必须**:
|
||||
- 恰好包含 1 个 `id` 元素;
|
||||
- **不得**包含 `inherit` 元素;
|
||||
- **不得**包含 `range` 元素。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779 §3.2.3.3 / §3.2.3.6 / §3.2.3.7)。
|
||||
- EE 证书 **不得**包含 IP 资源扩展(IP Address Delegation Extension)。`draft-ietf-sidrops-aspa-profile-21` §4(引用 RFC 3779)。
|
||||
|
||||
## 8.7 实现建议(非规范约束)
|
||||
|
||||
`draft-ietf-sidrops-aspa-profile-21` 给出了一条 RP 侧建议:实现可对单个 `customer_as_id` 的 `provider_as_ids` 数量施加上界(例如 4,000~10,000),超过阈值时建议将该 `customer_as_id` 的所有 ASPA 视为无效并记录错误日志。`draft-ietf-sidrops-aspa-profile-21` §6。
|
||||
|
||||
87
specs/09_ghostbusters_gbr.md
Normal file
87
specs/09_ghostbusters_gbr.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 09. Ghostbusters Record(GBR)
|
||||
|
||||
## 9.1 对象定位
|
||||
|
||||
Ghostbusters Record(GBR)是一个可选的 RPKI Signed Object,用于承载“联系人信息”(人类可读的联系渠道),以便在证书过期、CRL 失效、密钥轮换等事件中能够联系到维护者。RFC 6493 §1。
|
||||
|
||||
GBR 由 CMS 外壳 + vCard 载荷组成:
|
||||
|
||||
- 外壳:RFC 6488(更新:RFC 9589)
|
||||
- 载荷(payload):RFC 6493 定义的 vCard profile(基于 RFC 6350 vCard 4.0 的严格子集)。RFC 6493 §5。
|
||||
|
||||
## 9.2 原始载体与编码
|
||||
|
||||
- 外壳:CMS SignedData DER(见 `05_signed_object_cms.md`)。RFC 6493 §6(引用 RFC 6488)。
|
||||
- eContentType:`id-ct-rpkiGhostbusters`,OID `1.2.840.113549.1.9.16.1.35`。RFC 6493 §6;RFC 6493 §9.1。
|
||||
- eContent:一个 OCTET STRING,其 octets 是 vCard 文本(vCard 4.0,且受 RFC 6493 的 profile 约束)。RFC 6493 §5;RFC 6493 §6。
|
||||
|
||||
> 说明:与 ROA/MFT 这类“eContent 内部再 DER 解码为 ASN.1 结构”的对象不同,GBR 的 eContent 语义上就是“vCard 文本内容本身”(由 Signed Object Template 的 `eContent OCTET STRING` 承载)。RFC 6493 §6。
|
||||
|
||||
## 9.3 vCard profile(RFC 6493 §5)
|
||||
|
||||
GBR 的 vCard payload 是 RFC 6350 vCard 4.0 的严格子集,仅允许以下属性(properties):
|
||||
|
||||
- `BEGIN`:必须为第一行,值必须为 `BEGIN:VCARD`。RFC 6493 §5。
|
||||
- `VERSION`:必须为第二行,值必须为 `VERSION:4.0`。RFC 6493 §5(引用 RFC 6350 §3.7.9)。
|
||||
- `FN`:联系人姓名或角色名。RFC 6493 §5(引用 RFC 6350 §6.2.1)。
|
||||
- `ORG`:组织信息(可选)。RFC 6493 §5(引用 RFC 6350 §6.6.4)。
|
||||
- `ADR`:邮寄地址(可选)。RFC 6493 §5(引用 RFC 6350 §6.3)。
|
||||
- `TEL`:语音/传真电话(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.1)。
|
||||
- `EMAIL`:邮箱(可选)。RFC 6493 §5(引用 RFC 6350 §6.4.2)。
|
||||
- `END`:必须为最后一行,值必须为 `END:VCARD`。RFC 6493 §5。
|
||||
|
||||
额外约束:
|
||||
|
||||
- `BEGIN`、`VERSION`、`FN`、`END` 必须包含。RFC 6493 §5。
|
||||
- 为保证可用性,`ADR`/`TEL`/`EMAIL` 三者中至少一个必须包含。RFC 6493 §5。
|
||||
- 除上述属性外,**其他属性 MUST NOT** 出现。RFC 6493 §5。
|
||||
|
||||
## 9.4 解析规则(payload 语义层)
|
||||
|
||||
输入:`RpkiSignedObject`。
|
||||
|
||||
1) 解析 CMS 外壳,得到 `econtent_type` 与 `econtent_bytes`。RFC 6488 §3;RFC 9589 §4。
|
||||
2) 要求 `econtent_type == 1.2.840.113549.1.9.16.1.35`。RFC 6493 §6。
|
||||
3) 将 `econtent_bytes` 解析为 vCard 文本,并按 RFC 6493 §5 的 profile 校验(属性集合、必选项、行首/行尾约束)。RFC 6493 §5;RFC 6493 §7。
|
||||
4) 通过校验后,将允许属性映射为 `GhostbustersVCard` 语义对象(见下)。
|
||||
|
||||
## 9.5 抽象数据模型(接口)
|
||||
|
||||
### 9.5.1 `GhostbustersObject`
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `signed_object` | `RpkiSignedObject` | CMS 外壳 | 外壳约束见 RFC 6488/9589 | RFC 6493 §6;RFC 6488 §3;RFC 9589 §4 |
|
||||
| `econtent_type` | `Oid` | eContentType | 必须为 `1.2.840.113549.1.9.16.1.35` | RFC 6493 §6 |
|
||||
| `vcard` | `GhostbustersVCard` | vCard 语义对象 | 由 eContent 文本解析并校验 profile | RFC 6493 §5;RFC 6493 §7 |
|
||||
|
||||
### 9.5.2 `GhostbustersVCard`(vCard 4.0 profile)
|
||||
|
||||
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||
|---|---|---|---|---|
|
||||
| `raw_text` | `string` | 原始 vCard 文本 | 由 eContent bytes 解码得到;用于保留原文/诊断 | RFC 6493 §5-§7 |
|
||||
| `fn` | `string` | 联系人姓名/角色名 | `FN` 必须存在 | RFC 6493 §5 |
|
||||
| `org` | `optional[string]` | 组织 | `ORG` 可选 | RFC 6493 §5 |
|
||||
| `adrs` | `list[string]` | 邮寄地址(原始 ADR value) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
| `tels` | `list[Uri]` | 电话 URI(从 TEL 提取的 `tel:` 等 URI) | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
| `emails` | `list[string]` | 邮箱地址 | 允许 0..N;至少满足“ADR/TEL/EMAIL 至少一项存在” | RFC 6493 §5 |
|
||||
|
||||
> 说明:RFC 6493 并未要求 RP 必须完整解析 vCard 参数(例如 `TYPE=WORK`、`VALUE=uri`),因此此处将 `ADR`/`TEL`/`EMAIL` 建模为“足以联络”的最小语义集合;实现可在保留 `raw_text` 的同时按 RFC 6350 扩展解析能力。
|
||||
|
||||
## 9.6 字段级约束清单(实现对照)
|
||||
|
||||
- eContentType 必须为 `id-ct-rpkiGhostbusters`(OID `1.2.840.113549.1.9.16.1.35`),且该 OID 必须同时出现在 eContentType 与 signedAttrs.content-type。RFC 6493 §6(引用 RFC 6488)。
|
||||
- eContent 必须是 vCard 4.0 文本,且必须满足 RFC 6493 §5 的 profile:
|
||||
- 第一行 `BEGIN:VCARD`;
|
||||
- 第二行 `VERSION:4.0`;
|
||||
- 末行 `END:VCARD`;
|
||||
- 必须包含 `FN`;
|
||||
- `ADR`/`TEL`/`EMAIL` 至少一个存在;
|
||||
- 除允许集合外不得出现其他属性。RFC 6493 §5;RFC 6493 §7。
|
||||
|
||||
## 9.7 与 EE 证书的语义约束(为后续验证准备)
|
||||
|
||||
GBR 使用 CMS 外壳内的 EE 证书验证签名。RFC 6493 对该 EE 证书提出一条资源扩展约束:
|
||||
|
||||
- 用于验证 GBR 的 EE 证书在描述 Internet Number Resources 时,必须使用 `inherit`,而不是显式资源集合。RFC 6493 §6(引用 RFC 3779)。
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
# 10. SLURM(Simplified Local Internet Number Resource Management with the RPKI)
|
||||
|
||||
## 10.1 目标与范围
|
||||
|
||||
SLURM 用于让 RP(Relying Party)在本地对上游 RPKI 验证结果做“过滤”和“补充断言”,而不修改上游发布对象。
|
||||
|
||||
本文档基于:
|
||||
- RFC 8416(SLURM v1,ROA/BGPsec)
|
||||
- draft-ietf-sidrops-aspa-slurm-04(SLURM v2,新增 ASPA)
|
||||
|
||||
## 10.2 版本与顶层结构
|
||||
|
||||
### 10.2.1 SLURM v1(RFC 8416)
|
||||
|
||||
`slurmVersion` 必须为 `1`,且顶层 JSON 对象必须包含且仅包含以下成员:
|
||||
- `slurmVersion`
|
||||
- `validationOutputFilters`(必须包含 `prefixFilters`、`bgpsecFilters`)
|
||||
- `locallyAddedAssertions`(必须包含 `prefixAssertions`、`bgpsecAssertions`)
|
||||
|
||||
空策略示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"slurmVersion": 1,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": []
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2.2 SLURM v2(draft-04)
|
||||
|
||||
`slurmVersion` 必须为 `2`,在 v1 基础上扩展 ASPA 两类成员:
|
||||
- `validationOutputFilters.aspaFilters`
|
||||
- `locallyAddedAssertions.aspaAssertions`
|
||||
|
||||
空策略示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"slurmVersion": 2,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": [],
|
||||
"aspaFilters": []
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": [],
|
||||
"aspaAssertions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10.3 字段规范(RFC 8416)
|
||||
|
||||
### 10.3.1 `prefixFilters`
|
||||
|
||||
数组元素每项:
|
||||
- 必须至少包含一个:`prefix` 或 `asn`
|
||||
- 可选:`comment`
|
||||
|
||||
匹配规则:
|
||||
- 若配置了 `prefix`:匹配“被该前缀覆盖(encompassed)”的 VRP 前缀
|
||||
- 若配置了 `asn`:匹配该 ASN
|
||||
- 同时配置时:两者都要匹配
|
||||
|
||||
### 10.3.2 `bgpsecFilters`
|
||||
|
||||
数组元素每项:
|
||||
- 必须至少包含一个:`asn` 或 `SKI`
|
||||
- 可选:`comment`
|
||||
|
||||
匹配规则:
|
||||
- 按 `asn`/`SKI` 单独或联合匹配 Router Key(BGPsec)
|
||||
|
||||
### 10.3.3 `prefixAssertions`
|
||||
|
||||
数组元素每项:
|
||||
- 必须:`prefix`、`asn`
|
||||
- 可选:`maxPrefixLength`、`comment`
|
||||
|
||||
约束:
|
||||
- 若给出 `maxPrefixLength`,应满足 `prefix 长度 <= maxPrefixLength <= 地址位宽(IPv4=32, IPv6=128)`
|
||||
|
||||
### 10.3.4 `bgpsecAssertions`
|
||||
|
||||
数组元素每项:
|
||||
- 必须:`asn`、`SKI`、`routerPublicKey`
|
||||
- 可选:`comment`
|
||||
|
||||
## 10.4 ASPA 扩展(draft-ietf-sidrops-aspa-slurm-04)
|
||||
|
||||
### 10.4.1 `aspaFilters`
|
||||
|
||||
数组元素每项:
|
||||
- 必须:`customerAsn`
|
||||
- 可选:`comment`
|
||||
|
||||
匹配规则:
|
||||
- 当 VAP(Validated ASPA Payload)的 `customerAsn` 等于过滤器 `customerAsn` 时命中并移除。
|
||||
|
||||
### 10.4.2 `aspaAssertions`
|
||||
|
||||
数组元素每项:
|
||||
- 必须:`customerAsn`
|
||||
- 必须:`providerAsns`(ASN 数组)
|
||||
- 可选:`comment`
|
||||
|
||||
关键约束(draft-04):
|
||||
- `customerAsn` 不得出现在 `providerAsns` 中
|
||||
- `providerAsns` 必须按升序排列
|
||||
- `providerAsns` 里的 ASN 必须唯一(无重复)
|
||||
|
||||
语义补充(draft-04):
|
||||
- `aspaAssertions` 仅用于“新增断言”,不构成隐式过滤(不会自动替代 `aspaFilters`)。
|
||||
- 在 RTRv2 输出阶段,新增的 ASPA 断言应加入 ASPA PDU 集合,并做去重。
|
||||
|
||||
## 10.5 应用语义(RFC 8416 Section 4)
|
||||
|
||||
### 10.5.1 原子性
|
||||
|
||||
SLURM 应用必须是原子的:
|
||||
- 要么完全不生效(等同未使用 SLURM)
|
||||
- 要么完整按当前 SLURM 配置生效
|
||||
|
||||
### 10.5.2 处理顺序
|
||||
|
||||
在同一次计算中:
|
||||
1. 先执行 `validationOutputFilters`(移除匹配验证结果)
|
||||
2. 再追加 `locallyAddedAssertions`
|
||||
|
||||
### 10.5.3 多文件
|
||||
|
||||
实现可以支持多个 SLURM 文件并行使用(取并集),但在启用前应检查断言重叠冲突;若存在冲突,整组文件应被拒绝。
|
||||
|
||||
## 10.6 最小可用示例(SLURM v2)
|
||||
|
||||
```json
|
||||
{
|
||||
"slurmVersion": 2,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [
|
||||
{
|
||||
"prefix": "203.0.113.0/24",
|
||||
"comment": "Filter a broken VRP from upstream"
|
||||
}
|
||||
],
|
||||
"bgpsecFilters": [],
|
||||
"aspaFilters": [
|
||||
{
|
||||
"customerAsn": 64496,
|
||||
"comment": "Filter one customer ASPA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [
|
||||
{
|
||||
"asn": 64496,
|
||||
"prefix": "203.0.113.0/24",
|
||||
"maxPrefixLength": 24,
|
||||
"comment": "Local business exception"
|
||||
}
|
||||
],
|
||||
"bgpsecAssertions": [],
|
||||
"aspaAssertions": [
|
||||
{
|
||||
"customerAsn": 64496,
|
||||
"providerAsns": [64497, 64498],
|
||||
"comment": "Local ASPA assertion"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 10.7 参考文献
|
||||
|
||||
- RFC 8416: https://www.rfc-editor.org/rfc/rfc8416.html
|
||||
- draft-ietf-sidrops-aspa-slurm-04: https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-slurm-04.html
|
||||
158
specs/11_rtr.md
158
specs/11_rtr.md
@@ -1,158 +0,0 @@
|
||||
# 11. RTR(RPKI to Router Protocol)
|
||||
|
||||
## 11.1 目标与文档范围
|
||||
|
||||
RTR 用于把 RP/Cache 已完成密码学验证的 RPKI 数据下发给路由器。
|
||||
|
||||
本文按以下规范整理:
|
||||
- RFC 6810(RTR v0)
|
||||
- RFC 8210(RTR v1,更新 RFC 6810)
|
||||
- draft-ietf-sidrops-8210bis-25(RTR v2,草案)
|
||||
|
||||
## 11.2 协议演进
|
||||
|
||||
### 11.2.1 RFC 6810(v0)
|
||||
|
||||
- 只定义 Prefix Origin 相关 payload(IPv4/IPv6 Prefix PDU)。
|
||||
- 主要 PDU:Serial Notify / Serial Query / Reset Query / Cache Response / Prefix / End of Data / Cache Reset / Error Report。
|
||||
|
||||
### 11.2.2 RFC 8210(v1)
|
||||
|
||||
在 v0 基础上新增/强化:
|
||||
- 新增 `Router Key PDU`(PDU Type 9,v1 可用,v0 保留)。
|
||||
- 强化协议版本协商与降级行为。
|
||||
- `End of Data` 在 v1 中携带 `Refresh/Retry/Expire` 三个计时参数。
|
||||
|
||||
### 11.2.3 Version 2(草案)
|
||||
|
||||
在 v1 基础上新增/强化:
|
||||
- 新增 `ASPA PDU`(PDU Type 11,仅 v2)。
|
||||
- 新增 “Races, Ordering, and Transactions” 章节,要求缓存按规定顺序输出 payload 以降低路由器短暂误判。
|
||||
- 协议版本提升到 `2`。
|
||||
- 明确 PDU 最大长度上限为 64k(65535)。
|
||||
|
||||
## 11.3 PDU 与版本矩阵
|
||||
|
||||
PDU 类型(按规范注册表):
|
||||
|
||||
| PDU Type | 名称 | v0 (RFC6810) | v1 (RFC8210) | v2 (8210bis-25) |
|
||||
|---|---|---|---|---|
|
||||
| 0 | Serial Notify | 支持 | 支持 | 支持 |
|
||||
| 1 | Serial Query | 支持 | 支持 | 支持 |
|
||||
| 2 | Reset Query | 支持 | 支持 | 支持 |
|
||||
| 3 | Cache Response | 支持 | 支持 | 支持 |
|
||||
| 4 | IPv4 Prefix | 支持 | 支持 | 支持 |
|
||||
| 6 | IPv6 Prefix | 支持 | 支持 | 支持 |
|
||||
| 7 | End of Data | 支持 | 支持(含计时参数) | 支持 |
|
||||
| 8 | Cache Reset | 支持 | 支持 | 支持 |
|
||||
| 9 | Router Key | 保留 | 支持 | 支持 |
|
||||
| 10 | Error Report | 支持 | 支持 | 支持 |
|
||||
| 11 | ASPA | 保留 | 保留 | 支持 |
|
||||
|
||||
通用字段约束:
|
||||
- `Protocol Version`:8-bit。
|
||||
- `PDU Type`:8-bit。
|
||||
- `Session ID`:16-bit。
|
||||
- `Length`:32-bit。
|
||||
- 保留位(zero/reserved)发送必须为 0,接收时按规范处理。
|
||||
|
||||
## 11.4 关键 PDU 语义
|
||||
|
||||
### 11.4.1 Serial Notify(Type 0)
|
||||
|
||||
- 由 Cache 主动发送,提示有新序列可拉取。
|
||||
- 是少数可不由 Router 请求触发的消息。
|
||||
|
||||
### 11.4.2 Reset Query(Type 2)与 Cache Response(Type 3)
|
||||
|
||||
- Router 启动或失配时发 `Reset Query` 请求全量。
|
||||
- Cache 回复 `Cache Response`,随后发送全量 payload,最后 `End of Data`。
|
||||
|
||||
### 11.4.3 Serial Query(Type 1)
|
||||
|
||||
- Router 持有上次 `Session ID + Serial` 时请求增量。
|
||||
- Cache 若可提供增量:返回变化集。
|
||||
- Cache 若无法从该 serial 补增量:返回 `Cache Reset`,要求 Router 走全量。
|
||||
|
||||
### 11.4.4 Prefix / Router Key / ASPA payload
|
||||
|
||||
- `IPv4 Prefix`(Type 4)/ `IPv6 Prefix`(Type 6):表示 VRP 的 announce/withdraw。
|
||||
- `Router Key`(Type 9,v1+):表示 BGPsec Router Key 的 announce/withdraw。
|
||||
- `ASPA`(Type 11,v2 草案):表示 ASPA 数据单元的 announce/withdraw。
|
||||
|
||||
语义要点(v1 / v2 草案):
|
||||
- 对同一 payload 键(如 Prefix 四元组、Router Key 三元组、ASPA customer 键)应维护清晰的替换/撤销关系。
|
||||
- Cache 负责把历史变化“合并简化”后再发给 Router,避免无意义抖动。
|
||||
|
||||
### 11.4.5 End of Data(Type 7)
|
||||
|
||||
- 标识一次响应结束,并给出当前 serial。
|
||||
- v0:不含定时器字段。
|
||||
- v1/v2:携带 `Refresh Interval`、`Retry Interval`、`Expire Interval`。
|
||||
|
||||
## 11.5 协议时序
|
||||
|
||||
### 11.5.1 初始同步(Full Sync)
|
||||
|
||||
1. Router 建连后发 `Reset Query`(带支持的协议版本)。
|
||||
2. Cache 回 `Cache Response`。
|
||||
3. Cache 按规范发送 payload 集合。
|
||||
4. Cache 发 `End of Data` 收尾。
|
||||
|
||||
### 11.5.2 增量同步(Incremental Sync)
|
||||
|
||||
1. Router 发 `Serial Query(session_id, serial)`。
|
||||
2. Cache 若可增量,返回变化并以 `End of Data` 收尾。
|
||||
3. 若不可增量,返回 `Cache Reset`;Router 退回 Full Sync。
|
||||
|
||||
## 11.6 版本协商与降级
|
||||
|
||||
- Router 每次新连接必须由 `Reset Query` 或 `Serial Query` 启动,携带其协议版本。
|
||||
- 双方在协商完成后,本连接内版本固定。
|
||||
- 遇到不支持版本时,可按规范降级(例如 v1 对 v0、v2 对 v1/v0)或返回 `Unsupported Protocol Version` 后断开。
|
||||
- 协商期若收到 `Serial Notify`,Router 应按规范兼容处理(通常忽略,待协商完成)。
|
||||
|
||||
## 11.7 计时器与失效(v1/v2)
|
||||
|
||||
`End of Data` 下发三个参数:
|
||||
- `Refresh Interval`:多久后主动刷新。
|
||||
- `Retry Interval`:失败后重试间隔。
|
||||
- `Expire Interval`:本地数据最长可保留时长。
|
||||
|
||||
规范边界(RFC 8210):
|
||||
- Refresh: 1 .. 86400(推荐 3600)
|
||||
- Retry: 1 .. 7200(推荐 600)
|
||||
- Expire: 600 .. 172800(推荐 7200)
|
||||
- 且 `Expire` 必须大于 `Refresh` 和 `Retry`。
|
||||
|
||||
## 11.8 Version 2(草案)新增关注点
|
||||
|
||||
### 11.8.1 ASPA PDU
|
||||
|
||||
- 新增 ASPA 传输能力(Type 11)。
|
||||
- 针对同一 customer ASN,Cache 需向 Router 提供一致且可替换的 ASPA 视图。
|
||||
|
||||
### 11.8.2 排序与事务
|
||||
|
||||
- 草案新增 race 条件说明(如前缀替换、撤销先后导致短暂误判)。
|
||||
- 对 Cache 输出 payload 的顺序提出约束。
|
||||
- 建议 Router 使用“事务式应用”(例如接收到完整响应后再切换生效)降低中间态影响。
|
||||
|
||||
## 11.9 传输与安全
|
||||
|
||||
规范定义可承载于多种传输:
|
||||
- SSH
|
||||
- TLS
|
||||
- TCP MD5
|
||||
- TCP-AO
|
||||
|
||||
安全原则:
|
||||
- Router 与 Cache 之间必须建立可信关系。
|
||||
- 需要完整性/机密性时优先使用具备认证与加密能力的传输。
|
||||
- 若使用普通 TCP,部署上应限制在可信受控网络中。
|
||||
|
||||
## 11.10 参考文献
|
||||
|
||||
- RFC 6810: https://www.rfc-editor.org/rfc/rfc6810.html
|
||||
- RFC 8210: https://www.rfc-editor.org/rfc/rfc8210.html
|
||||
- draft-ietf-sidrops-8210bis-25: https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-25.html
|
||||
5532
specs/arch.excalidraw
Normal file
5532
specs/arch.excalidraw
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
1
src/analysis/mod.rs
Normal file
1
src/analysis/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod timing;
|
||||
308
src/analysis/timing.rs
Normal file
308
src/analysis/timing.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TimingHandle {
|
||||
inner: Arc<Mutex<TimingCollector>>,
|
||||
}
|
||||
|
||||
impl TimingHandle {
|
||||
pub fn new(meta: TimingMeta) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(TimingCollector::new(meta))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_phase(&self, phase: &'static str) -> TimingSpanGuard<'_> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::Phase(phase),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_rrdp_repo<'a>(&self, repo_uri: &'a str) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::RrdpRepo(repo_uri),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_rrdp_repo_step<'a>(
|
||||
&self,
|
||||
repo_uri: &'a str,
|
||||
step: &'static str,
|
||||
) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::RrdpRepoStep { repo_uri, step },
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn span_publication_point<'a>(&self, manifest_rsync_uri: &'a str) -> TimingSpanGuard<'a> {
|
||||
TimingSpanGuard {
|
||||
handle: self.clone(),
|
||||
kind: TimingSpanKind::PublicationPoint(manifest_rsync_uri),
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_meta(&self, update: TimingMetaUpdate<'_>) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
if let Some(v) = update.tal_url {
|
||||
g.meta.tal_url = Some(v.to_string());
|
||||
}
|
||||
if let Some(v) = update.db_path {
|
||||
g.meta.db_path = Some(v.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_count(&self, key: &'static str, inc: u64) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
g.counts
|
||||
.entry(key)
|
||||
.and_modify(|v| *v = v.saturating_add(inc))
|
||||
.or_insert(inc);
|
||||
}
|
||||
|
||||
/// Record a phase duration directly in nanoseconds.
|
||||
///
|
||||
/// This is useful when aggregating sub-phase timings locally (to reduce lock contention)
|
||||
/// and then emitting a single record per publication point.
|
||||
pub fn record_phase_nanos(&self, phase: &'static str, nanos: u64) {
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
g.phases.record(phase, nanos);
|
||||
}
|
||||
|
||||
pub fn write_json(&self, path: &Path, top_n: usize) -> Result<(), String> {
|
||||
let report = {
|
||||
let g = self.inner.lock().expect("timing lock");
|
||||
g.to_report(top_n)
|
||||
};
|
||||
|
||||
let f = std::fs::File::create(path)
|
||||
.map_err(|e| format!("create timing json failed: {}: {e}", path.display()))?;
|
||||
serde_json::to_writer_pretty(f, &report)
|
||||
.map_err(|e| format!("write timing json failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn record_duration(&self, kind: TimingSpanKind<'_>, duration: Duration) {
|
||||
let nanos_u64 = duration.as_nanos().min(u128::from(u64::MAX)) as u64;
|
||||
let mut g = self.inner.lock().expect("timing lock");
|
||||
match kind {
|
||||
TimingSpanKind::Phase(name) => g.phases.record(name, nanos_u64),
|
||||
TimingSpanKind::RrdpRepo(uri) => g.rrdp_repos.record(uri, nanos_u64),
|
||||
TimingSpanKind::RrdpRepoStep { repo_uri, step } => g
|
||||
.rrdp_repo_steps
|
||||
.record(&format!("{repo_uri}::{step}"), nanos_u64),
|
||||
TimingSpanKind::PublicationPoint(uri) => g.publication_points.record(uri, nanos_u64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TimingMeta {
|
||||
pub recorded_at_utc_rfc3339: String,
|
||||
pub validation_time_utc_rfc3339: String,
|
||||
pub tal_url: Option<String>,
|
||||
pub db_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TimingMetaUpdate<'a> {
|
||||
pub tal_url: Option<&'a str>,
|
||||
pub db_path: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub struct TimingSpanGuard<'a> {
|
||||
handle: TimingHandle,
|
||||
kind: TimingSpanKind<'a>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl Drop for TimingSpanGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.handle
|
||||
.record_duration(self.kind.clone(), self.start.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum TimingSpanKind<'a> {
|
||||
Phase(&'static str),
|
||||
RrdpRepo(&'a str),
|
||||
RrdpRepoStep {
|
||||
repo_uri: &'a str,
|
||||
step: &'static str,
|
||||
},
|
||||
PublicationPoint(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct DurationStats {
|
||||
pub count: u64,
|
||||
pub total_nanos: u64,
|
||||
}
|
||||
|
||||
impl DurationStats {
|
||||
fn record(&mut self, nanos: u64) {
|
||||
self.count = self.count.saturating_add(1);
|
||||
self.total_nanos = self.total_nanos.saturating_add(nanos);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct DurationStatsMap {
|
||||
map: HashMap<String, DurationStats>,
|
||||
}
|
||||
|
||||
impl DurationStatsMap {
|
||||
fn record(&mut self, key: &str, nanos: u64) {
|
||||
self.map.entry(key.to_string()).or_default().record(nanos);
|
||||
}
|
||||
|
||||
fn top(&self, n: usize) -> Vec<TopDurationEntry> {
|
||||
let mut v = self
|
||||
.map
|
||||
.iter()
|
||||
.map(|(k, s)| TopDurationEntry {
|
||||
key: k.clone(),
|
||||
count: s.count,
|
||||
total_nanos: s.total_nanos,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
v.sort_by(|a, b| b.total_nanos.cmp(&a.total_nanos));
|
||||
v.truncate(n);
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
struct TimingCollector {
|
||||
meta: TimingMeta,
|
||||
counts: HashMap<&'static str, u64>,
|
||||
phases: DurationStatsMap,
|
||||
rrdp_repos: DurationStatsMap,
|
||||
rrdp_repo_steps: DurationStatsMap,
|
||||
publication_points: DurationStatsMap,
|
||||
}
|
||||
|
||||
impl TimingCollector {
|
||||
fn new(meta: TimingMeta) -> Self {
|
||||
Self {
|
||||
meta,
|
||||
counts: HashMap::new(),
|
||||
phases: DurationStatsMap::default(),
|
||||
rrdp_repos: DurationStatsMap::default(),
|
||||
rrdp_repo_steps: DurationStatsMap::default(),
|
||||
publication_points: DurationStatsMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_report(&self, top_n: usize) -> TimingReportV1 {
|
||||
TimingReportV1 {
|
||||
format_version: 1,
|
||||
meta: self.meta.clone(),
|
||||
counts: self
|
||||
.counts
|
||||
.iter()
|
||||
.map(|(k, v)| ((*k).to_string(), *v))
|
||||
.collect(),
|
||||
phases: self
|
||||
.phases
|
||||
.map
|
||||
.iter()
|
||||
.map(|(k, s)| (k.clone(), s.clone()))
|
||||
.collect(),
|
||||
top_rrdp_repos: self.rrdp_repos.top(top_n),
|
||||
top_rrdp_repo_steps: self.rrdp_repo_steps.top(top_n),
|
||||
top_publication_points: self.publication_points.top(top_n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TimingReportV1 {
|
||||
pub format_version: u64,
|
||||
pub meta: TimingMeta,
|
||||
pub counts: HashMap<String, u64>,
|
||||
pub phases: HashMap<String, DurationStats>,
|
||||
pub top_rrdp_repos: Vec<TopDurationEntry>,
|
||||
pub top_rrdp_repo_steps: Vec<TopDurationEntry>,
|
||||
pub top_publication_points: Vec<TopDurationEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct TopDurationEntry {
|
||||
pub key: String,
|
||||
pub count: u64,
|
||||
pub total_nanos: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn timing_handle_writes_json_with_phases_and_tops() {
|
||||
let meta = TimingMeta {
|
||||
recorded_at_utc_rfc3339: "2026-02-28T00:00:00Z".to_string(),
|
||||
validation_time_utc_rfc3339: "2026-02-28T00:00:00Z".to_string(),
|
||||
tal_url: Some("https://example.test/x.tal".to_string()),
|
||||
db_path: Some("db".to_string()),
|
||||
};
|
||||
let h = TimingHandle::new(meta);
|
||||
|
||||
{
|
||||
let _p = h.span_phase("tal_bootstrap");
|
||||
}
|
||||
{
|
||||
let _r = h.span_rrdp_repo("https://rrdp.example.test/notification.xml");
|
||||
}
|
||||
{
|
||||
let _s = h.span_rrdp_repo_step(
|
||||
"https://rrdp.example.test/notification.xml",
|
||||
"fetch_notification",
|
||||
);
|
||||
}
|
||||
{
|
||||
let _pp = h.span_publication_point("rsync://example.test/repo/manifest.mft");
|
||||
}
|
||||
h.record_count("vrps", 42);
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("timing.json");
|
||||
h.write_json(&path, 10).expect("write_json");
|
||||
|
||||
let rep: TimingReportV1 =
|
||||
serde_json::from_slice(&std::fs::read(&path).expect("read timing.json"))
|
||||
.expect("parse timing.json");
|
||||
assert_eq!(rep.format_version, 1);
|
||||
assert!(rep.phases.contains_key("tal_bootstrap"));
|
||||
assert_eq!(rep.counts.get("vrps").copied(), Some(42));
|
||||
assert!(
|
||||
rep.top_rrdp_repos
|
||||
.iter()
|
||||
.any(|e| e.key.contains("rrdp.example.test")),
|
||||
"expected repo in top list"
|
||||
);
|
||||
assert!(
|
||||
rep.top_rrdp_repo_steps
|
||||
.iter()
|
||||
.any(|e| e.key.contains("fetch_notification")),
|
||||
"expected repo step in top list"
|
||||
);
|
||||
assert!(
|
||||
rep.top_publication_points
|
||||
.iter()
|
||||
.any(|e| e.key.contains("manifest.mft")),
|
||||
"expected PP in top list"
|
||||
);
|
||||
}
|
||||
}
|
||||
224
src/audit.rs
Normal file
224
src/audit.rs
Normal file
@@ -0,0 +1,224 @@
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::policy::Policy;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditObjectKind {
|
||||
Manifest,
|
||||
Crl,
|
||||
Certificate,
|
||||
Roa,
|
||||
Aspa,
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditObjectResult {
|
||||
Ok,
|
||||
Skipped,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ObjectAuditEntry {
|
||||
pub rsync_uri: String,
|
||||
pub sha256_hex: String,
|
||||
pub kind: AuditObjectKind,
|
||||
pub result: AuditObjectResult,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditWarning {
|
||||
pub message: String,
|
||||
pub rfc_refs: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&crate::report::Warning> for AuditWarning {
|
||||
fn from(w: &crate::report::Warning) -> Self {
|
||||
Self {
|
||||
message: w.message.clone(),
|
||||
rfc_refs: w.rfc_refs.iter().map(|r| r.0.to_string()).collect(),
|
||||
context: w.context.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct PublicationPointAudit {
|
||||
/// Monotonic node ID assigned by the traversal engine.
|
||||
///
|
||||
/// Present when running via the Stage2 tree engine; may be absent in ad-hoc runs.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub node_id: Option<u64>,
|
||||
/// Parent node ID in the traversal tree.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub parent_node_id: Option<u64>,
|
||||
/// Provenance metadata for non-root nodes (how this CA instance was discovered).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub discovered_from: Option<DiscoveredFrom>,
|
||||
|
||||
pub rsync_base_uri: String,
|
||||
pub manifest_rsync_uri: String,
|
||||
pub publication_point_rsync_uri: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rrdp_notification_uri: Option<String>,
|
||||
|
||||
pub source: String,
|
||||
pub this_update_rfc3339_utc: String,
|
||||
pub next_update_rfc3339_utc: String,
|
||||
pub verified_at_rfc3339_utc: String,
|
||||
|
||||
pub warnings: Vec<AuditWarning>,
|
||||
pub objects: Vec<ObjectAuditEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct DiscoveredFrom {
|
||||
pub parent_manifest_rsync_uri: String,
|
||||
pub child_ca_certificate_rsync_uri: String,
|
||||
pub child_ca_certificate_sha256_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct TreeSummary {
|
||||
pub instances_processed: usize,
|
||||
pub instances_failed: usize,
|
||||
pub warnings: Vec<AuditWarning>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditRunMeta {
|
||||
pub validation_time_rfc3339_utc: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AuditDownloadKind {
|
||||
RrdpNotification,
|
||||
RrdpSnapshot,
|
||||
RrdpDelta,
|
||||
Rsync,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadObjectsStat {
|
||||
pub objects_count: u64,
|
||||
pub objects_bytes_total: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadEvent {
|
||||
pub kind: AuditDownloadKind,
|
||||
pub uri: String,
|
||||
pub started_at_rfc3339_utc: String,
|
||||
pub finished_at_rfc3339_utc: String,
|
||||
pub duration_ms: u64,
|
||||
pub success: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects: Option<AuditDownloadObjectsStat>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadKindStats {
|
||||
pub ok_total: u64,
|
||||
pub fail_total: u64,
|
||||
pub duration_ms_total: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bytes_total: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects_count_total: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub objects_bytes_total: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditDownloadStats {
|
||||
pub events_total: u64,
|
||||
/// Statistics keyed by serialized `AuditDownloadKind` string (e.g. "rrdp_snapshot").
|
||||
pub by_kind: std::collections::BTreeMap<String, AuditDownloadKindStats>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditReportV1 {
|
||||
pub format_version: u32,
|
||||
pub meta: AuditRunMeta,
|
||||
pub policy: Policy,
|
||||
pub tree: TreeSummary,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
|
||||
pub vrps: Vec<VrpOutput>,
|
||||
pub aspas: Vec<AspaOutput>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AuditReportV2 {
|
||||
pub format_version: u32,
|
||||
pub meta: AuditRunMeta,
|
||||
pub policy: Policy,
|
||||
pub tree: TreeSummary,
|
||||
pub publication_points: Vec<PublicationPointAudit>,
|
||||
|
||||
pub vrps: Vec<VrpOutput>,
|
||||
pub aspas: Vec<AspaOutput>,
|
||||
|
||||
pub downloads: Vec<AuditDownloadEvent>,
|
||||
pub download_stats: AuditDownloadStats,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct VrpOutput {
|
||||
pub asn: u32,
|
||||
pub prefix: String,
|
||||
pub max_length: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct AspaOutput {
|
||||
pub customer_as_id: u32,
|
||||
pub provider_as_ids: Vec<u32>,
|
||||
}
|
||||
|
||||
pub fn sha256_hex_from_32(bytes: &[u8; 32]) -> String {
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
hex::encode(digest)
|
||||
}
|
||||
|
||||
pub fn format_roa_ip_prefix(p: &crate::data_model::roa::IpPrefix) -> String {
|
||||
let addr = p.addr_bytes();
|
||||
match p.afi {
|
||||
crate::data_model::roa::RoaAfi::Ipv4 => {
|
||||
format!(
|
||||
"{}.{}.{}.{}{}",
|
||||
addr[0],
|
||||
addr[1],
|
||||
addr[2],
|
||||
addr[3],
|
||||
format!("/{}", p.prefix_len)
|
||||
)
|
||||
}
|
||||
crate::data_model::roa::RoaAfi::Ipv6 => {
|
||||
let mut parts = Vec::with_capacity(8);
|
||||
for i in 0..8 {
|
||||
let hi = addr[i * 2] as u16;
|
||||
let lo = addr[i * 2 + 1] as u16;
|
||||
parts.push(format!("{:x}", (hi << 8) | lo));
|
||||
}
|
||||
format!("{}{}", parts.join(":"), format!("/{}", p.prefix_len))
|
||||
}
|
||||
}
|
||||
}
|
||||
168
src/audit_downloads.rs
Normal file
168
src/audit_downloads.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::audit::{
|
||||
AuditDownloadEvent, AuditDownloadKind, AuditDownloadKindStats, AuditDownloadObjectsStat,
|
||||
AuditDownloadStats,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DownloadLogHandle {
|
||||
inner: Arc<Mutex<Vec<AuditDownloadEvent>>>,
|
||||
}
|
||||
|
||||
impl DownloadLogHandle {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn record_event(&self, event: AuditDownloadEvent) {
|
||||
self.inner.lock().expect("download log lock").push(event);
|
||||
}
|
||||
|
||||
pub fn snapshot_events(&self) -> Vec<AuditDownloadEvent> {
|
||||
self.inner.lock().expect("download log lock").clone()
|
||||
}
|
||||
|
||||
pub fn stats_from_events(events: &[AuditDownloadEvent]) -> AuditDownloadStats {
|
||||
let mut out = AuditDownloadStats {
|
||||
events_total: events.len() as u64,
|
||||
by_kind: BTreeMap::new(),
|
||||
};
|
||||
for e in events {
|
||||
let kind_key = match e.kind {
|
||||
AuditDownloadKind::RrdpNotification => "rrdp_notification",
|
||||
AuditDownloadKind::RrdpSnapshot => "rrdp_snapshot",
|
||||
AuditDownloadKind::RrdpDelta => "rrdp_delta",
|
||||
AuditDownloadKind::Rsync => "rsync",
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let st = out.by_kind.entry(kind_key).or_insert_with(|| AuditDownloadKindStats {
|
||||
ok_total: 0,
|
||||
fail_total: 0,
|
||||
duration_ms_total: 0,
|
||||
bytes_total: None,
|
||||
objects_count_total: None,
|
||||
objects_bytes_total: None,
|
||||
});
|
||||
if e.success {
|
||||
st.ok_total = st.ok_total.saturating_add(1);
|
||||
} else {
|
||||
st.fail_total = st.fail_total.saturating_add(1);
|
||||
}
|
||||
st.duration_ms_total = st.duration_ms_total.saturating_add(e.duration_ms);
|
||||
if let Some(b) = e.bytes {
|
||||
st.bytes_total = Some(st.bytes_total.unwrap_or(0).saturating_add(b));
|
||||
}
|
||||
if let Some(objects) = &e.objects {
|
||||
st.objects_count_total = Some(
|
||||
st.objects_count_total
|
||||
.unwrap_or(0)
|
||||
.saturating_add(objects.objects_count),
|
||||
);
|
||||
st.objects_bytes_total = Some(
|
||||
st.objects_bytes_total
|
||||
.unwrap_or(0)
|
||||
.saturating_add(objects.objects_bytes_total),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> AuditDownloadStats {
|
||||
let events = self.snapshot_events();
|
||||
Self::stats_from_events(&events)
|
||||
}
|
||||
|
||||
pub fn span_download<'a>(
|
||||
&'a self,
|
||||
kind: AuditDownloadKind,
|
||||
uri: &'a str,
|
||||
) -> DownloadSpanGuard<'a> {
|
||||
DownloadSpanGuard {
|
||||
handle: self,
|
||||
kind,
|
||||
uri,
|
||||
start_instant: Instant::now(),
|
||||
started_at: time::OffsetDateTime::now_utc(),
|
||||
bytes: None,
|
||||
objects: None,
|
||||
error: None,
|
||||
success: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DownloadSpanGuard<'a> {
|
||||
handle: &'a DownloadLogHandle,
|
||||
kind: AuditDownloadKind,
|
||||
uri: &'a str,
|
||||
start_instant: Instant,
|
||||
started_at: time::OffsetDateTime,
|
||||
bytes: Option<u64>,
|
||||
objects: Option<AuditDownloadObjectsStat>,
|
||||
error: Option<String>,
|
||||
success: Option<bool>,
|
||||
}
|
||||
|
||||
impl DownloadSpanGuard<'_> {
|
||||
pub fn set_bytes(&mut self, bytes: u64) {
|
||||
self.bytes = Some(bytes);
|
||||
}
|
||||
|
||||
pub fn set_objects(&mut self, objects_count: u64, objects_bytes_total: u64) {
|
||||
self.objects = Some(AuditDownloadObjectsStat {
|
||||
objects_count,
|
||||
objects_bytes_total,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_ok(&mut self) {
|
||||
self.success = Some(true);
|
||||
}
|
||||
|
||||
pub fn set_err(&mut self, msg: impl Into<String>) {
|
||||
self.success = Some(false);
|
||||
self.error = Some(msg.into());
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for DownloadSpanGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
let finished_at = time::OffsetDateTime::now_utc();
|
||||
let dur = self.start_instant.elapsed();
|
||||
let duration_ms = duration_to_ms(dur);
|
||||
let started_at_rfc3339_utc = self
|
||||
.started_at
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "<format-error>".to_string());
|
||||
let finished_at_rfc3339_utc = finished_at
|
||||
.to_offset(time::UtcOffset::UTC)
|
||||
.format(&Rfc3339)
|
||||
.unwrap_or_else(|_| "<format-error>".to_string());
|
||||
let success = self.success.unwrap_or(false);
|
||||
let event = AuditDownloadEvent {
|
||||
kind: self.kind.clone(),
|
||||
uri: self.uri.to_string(),
|
||||
started_at_rfc3339_utc,
|
||||
finished_at_rfc3339_utc,
|
||||
duration_ms,
|
||||
success,
|
||||
error: if success { None } else { self.error.clone() },
|
||||
bytes: self.bytes,
|
||||
objects: self.objects.clone(),
|
||||
};
|
||||
self.handle.record_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_to_ms(d: Duration) -> u64 {
|
||||
let ms = d.as_millis();
|
||||
ms.min(u128::from(u64::MAX)) as u64
|
||||
}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
use std::fs;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
const CONTENT_TYPE_OID: &str = "1.2.840.113549.1.9.16.1.54";
|
||||
const SHA256_OID: &str = "2.16.840.1.101.3.4.2.1";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Vrp {
|
||||
addr: IpAddr,
|
||||
prefix_len: u8,
|
||||
max_len: u8,
|
||||
asn: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Vap {
|
||||
customer_asn: u32,
|
||||
providers: Vec<u32>,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let out_dir = parse_out_dir_arg();
|
||||
fs::create_dir_all(&out_dir)
|
||||
.with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
|
||||
|
||||
write_snapshot(
|
||||
&out_dir.join("20260403T000001Z-mini-a.ccr"),
|
||||
"20260403000001Z",
|
||||
vec![
|
||||
Vrp {
|
||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
||||
prefix_len: 24,
|
||||
max_len: 24,
|
||||
asn: 65001,
|
||||
},
|
||||
Vrp {
|
||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
||||
prefix_len: 48,
|
||||
max_len: 48,
|
||||
asn: 65002,
|
||||
},
|
||||
],
|
||||
vec![Vap {
|
||||
customer_asn: 65010,
|
||||
providers: vec![65011, 65012],
|
||||
}],
|
||||
)?;
|
||||
|
||||
write_snapshot(
|
||||
&out_dir.join("20260403T000101Z-mini-b.ccr"),
|
||||
"20260403000101Z",
|
||||
vec![
|
||||
Vrp {
|
||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
||||
prefix_len: 24,
|
||||
max_len: 24,
|
||||
asn: 65001,
|
||||
},
|
||||
Vrp {
|
||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
||||
prefix_len: 24,
|
||||
max_len: 24,
|
||||
asn: 65003,
|
||||
},
|
||||
Vrp {
|
||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
||||
prefix_len: 48,
|
||||
max_len: 48,
|
||||
asn: 65002,
|
||||
},
|
||||
],
|
||||
vec![Vap {
|
||||
customer_asn: 65010,
|
||||
providers: vec![65011, 65012, 65013],
|
||||
}],
|
||||
)?;
|
||||
|
||||
write_snapshot(
|
||||
&out_dir.join("20260403T000201Z-mini-c.ccr"),
|
||||
"20260403000201Z",
|
||||
vec![
|
||||
Vrp {
|
||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
||||
prefix_len: 24,
|
||||
max_len: 24,
|
||||
asn: 65003,
|
||||
},
|
||||
Vrp {
|
||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 0)),
|
||||
prefix_len: 48,
|
||||
max_len: 48,
|
||||
asn: 65004,
|
||||
},
|
||||
],
|
||||
vec![
|
||||
Vap {
|
||||
customer_asn: 65010,
|
||||
providers: vec![65012, 65013],
|
||||
},
|
||||
Vap {
|
||||
customer_asn: 65020,
|
||||
providers: vec![65021],
|
||||
},
|
||||
],
|
||||
)?;
|
||||
|
||||
println!("generated CCR fixtures under {}", out_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_out_dir_arg() -> PathBuf {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let mut out_dir = PathBuf::from("data");
|
||||
while let Some(arg) = args.next() {
|
||||
if arg == "--out-dir" {
|
||||
if let Some(v) = args.next() {
|
||||
out_dir = PathBuf::from(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
out_dir
|
||||
}
|
||||
|
||||
fn write_snapshot(path: &Path, produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Result<()> {
|
||||
let bytes = encode_ccr_snapshot(produced_at, vrps, vaps);
|
||||
fs::write(path, bytes).with_context(|| format!("failed to write {}", path.display()))?;
|
||||
println!("wrote {}", path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_ccr_snapshot(produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Vec<u8> {
|
||||
let vrp_sets = vrps
|
||||
.into_iter()
|
||||
.map(encode_roa_payload_set)
|
||||
.collect::<Vec<_>>();
|
||||
let vap_sets = vaps
|
||||
.into_iter()
|
||||
.map(encode_aspa_payload_set)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let vrp_set_seq = der_sequence(vrp_sets);
|
||||
let vap_set_seq = der_sequence(vap_sets);
|
||||
|
||||
let vrp_hash = Sha256::digest(&vrp_set_seq).to_vec();
|
||||
let vap_hash = Sha256::digest(&vap_set_seq).to_vec();
|
||||
|
||||
// draft-ietf-sidrops-rpki-ccr-02:
|
||||
// ROAPayloadState/ASPAPayloadState include payload-set sequence + hash.
|
||||
let vrp_state = der_sequence(vec![vrp_set_seq, der_octet_string(vrp_hash)]);
|
||||
let vap_state = der_sequence(vec![vap_set_seq, der_octet_string(vap_hash)]);
|
||||
|
||||
// AlgorithmIdentifier for SHA-256.
|
||||
let hash_alg = der_sequence(vec![der_oid(SHA256_OID), der_null()]);
|
||||
|
||||
let payload = der_sequence(vec![
|
||||
der_integer(0),
|
||||
hash_alg,
|
||||
der_generalized_time(produced_at),
|
||||
der_ctx(2, vrp_state),
|
||||
der_ctx(3, vap_state),
|
||||
]);
|
||||
|
||||
der_sequence(vec![der_oid(CONTENT_TYPE_OID), der_ctx(0, payload)])
|
||||
}
|
||||
|
||||
fn encode_roa_payload_set(v: Vrp) -> Vec<u8> {
|
||||
let (afi, addr_bytes) = match v.addr {
|
||||
IpAddr::V4(ip) => ([0u8, 1u8].to_vec(), ip.octets().to_vec()),
|
||||
IpAddr::V6(ip) => ([0u8, 2u8].to_vec(), ip.octets().to_vec()),
|
||||
};
|
||||
let bit_string = prefix_to_bit_string(&addr_bytes, v.prefix_len);
|
||||
let roa_ip = der_sequence(vec![
|
||||
der_bit_string(0, bit_string),
|
||||
der_integer(u32::from(v.max_len)),
|
||||
]);
|
||||
let family = der_sequence(vec![der_octet_string(afi), der_sequence(vec![roa_ip])]);
|
||||
der_sequence(vec![der_integer(v.asn), der_sequence(vec![family])])
|
||||
}
|
||||
|
||||
fn encode_aspa_payload_set(v: Vap) -> Vec<u8> {
|
||||
let providers = v.providers.into_iter().map(der_integer).collect::<Vec<_>>();
|
||||
der_sequence(vec![der_integer(v.customer_asn), der_sequence(providers)])
|
||||
}
|
||||
|
||||
fn prefix_to_bit_string(addr: &[u8], prefix_len: u8) -> Vec<u8> {
|
||||
let byte_len = usize::from(prefix_len).div_ceil(8);
|
||||
let mut out = addr[..byte_len].to_vec();
|
||||
let rem = prefix_len % 8;
|
||||
if rem != 0 {
|
||||
let mask = 0xFFu8 << (8 - rem);
|
||||
let last = out.len() - 1;
|
||||
out[last] &= mask;
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn der_sequence(items: Vec<Vec<u8>>) -> Vec<u8> {
|
||||
let content = items.concat();
|
||||
der_tlv(0x30, content)
|
||||
}
|
||||
|
||||
fn der_integer(v: u32) -> Vec<u8> {
|
||||
if v == 0 {
|
||||
return der_tlv(0x02, vec![0]);
|
||||
}
|
||||
let mut bytes = v.to_be_bytes().to_vec();
|
||||
while bytes.len() > 1 && bytes[0] == 0 {
|
||||
bytes.remove(0);
|
||||
}
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0);
|
||||
}
|
||||
der_tlv(0x02, bytes)
|
||||
}
|
||||
|
||||
fn der_oid(oid: &str) -> Vec<u8> {
|
||||
let parts = oid
|
||||
.split('.')
|
||||
.map(|s| s.parse::<u32>().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(parts.len() >= 2);
|
||||
let mut out = Vec::new();
|
||||
out.push((parts[0] * 40 + parts[1]) as u8);
|
||||
for &part in &parts[2..] {
|
||||
out.extend(base128(part));
|
||||
}
|
||||
der_tlv(0x06, out)
|
||||
}
|
||||
|
||||
fn base128(mut n: u32) -> Vec<u8> {
|
||||
let mut buf = vec![(n & 0x7F) as u8];
|
||||
n >>= 7;
|
||||
while n > 0 {
|
||||
buf.push(((n & 0x7F) as u8) | 0x80);
|
||||
n >>= 7;
|
||||
}
|
||||
buf.reverse();
|
||||
buf
|
||||
}
|
||||
|
||||
fn der_octet_string(bytes: Vec<u8>) -> Vec<u8> {
|
||||
der_tlv(0x04, bytes)
|
||||
}
|
||||
|
||||
fn der_null() -> Vec<u8> {
|
||||
der_tlv(0x05, Vec::new())
|
||||
}
|
||||
|
||||
fn der_bit_string(unused_bits: u8, bytes: Vec<u8>) -> Vec<u8> {
|
||||
let mut content = Vec::with_capacity(1 + bytes.len());
|
||||
content.push(unused_bits);
|
||||
content.extend(bytes);
|
||||
der_tlv(0x03, content)
|
||||
}
|
||||
|
||||
fn der_generalized_time(v: &str) -> Vec<u8> {
|
||||
der_tlv(0x18, v.as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn der_ctx(tag_no: u8, encoded_inner_der: Vec<u8>) -> Vec<u8> {
|
||||
der_tlv(0xA0 + tag_no, encoded_inner_der)
|
||||
}
|
||||
|
||||
fn der_tlv(tag: u8, content: Vec<u8>) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(2 + content.len());
|
||||
out.push(tag);
|
||||
out.extend(der_len(content.len()));
|
||||
out.extend(content);
|
||||
out
|
||||
}
|
||||
|
||||
fn der_len(len: usize) -> Vec<u8> {
|
||||
if len < 128 {
|
||||
return vec![len as u8];
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
let mut n = len;
|
||||
while n > 0 {
|
||||
bytes.push((n & 0xFF) as u8);
|
||||
n >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
let mut out = vec![0x80 | (bytes.len() as u8)];
|
||||
out.extend(bytes);
|
||||
out
|
||||
}
|
||||
125
src/bin/db_stats.rs
Normal file
125
src/bin/db_stats.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rocksdb::{ColumnFamilyDescriptor, DB, IteratorMode, Options};
|
||||
|
||||
const CF_RAW_OBJECTS: &str = "raw_objects";
|
||||
const CF_FETCH_CACHE_PP: &str = "fetch_cache_pp";
|
||||
const CF_RRDP_STATE: &str = "rrdp_state";
|
||||
const CF_RRDP_OBJECT_INDEX: &str = "rrdp_object_index";
|
||||
|
||||
fn enable_blobdb_if_supported(opts: &mut Options) {
|
||||
// Keep this in sync with `rpki::storage`:
|
||||
// blob files are CF-level options; readers should open CFs with blob enabled too.
|
||||
#[allow(dead_code)]
|
||||
fn _set(opts: &mut Options) {
|
||||
opts.set_enable_blob_files(true);
|
||||
opts.set_min_blob_size(1024);
|
||||
}
|
||||
_set(opts);
|
||||
}
|
||||
|
||||
fn usage() -> String {
|
||||
let bin = "db_stats";
|
||||
format!(
|
||||
"\
|
||||
Usage:
|
||||
{bin} --db <path> [--exact]
|
||||
|
||||
Options:
|
||||
--db <path> RocksDB directory
|
||||
--exact Iterate to count keys (slower; default uses RocksDB estimates)
|
||||
--help Show this help
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
fn cf_descriptors() -> Vec<ColumnFamilyDescriptor> {
|
||||
let mut cf_opts = Options::default();
|
||||
enable_blobdb_if_supported(&mut cf_opts);
|
||||
vec![
|
||||
ColumnFamilyDescriptor::new(CF_RAW_OBJECTS, cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new(CF_FETCH_CACHE_PP, cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new(CF_RRDP_STATE, cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new(CF_RRDP_OBJECT_INDEX, cf_opts),
|
||||
]
|
||||
}
|
||||
|
||||
fn estimate_keys(db: &DB, cf_name: &str) -> Result<Option<u64>, Box<dyn std::error::Error>> {
|
||||
let cf = db
|
||||
.cf_handle(cf_name)
|
||||
.ok_or_else(|| format!("missing column family: {cf_name}"))?;
|
||||
Ok(db.property_int_value_cf(cf, "rocksdb.estimate-num-keys")?)
|
||||
}
|
||||
|
||||
fn exact_keys(db: &DB, cf_name: &str) -> Result<u64, Box<dyn std::error::Error>> {
|
||||
let cf = db
|
||||
.cf_handle(cf_name)
|
||||
.ok_or_else(|| format!("missing column family: {cf_name}"))?;
|
||||
let mode = IteratorMode::Start;
|
||||
let mut count = 0u64;
|
||||
for res in db.iterator_cf(cf, mode) {
|
||||
res?;
|
||||
count += 1;
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
if argv.iter().any(|a| a == "--help" || a == "-h") {
|
||||
print!("{}", usage());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut db_path: Option<PathBuf> = None;
|
||||
let mut exact = false;
|
||||
let mut i = 1usize;
|
||||
while i < argv.len() {
|
||||
match argv[i].as_str() {
|
||||
"--db" => {
|
||||
i += 1;
|
||||
let v = argv.get(i).ok_or("--db requires a value")?;
|
||||
db_path = Some(PathBuf::from(v));
|
||||
}
|
||||
"--exact" => exact = true,
|
||||
other => return Err(format!("unknown argument: {other}\n\n{}", usage()).into()),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let db_path = db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?;
|
||||
|
||||
let mut opts = Options::default();
|
||||
opts.create_if_missing(false);
|
||||
opts.create_missing_column_families(false);
|
||||
|
||||
let db = DB::open_cf_descriptors(&opts, &db_path, cf_descriptors())?;
|
||||
|
||||
let cf_names = [
|
||||
CF_RAW_OBJECTS,
|
||||
CF_FETCH_CACHE_PP,
|
||||
CF_RRDP_STATE,
|
||||
CF_RRDP_OBJECT_INDEX,
|
||||
];
|
||||
|
||||
println!("db={}", db_path.display());
|
||||
println!("mode={}", if exact { "exact" } else { "estimate" });
|
||||
|
||||
let mut total: u64 = 0;
|
||||
for name in cf_names {
|
||||
let n = if exact {
|
||||
exact_keys(&db, name)?
|
||||
} else {
|
||||
estimate_keys(&db, name)?.unwrap_or(0)
|
||||
};
|
||||
total = total.saturating_add(n);
|
||||
println!("{name}={n}");
|
||||
}
|
||||
println!("total={total}");
|
||||
|
||||
// Also print # of SST files (useful sanity signal).
|
||||
let live = db.live_files()?;
|
||||
println!("sst_files={}", live.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
12
src/bin/rpki.rs
Normal file
12
src/bin/rpki.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
fn main() {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
if let Err(e) = rpki::cli::run(&argv) {
|
||||
// `parse_args` uses `Err(usage())` for `--help`, so treat it as success.
|
||||
if argv.iter().any(|a| a == "--help" || a == "-h") {
|
||||
println!("{e}");
|
||||
return;
|
||||
}
|
||||
eprintln!("{e}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
85
src/bin/rrdp_state_dump.rs
Normal file
85
src/bin/rrdp_state_dump.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rocksdb::{ColumnFamilyDescriptor, DB, IteratorMode, Options};
|
||||
|
||||
fn enable_blobdb_if_supported(opts: &mut Options) {
|
||||
// Keep this in sync with `rpki::storage`:
|
||||
// blob files are CF-level options; readers should open CFs with blob enabled too.
|
||||
#[allow(dead_code)]
|
||||
fn _set(opts: &mut Options) {
|
||||
opts.set_enable_blob_files(true);
|
||||
opts.set_min_blob_size(1024);
|
||||
}
|
||||
_set(opts);
|
||||
}
|
||||
|
||||
fn usage() -> String {
|
||||
let bin = "rrdp_state_dump";
|
||||
format!(
|
||||
"\
|
||||
Usage:
|
||||
{bin} --db <path>
|
||||
|
||||
Options:
|
||||
--db <path> RocksDB directory
|
||||
--help Show this help
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
if argv.iter().any(|a| a == "--help" || a == "-h") {
|
||||
print!("{}", usage());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut db_path: Option<PathBuf> = None;
|
||||
let mut i = 1usize;
|
||||
while i < argv.len() {
|
||||
match argv[i].as_str() {
|
||||
"--db" => {
|
||||
i += 1;
|
||||
let v = argv.get(i).ok_or("--db requires a value")?;
|
||||
db_path = Some(PathBuf::from(v));
|
||||
}
|
||||
other => return Err(format!("unknown argument: {other}\n\n{}", usage()).into()),
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let db_path = db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?;
|
||||
|
||||
let mut opts = Options::default();
|
||||
opts.create_if_missing(false);
|
||||
opts.create_missing_column_families(false);
|
||||
|
||||
// Open only the column families we need.
|
||||
let mut cf_opts = Options::default();
|
||||
enable_blobdb_if_supported(&mut cf_opts);
|
||||
let cfs = vec![
|
||||
ColumnFamilyDescriptor::new("raw_objects", cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new("fetch_cache_pp", cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new("rrdp_state", cf_opts.clone()),
|
||||
ColumnFamilyDescriptor::new("rrdp_object_index", cf_opts),
|
||||
];
|
||||
let db = DB::open_cf_descriptors(&opts, &db_path, cfs)?;
|
||||
let cf = db
|
||||
.cf_handle("rrdp_state")
|
||||
.ok_or("missing column family: rrdp_state")?;
|
||||
|
||||
let mut out: Vec<(String, u64, String)> = Vec::new();
|
||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
||||
let (k, v) = res?;
|
||||
let k = String::from_utf8_lossy(&k).to_string();
|
||||
let st = rpki::sync::rrdp::RrdpState::decode(&v)
|
||||
.map_err(|e| format!("decode rrdp_state failed for {k}: {e}"))?;
|
||||
out.push((k, st.serial, st.session_id));
|
||||
}
|
||||
|
||||
out.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (k, serial, session) in out {
|
||||
println!("{k}\t{serial}\t{session}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
# rtr_debug_client
|
||||
|
||||
`rtr_debug_client` 是用于 RTR 协议联调的命令行调试客户端,支持 `TCP`、`TLS`、`SSH` 三种传输。
|
||||
|
||||
它用于:
|
||||
- 手动发送 `Reset Query`、`Serial Query`
|
||||
- 持续接收并打印服务端 PDU
|
||||
- 观察 `session_id`、`serial`、`EndOfData` timing hint、`ErrorReport` 等状态变化
|
||||
|
||||
## 构建
|
||||
|
||||
```sh
|
||||
cargo build --bin rtr_debug_client
|
||||
```
|
||||
|
||||
## 基本用法
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
||||
```
|
||||
|
||||
默认值:
|
||||
- `addr`: `127.0.0.1:323`
|
||||
- `version`: `1`
|
||||
- `mode`: `reset`
|
||||
- `--timeout`: `30`
|
||||
- `--poll`: `600`
|
||||
|
||||
## TCP 用法
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
||||
```
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
||||
```
|
||||
|
||||
## TLS 用法
|
||||
|
||||
仅校验服务端证书:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:324 1 reset \
|
||||
--tls \
|
||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
||||
--server-name localhost
|
||||
```
|
||||
|
||||
双向 TLS:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:324 1 reset \
|
||||
--tls \
|
||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
||||
--server-name localhost \
|
||||
--client-cert tests/fixtures/tls/client-good.crt \
|
||||
--client-key tests/fixtures/tls/client-good.key
|
||||
```
|
||||
|
||||
## SSH 用法(按 draft-ietf-sidrops-8210bis-25)
|
||||
|
||||
`rtr_debug_client --ssh` 采用以下流程:
|
||||
- SSHv2 连接
|
||||
- `session` channel
|
||||
- 请求 `subsystem`,默认 `rpki-rtr`
|
||||
- 使用 `publickey` 认证
|
||||
- 强制服务端 host key 校验(`known_hosts` 或 pinned server key 二选一)
|
||||
|
||||
### 1. 使用 known_hosts 校验服务端
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:22 1 reset \
|
||||
--ssh \
|
||||
--ssh-user rpki-rtr \
|
||||
--ssh-key certs/rtr-client.key \
|
||||
--ssh-known-hosts certs/known_hosts
|
||||
```
|
||||
|
||||
### 2. 使用固定服务端公钥校验
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:22 1 reset \
|
||||
--ssh \
|
||||
--ssh-user rpki-rtr \
|
||||
--ssh-key certs/rtr-client.key \
|
||||
--ssh-server-key certs/ssh_host_ed25519_key.pub
|
||||
```
|
||||
|
||||
### 3. 自定义 subsystem 名称
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:22 1 reset \
|
||||
--ssh \
|
||||
--ssh-user rpki-rtr \
|
||||
--ssh-key certs/rtr-client.key \
|
||||
--ssh-known-hosts certs/known_hosts \
|
||||
--ssh-subsystem rpki-rtr
|
||||
```
|
||||
|
||||
## 参数说明
|
||||
|
||||
通用参数:
|
||||
- `--timeout <secs>`:读取 PDU 超时时间(秒)
|
||||
- `--poll <secs>`:默认自动轮询间隔(秒)
|
||||
- `--keep-after-error`:收到 `ErrorReport` 后不暂停自动轮询
|
||||
- `--summary-only`:仅打印摘要,抑制 payload PDU 详细内容
|
||||
|
||||
TLS 参数:
|
||||
- `--tls`
|
||||
- `--ca-cert <path>`
|
||||
- `--server-name <name>`
|
||||
- `--client-cert <path>`
|
||||
- `--client-key <path>`
|
||||
|
||||
SSH 参数:
|
||||
- `--ssh`
|
||||
- `--ssh-user <name>`
|
||||
- `--ssh-key <path>`(OpenSSH 私钥)
|
||||
- `--ssh-subsystem <name>`(默认 `rpki-rtr`)
|
||||
- `--ssh-known-hosts <path>` 或 `--ssh-server-key <path>`(二选一,必须提供)
|
||||
|
||||
## SSH 连通性测试建议
|
||||
|
||||
如果你已经在 Docker 中启动了支持 SSH 的 RTR server,可按以下方式验证:
|
||||
|
||||
1. 先用 `ssh` 命令确认认证与 host key 配置正确(可连通)。
|
||||
2. 再用 `rtr_debug_client --ssh` 发起连接并发送 `reset`。
|
||||
3. 观察是否收到 `Cache Response` 和 `EndOfData`。
|
||||
|
||||
如果 `rtr_debug_client` 报 `failed to request SSH subsystem 'rpki-rtr'`,通常表示服务端未开启对应 subsystem 名称,或名称不一致。
|
||||
|
||||
## 运行时交互命令
|
||||
|
||||
客户端启动后可在标准输入中使用:
|
||||
- `help`
|
||||
- `state`
|
||||
- `version` / `version <n>`
|
||||
- `reset`
|
||||
- `serial` / `serial <sid> <serial>`
|
||||
- `timeout` / `timeout <secs>`
|
||||
- `poll` / `poll <secs>` / `poll pause` / `poll resume`
|
||||
- `keep-after-error`
|
||||
- `output` / `output verbose` / `output summary`
|
||||
- `quit`
|
||||
|
||||
## SSH Password Auth (Added)
|
||||
|
||||
`rtr_debug_client` now supports password auth in SSH mode.
|
||||
|
||||
- Use exactly one auth option in SSH mode:
|
||||
- `--ssh-key <path>`
|
||||
- `--ssh-password <value>`
|
||||
- Host key verification is still required:
|
||||
- `--ssh-known-hosts <path>` or `--ssh-server-key <path>`
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:22 1 reset \
|
||||
--ssh \
|
||||
--ssh-user rpki-rtr \
|
||||
--ssh-password 'your-password' \
|
||||
--ssh-server-key certs/ssh_host_rsa_key.pub
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user