Compare commits
42 Commits
cf764c35bb
...
dev_1.2_xu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6597476e | ||
|
|
5fb79a7c8d | ||
|
|
490b79f22c | ||
|
|
e2bdcd3820 | ||
|
|
b5cbc7fbee | ||
|
|
cc3518897f | ||
|
|
b105b3b7d0 | ||
|
|
303bdffd97 | ||
|
|
3c61fbf152 | ||
|
|
b70bad4721 | ||
|
|
fa19fc8722 | ||
|
|
13f29843db | ||
|
|
cdf9372929 | ||
|
|
a11f2bc864 | ||
|
|
ad76745619 | ||
|
|
9dc69e5f31 | ||
|
|
ff711a2fe1 | ||
|
|
4c6b441753 | ||
|
|
a4d674190a | ||
|
|
b79e25ffea | ||
|
|
84866c1d86 | ||
|
|
ddeff71618 | ||
|
|
ca0cf4800a | ||
|
|
897d168ceb | ||
|
|
19225edaa7 | ||
|
|
6e4b59a208 | ||
|
|
88c11e7a97 | ||
|
|
8a65f639cb | ||
|
|
cef9495534 | ||
|
|
b60d579a38 | ||
|
|
99250f8aa9 | ||
|
|
17c1a02c90 | ||
|
|
c1d3112a45 | ||
|
|
23cdad095d | ||
|
|
b483c2af8b | ||
|
|
cd0330e8ae | ||
|
|
8d6173f605 | ||
|
|
7432fbcac4 | ||
|
|
03c0ab0ec7 | ||
|
|
9cbea4e2d0 | ||
|
|
251dea8e5e | ||
|
|
f5b7da921a |
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.idea
|
||||||
|
target
|
||||||
|
rtr-db
|
||||||
|
tests
|
||||||
|
specs
|
||||||
|
scripts
|
||||||
|
README.md
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
target/
|
target/
|
||||||
Cargo.lock
|
rtr-db/
|
||||||
perf.*
|
.idea/
|
||||||
|
logs/
|
||||||
|
report/
|
||||||
|
|||||||
3528
Cargo.lock
generated
Normal file
3528
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
51
Cargo.toml
51
Cargo.toml
@@ -3,33 +3,38 @@ name = "rpki"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[[bin]]
|
||||||
default = ["full"]
|
name = "rpki_rtr"
|
||||||
# Full build used by the main RP implementation (includes RocksDB-backed storage).
|
path = "src/main_rtr.rs"
|
||||||
full = ["dep:rocksdb"]
|
|
||||||
profile = ["dep:pprof", "dep:flate2"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
asn1-rs = "0.7.1"
|
der-parser = "10.0.0"
|
||||||
der-parser = { version = "10.0.0", features = ["serialize"] }
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
base64 = "0.22.1"
|
|
||||||
sha2 = "0.10.8"
|
|
||||||
thiserror = "2.0.18"
|
thiserror = "2.0.18"
|
||||||
time = "0.3.45"
|
time = "0.3.45"
|
||||||
ring = "0.17.14"
|
|
||||||
x509-parser = { version = "0.18.0", features = ["verify"] }
|
x509-parser = { version = "0.18.0", features = ["verify"] }
|
||||||
url = "2.5.8"
|
url = "2.5.8"
|
||||||
serde = { version = "1.0.218", features = ["derive"] }
|
asn1-rs = "0.7.1"
|
||||||
serde_json = "1.0.140"
|
asn1-rs-derive = "0.6.0"
|
||||||
toml = "0.8.20"
|
asn1 = "0.23.0"
|
||||||
rocksdb = { version = "0.22.0", optional = true, default-features = false, features = ["lz4"] }
|
arc-swap = "1.7.0"
|
||||||
serde_cbor = "0.11.2"
|
chrono = { version = "0.4.44", features = ["serde"] }
|
||||||
roxmltree = "0.20.0"
|
chrono-tz = "0.10"
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
bytes = "1.11.1"
|
||||||
reqwest = { version = "0.12.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
tokio = { version = "1.49.0", features = ["full"] }
|
||||||
pprof = { version = "0.14.1", optional = true, features = ["flamegraph", "prost-codec"] }
|
rand = "0.10.0"
|
||||||
flate2 = { version = "1.0.35", optional = true }
|
rocksdb = { version = "0.21.0", default-features = false }
|
||||||
|
serde = { version = "1", features = ["derive", "rc"] }
|
||||||
[dev-dependencies]
|
serde_json = "1"
|
||||||
tempfile = "3.16.0"
|
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"] }
|
||||||
|
|||||||
238
README.client.md
Normal file
238
README.client.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# 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,28 +1,373 @@
|
|||||||
|
# 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。
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
- [协议参考](#协议参考)
|
||||||
|
- [项目结构](#项目结构)
|
||||||
|
- [构建与测试](#构建与测试)
|
||||||
|
- [数据输入](#数据输入)
|
||||||
|
- [快速启动](#快速启动)
|
||||||
|
- [前置数据生成](#前置数据生成)
|
||||||
|
- [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
|
||||||
|
|
||||||
# 查看输出
|
|
||||||
cargo test -- --nocapture
|
|
||||||
```
|
```
|
||||||
|
|
||||||
# 覆盖率(cargo-llvm-cov)
|
服务端 binary 名称是 `rpki_rtr`:
|
||||||
|
|
||||||
安装工具:
|
```bash
|
||||||
|
cargo run --bin rpki_rtr
|
||||||
```
|
|
||||||
rustup component add llvm-tools-preview
|
|
||||||
cargo install cargo-llvm-cov --locked
|
|
||||||
```
|
```
|
||||||
|
|
||||||
统计行覆盖率并要求 >=90%:
|
默认配置会监听 `0.0.0.0:323`,从 `./data` 查找 CCR 文件,并使用 `./rtr-db` 保存 RocksDB 数据。启动前需要确保 CCR 目录里存在可解析的 `.ccr` 文件。
|
||||||
|
|
||||||
```
|
## 数据输入
|
||||||
./scripts/coverage.sh
|
|
||||||
# 或
|
服务端启动和刷新时会读取 `RPKI_RTR_CCR_DIR` 指向的目录:
|
||||||
cargo llvm-cov --fail-under-lines 90
|
|
||||||
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如需覆盖已有 `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`。
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "ours-manifest-bench"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rpki = { path = "../..", default-features = false }
|
|
||||||
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "routinator-object-bench"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
rpki = { version = "=0.19.1", features = ["repository"] }
|
|
||||||
@@ -1,552 +0,0 @@
|
|||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
data/example.slurm
Normal file
23
data/example.slurm
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
33
deploy/bird/.env
Normal file
33
deploy/bird/.env
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 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
|
||||||
56
deploy/bird/Dockerfile
Normal file
56
deploy/bird/Dockerfile
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
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"]
|
||||||
90
deploy/bird/README.md
Normal file
90
deploy/bird/README.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# 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.
|
||||||
80
deploy/bird/README.zh.md
Normal file
80
deploy/bird/README.zh.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 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 容器名或宿主机地址。
|
||||||
31
deploy/bird/_debug_ssh.conf
Normal file
31
deploy/bird/_debug_ssh.conf
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
28
deploy/bird/bird.conf
Normal file
28
deploy/bird/bird.conf
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
28
deploy/bird/bird.conf.example
Normal file
28
deploy/bird/bird.conf.example
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
30
deploy/bird/bird.conf.ssh.example
Normal file
30
deploy/bird/bird.conf.ssh.example
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
30
deploy/bird/bird.conf.ssh.template
Normal file
30
deploy/bird/bird.conf.ssh.template
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
28
deploy/bird/bird.conf.template
Normal file
28
deploy/bird/bird.conf.template
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
17
deploy/bird/docker-compose.ssh.yml
Normal file
17
deploy/bird/docker-compose.ssh.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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
|
||||||
41
deploy/bird/docker-compose.yml
Normal file
41
deploy/bird/docker-compose.yml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
218
deploy/bird/entrypoint.sh
Normal file
218
deploy/bird/entrypoint.sh
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
#!/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"
|
||||||
32
deploy/client/.env
Normal file
32
deploy/client/.env
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# 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=
|
||||||
26
deploy/client/Dockerfile
Normal file
26
deploy/client/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
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"]
|
||||||
237
deploy/client/build-run.sh
Normal file
237
deploy/client/build-run.sh
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
#!/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
|
||||||
52
deploy/client/docker-compose.clients.yml
Normal file
52
deploy/client/docker-compose.clients.yml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
36
deploy/client/docker-compose.ssh.password.yml
Normal file
36
deploy/client/docker-compose.ssh.password.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
36
deploy/client/docker-compose.ssh.yml
Normal file
36
deploy/client/docker-compose.ssh.yml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
19
deploy/client/docker-compose.tcp.yml
Normal file
19
deploy/client/docker-compose.tcp.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
38
deploy/client/docker-compose.tls.yml
Normal file
38
deploy/client/docker-compose.tls.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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
|
||||||
19
deploy/client/docker-compose.yml
Normal file
19
deploy/client/docker-compose.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
10
deploy/client/entrypoint.sh
Normal file
10
deploy/client/entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/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"
|
||||||
4
deploy/frr/.env
Normal file
4
deploy/frr/.env
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 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
|
||||||
78
deploy/frr/README.md
Normal file
78
deploy/frr/README.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# 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`.
|
||||||
83
deploy/frr/README.zh.md
Normal file
83
deploy/frr/README.zh.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 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` 控制
|
||||||
7
deploy/frr/daemons.example
Normal file
7
deploy/frr/daemons.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Minimal FRR daemons config for RPKI testing
|
||||||
|
|
||||||
|
zebra=yes
|
||||||
|
bgpd=yes
|
||||||
|
|
||||||
|
# Enable bgpd RPKI module
|
||||||
|
bgpd_options=" -A 127.0.0.1 -M rpki"
|
||||||
16
deploy/frr/docker-compose.yml
Normal file
16
deploy/frr/docker-compose.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
24
deploy/frr/entrypoint.sh
Normal file
24
deploy/frr/entrypoint.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/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
|
||||||
22
deploy/frr/frr.conf.example
Normal file
22
deploy/frr/frr.conf.example
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
22
deploy/frr/frr.conf.template
Normal file
22
deploy/frr/frr.conf.template
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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
|
||||||
49
deploy/server/.env
Normal file
49
deploy/server/.env
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 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
|
||||||
99
deploy/server/DEPLOYMENT.md
Normal file
99
deploy/server/DEPLOYMENT.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# 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.
|
||||||
85
deploy/server/Dockerfile
Normal file
85
deploy/server/Dockerfile
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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"]
|
||||||
215
deploy/server/build-run.sh
Normal file
215
deploy/server/build-run.sh
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
#!/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
|
||||||
54
deploy/server/docker-compose.ssh.yml
Normal file
54
deploy/server/docker-compose.ssh.yml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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
|
||||||
44
deploy/server/docker-compose.tcp.yml
Normal file
44
deploy/server/docker-compose.tcp.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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
|
||||||
48
deploy/server/docker-compose.tls.yml
Normal file
48
deploy/server/docker-compose.tls.yml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
58
deploy/server/docker-compose.yml
Normal file
58
deploy/server/docker-compose.yml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
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
|
||||||
10
deploy/server/entrypoint.sh
Normal file
10
deploy/server/entrypoint.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/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"
|
||||||
310
docs/rtr-admin-api.md
Normal file
310
docs/rtr-admin-api.md
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
# 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` | 请求体超过限制。 |
|
||||||
203
scripts/00_0_install.sh
Normal file
203
scripts/00_0_install.sh
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
#!/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"
|
||||||
155
scripts/00_1_pre_check.sh
Normal file
155
scripts/00_1_pre_check.sh
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/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"
|
||||||
54
scripts/00_2_generate_ccr.sh
Normal file
54
scripts/00_2_generate_ccr.sh
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/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
|
||||||
96
scripts/00_2_pre_check_prepare.sh
Normal file
96
scripts/00_2_pre_check_prepare.sh
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#!/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
|
||||||
56
scripts/00_3_pre_check_cleanup.sh
Normal file
56
scripts/00_3_pre_check_cleanup.sh
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/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
|
||||||
221
scripts/01_start.sh
Normal file
221
scripts/01_start.sh
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
#!/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"
|
||||||
184
scripts/02_self_check.sh
Normal file
184
scripts/02_self_check.sh
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/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"
|
||||||
92
scripts/03_shutdown.sh
Normal file
92
scripts/03_shutdown.sh
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/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"
|
||||||
152
scripts/04_cleanup.sh
Normal file
152
scripts/04_cleanup.sh
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/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"
|
||||||
303
scripts/10_generate_certs.sh
Normal file
303
scripts/10_generate_certs.sh
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#!/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
|
||||||
136
scripts/11_start_client.sh
Normal file
136
scripts/11_start_client.sh
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
#!/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"
|
||||||
82
scripts/12_shutdown_client.sh
Normal file
82
scripts/12_shutdown_client.sh
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/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"
|
||||||
218
scripts/README.md
Normal file
218
scripts/README.md
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
# 脚本说明
|
||||||
|
|
||||||
|
`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。
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#!/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/"
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# 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 legacy session/serial dump (`rrdp_state_dump --view legacy-state`)
|
|
||||||
|
|
||||||
### `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 --view legacy-state` 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).
|
|
||||||
|
|
||||||
The standalone `rrdp_state_dump` tool also supports `source`, `members`, `owners`, and `all` views.
|
|
||||||
The manual sync scripts intentionally call `--view legacy-state` so delta analysis keeps using a stable session/serial TSV format.
|
|
||||||
|
|
||||||
## 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
|
|
||||||
```
|
|
||||||
@@ -1,543 +0,0 @@
|
|||||||
#!/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 --view legacy-state 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 (legacy-state) ==" >&2
|
|
||||||
state_start_s="$(date +%s)"
|
|
||||||
cargo run --release --bin rrdp_state_dump -- --db "$BASE_DB_DIR" --view legacy-state >"$BASE_RRDP_STATE_TSV"
|
|
||||||
cargo run --release --bin rrdp_state_dump -- --db "$DELTA_DB_DIR" --view legacy-state >"$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 from `rrdp_state_dump --view legacy-state`:
|
|
||||||
# [legacy-state]
|
|
||||||
# notify_uri serial session_id
|
|
||||||
# <notify_uri> <serial> <session_id>
|
|
||||||
out = {}
|
|
||||||
for line in p.read_text(encoding="utf-8").splitlines():
|
|
||||||
line = line.strip()
|
|
||||||
if not line or line.startswith("["):
|
|
||||||
continue
|
|
||||||
if line == "notify_uri serial session_id":
|
|
||||||
continue
|
|
||||||
parts = line.split(" ")
|
|
||||||
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") != "vcir_current_instance":
|
|
||||||
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:
|
|
||||||
ordered = [
|
|
||||||
"mode",
|
|
||||||
"repository_view",
|
|
||||||
"raw_by_hash",
|
|
||||||
"vcir",
|
|
||||||
"audit_rule_index",
|
|
||||||
"rrdp_source",
|
|
||||||
"rrdp_source_member",
|
|
||||||
"rrdp_uri_owner",
|
|
||||||
"rrdp_state",
|
|
||||||
"raw_objects",
|
|
||||||
"rrdp_object_index",
|
|
||||||
"group_current_repository_view",
|
|
||||||
"group_current_validation_state",
|
|
||||||
"group_current_rrdp_state",
|
|
||||||
"group_legacy_compatibility",
|
|
||||||
"total",
|
|
||||||
"sst_files",
|
|
||||||
]
|
|
||||||
out = []
|
|
||||||
seen = set()
|
|
||||||
for k in ordered:
|
|
||||||
if k in db:
|
|
||||||
out.append(f"- `{k}={db[k]}`")
|
|
||||||
seen.add(k)
|
|
||||||
for k in sorted(set(db) - seen):
|
|
||||||
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 / VCIR 当前实例缓存复用情况\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`(未使用 VCIR 当前实例缓存复用)\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=vcir_current_instance`", base_cache_reasons))
|
|
||||||
lines.append(render_cache_reasons("- delta `source=vcir_current_instance`", 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
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
#!/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 --view legacy-state 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 (legacy-state) ==" >&2
|
|
||||||
state_start_s="$(date +%s)"
|
|
||||||
cargo run --release --bin rrdp_state_dump -- --db "$DB_DIR" --view legacy-state >"$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
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
#!/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,8 +182,6 @@ 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.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.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.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.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.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 |
|
| `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,94 +1,36 @@
|
|||||||
# 01. TAL(Trust Anchor Locator)
|
# 01. Trust Anchor Locator (TAL)
|
||||||
|
|
||||||
## 1.1 对象定位
|
## 1.1 对象定位
|
||||||
|
TAL是一个数据格式/配置文件,目的是告诉RP信任锚的公钥是什么,以及相关对象可以从哪里获取。
|
||||||
|
|
||||||
TAL(Trust Anchor Locator)用于向 RP 提供:
|
## 1.2 数据格式 (RFC 8630 §2.2)
|
||||||
|
TAL是一个配置文件,格式定义如下:
|
||||||
1) 可检索“当前 TA 证书”的一个或多个 URI;以及
|
```
|
||||||
2) 该 TA 证书的 `subjectPublicKeyInfo`(SPKI)期望值(用于绑定/防替换)。
|
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
|
||||||
RFC 8630 §2;RFC 8630 §2.3。
|
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,
|
||||||
## 1.2 原始载体与编码
|
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,
|
||||||
- 载体:文本文件(ASCII/UTF-8 兼容的行文本)。
|
line breaks MAY be inserted into the base64-encoded string.
|
||||||
- 行结束:允许 `CRLF` 或 `LF`。RFC 8630 §2.2。
|
Note that line breaks in this file can use either "<CRLF>" or "<LF>".
|
||||||
- 结构:`[可选注释区] + 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 }
|
|
||||||
```
|
```
|
||||||
|
|
||||||
其中 `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.3 解析规则(语义层)
|
### 1.3.1 TAL
|
||||||
|
|
||||||
输入:`TalFileBytes: bytes`。
|
| 字段 | 类型 | 语义 | 约束/解析规则 | 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 |
|
||||||
|
|
||||||
解析步骤:
|
|
||||||
|
|
||||||
1) 按 `LF` / `CRLF` 识别行。RFC 8630 §2.2。
|
### 1.3.2 TalUri
|
||||||
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。
|
|
||||||
|
|
||||||
URI 解析与约束:
|
| 字段 | 类型 | 语义 | 约束/解析规则 | RFC 引用 |
|
||||||
|
|-------|--------|---------|---------|---------------|
|
||||||
|
| Rsync | String | rsync地址 | | RFC 8630 §2.1 |
|
||||||
|
| Https | String | https地址 | | RFC 8630 §2.1 |
|
||||||
|
|
||||||
- `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
Normal file
121
specs/02_ta.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# 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. 更新本地存储库缓存。
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
# 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
Normal file
314
specs/03_rc.md
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
|
|
||||||
@@ -1,460 +0,0 @@
|
|||||||
# 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。
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
# 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)。
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
# 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
159
specs/07_roa.md
@@ -1,159 +0,0 @@
|
|||||||
# 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
100
specs/08_aspa.md
@@ -1,100 +0,0 @@
|
|||||||
# 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。
|
|
||||||
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# 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)。
|
|
||||||
|
|
||||||
186
specs/10_slurm.md
Normal file
186
specs/10_slurm.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 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
Normal file
158
specs/11_rtr.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# 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
|
||||||
File diff suppressed because it is too large
Load Diff
16200
specs/delta.excalidraw
16200
specs/delta.excalidraw
File diff suppressed because it is too large
Load Diff
BIN
specs/img/img.png
Normal file
BIN
specs/img/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -1 +0,0 @@
|
|||||||
pub mod timing;
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
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
224
src/audit.rs
@@ -1,224 +0,0 @@
|
|||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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,581 +0,0 @@
|
|||||||
use crate::storage::{
|
|
||||||
AuditRuleIndexEntry, AuditRuleKind, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
|
|
||||||
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirLocalOutput,
|
|
||||||
VcirOutputType,
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum AuditTraceError {
|
|
||||||
#[error("storage error: {0}")]
|
|
||||||
Storage(#[from] crate::storage::StorageError),
|
|
||||||
|
|
||||||
#[error("audit rule index points to missing VCIR: {manifest_rsync_uri}")]
|
|
||||||
MissingVcir { manifest_rsync_uri: String },
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"audit rule index points to missing local output: rule_hash={rule_hash}, output_id={output_id}, manifest={manifest_rsync_uri}"
|
|
||||||
)]
|
|
||||||
MissingLocalOutput {
|
|
||||||
rule_hash: String,
|
|
||||||
output_id: String,
|
|
||||||
manifest_rsync_uri: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("detected VCIR parent cycle at {manifest_rsync_uri}")]
|
|
||||||
ParentCycle { manifest_rsync_uri: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceRawRef {
|
|
||||||
pub sha256_hex: String,
|
|
||||||
pub raw_present: bool,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
|
||||||
pub origin_uris: Vec<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_type: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub byte_len: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceArtifact {
|
|
||||||
pub artifact_role: VcirArtifactRole,
|
|
||||||
pub artifact_kind: VcirArtifactKind,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub uri: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub object_type: Option<String>,
|
|
||||||
pub validation_status: VcirArtifactValidationStatus,
|
|
||||||
pub raw: AuditTraceRawRef,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceChainNode {
|
|
||||||
pub manifest_rsync_uri: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub parent_manifest_rsync_uri: Option<String>,
|
|
||||||
pub tal_id: String,
|
|
||||||
pub ca_subject_name: String,
|
|
||||||
pub ca_ski: String,
|
|
||||||
pub issuer_ski: String,
|
|
||||||
pub current_manifest_rsync_uri: String,
|
|
||||||
pub current_crl_rsync_uri: String,
|
|
||||||
pub last_successful_validation_time_rfc3339_utc: String,
|
|
||||||
pub local_output_count: usize,
|
|
||||||
pub child_count: usize,
|
|
||||||
pub related_artifacts: Vec<AuditTraceArtifact>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditTraceResolvedOutput {
|
|
||||||
pub output_id: String,
|
|
||||||
pub output_type: VcirOutputType,
|
|
||||||
pub rule_hash: String,
|
|
||||||
pub source_object_uri: String,
|
|
||||||
pub source_object_type: String,
|
|
||||||
pub source_object_hash: String,
|
|
||||||
pub source_ee_cert_hash: String,
|
|
||||||
pub item_effective_until_rfc3339_utc: String,
|
|
||||||
pub payload_json: String,
|
|
||||||
pub validation_path_hint: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct AuditRuleTrace {
|
|
||||||
pub rule: AuditRuleIndexEntry,
|
|
||||||
pub resolved_output: AuditTraceResolvedOutput,
|
|
||||||
pub source_object_raw: AuditTraceRawRef,
|
|
||||||
pub source_ee_cert_raw: AuditTraceRawRef,
|
|
||||||
pub chain_leaf_to_root: Vec<AuditTraceChainNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trace_rule_to_root(
|
|
||||||
store: &RocksStore,
|
|
||||||
kind: AuditRuleKind,
|
|
||||||
rule_hash: &str,
|
|
||||||
) -> Result<Option<AuditRuleTrace>, AuditTraceError> {
|
|
||||||
let Some(rule) = store.get_audit_rule_index_entry(kind, rule_hash)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(leaf_vcir) = store.get_vcir(&rule.manifest_rsync_uri)? else {
|
|
||||||
return Err(AuditTraceError::MissingVcir {
|
|
||||||
manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(local_output) = leaf_vcir
|
|
||||||
.local_outputs
|
|
||||||
.iter()
|
|
||||||
.find(|output| output.output_id == rule.output_id && output.rule_hash == rule.rule_hash)
|
|
||||||
.or_else(|| {
|
|
||||||
leaf_vcir
|
|
||||||
.local_outputs
|
|
||||||
.iter()
|
|
||||||
.find(|output| output.rule_hash == rule.rule_hash)
|
|
||||||
})
|
|
||||||
.cloned()
|
|
||||||
else {
|
|
||||||
return Err(AuditTraceError::MissingLocalOutput {
|
|
||||||
rule_hash: rule.rule_hash.clone(),
|
|
||||||
output_id: rule.output_id.clone(),
|
|
||||||
manifest_rsync_uri: rule.manifest_rsync_uri.clone(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let chain = trace_vcir_chain_to_root(store, &leaf_vcir.manifest_rsync_uri)?
|
|
||||||
.expect("leaf VCIR already loaded must exist");
|
|
||||||
|
|
||||||
Ok(Some(AuditRuleTrace {
|
|
||||||
rule,
|
|
||||||
resolved_output: resolved_output_from_local(&local_output),
|
|
||||||
source_object_raw: resolve_raw_ref(store, &local_output.source_object_hash)?,
|
|
||||||
source_ee_cert_raw: resolve_raw_ref(store, &local_output.source_ee_cert_hash)?,
|
|
||||||
chain_leaf_to_root: chain,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trace_vcir_chain_to_root(
|
|
||||||
store: &RocksStore,
|
|
||||||
manifest_rsync_uri: &str,
|
|
||||||
) -> Result<Option<Vec<AuditTraceChainNode>>, AuditTraceError> {
|
|
||||||
let Some(mut current) = store.get_vcir(manifest_rsync_uri)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut seen = HashSet::new();
|
|
||||||
let mut chain = Vec::new();
|
|
||||||
loop {
|
|
||||||
if !seen.insert(current.manifest_rsync_uri.clone()) {
|
|
||||||
return Err(AuditTraceError::ParentCycle {
|
|
||||||
manifest_rsync_uri: current.manifest_rsync_uri,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let parent = current.parent_manifest_rsync_uri.clone();
|
|
||||||
chain.push(trace_chain_node(store, ¤t)?);
|
|
||||||
let Some(parent_manifest_rsync_uri) = parent else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Some(parent_vcir) = store.get_vcir(&parent_manifest_rsync_uri)? else {
|
|
||||||
return Err(AuditTraceError::MissingVcir {
|
|
||||||
manifest_rsync_uri: parent_manifest_rsync_uri,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
current = parent_vcir;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(chain))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn trace_chain_node(
|
|
||||||
store: &RocksStore,
|
|
||||||
vcir: &ValidatedCaInstanceResult,
|
|
||||||
) -> Result<AuditTraceChainNode, AuditTraceError> {
|
|
||||||
let mut related_artifacts = Vec::with_capacity(vcir.related_artifacts.len());
|
|
||||||
for artifact in &vcir.related_artifacts {
|
|
||||||
related_artifacts.push(AuditTraceArtifact {
|
|
||||||
artifact_role: artifact.artifact_role,
|
|
||||||
artifact_kind: artifact.artifact_kind,
|
|
||||||
uri: artifact.uri.clone(),
|
|
||||||
object_type: artifact.object_type.clone(),
|
|
||||||
validation_status: artifact.validation_status,
|
|
||||||
raw: resolve_raw_ref(store, &artifact.sha256)?,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(AuditTraceChainNode {
|
|
||||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
|
||||||
parent_manifest_rsync_uri: vcir.parent_manifest_rsync_uri.clone(),
|
|
||||||
tal_id: vcir.tal_id.clone(),
|
|
||||||
ca_subject_name: vcir.ca_subject_name.clone(),
|
|
||||||
ca_ski: vcir.ca_ski.clone(),
|
|
||||||
issuer_ski: vcir.issuer_ski.clone(),
|
|
||||||
current_manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
|
||||||
current_crl_rsync_uri: vcir.current_crl_rsync_uri.clone(),
|
|
||||||
last_successful_validation_time_rfc3339_utc: vcir
|
|
||||||
.last_successful_validation_time
|
|
||||||
.rfc3339_utc
|
|
||||||
.clone(),
|
|
||||||
local_output_count: vcir.local_outputs.len(),
|
|
||||||
child_count: vcir.child_entries.len(),
|
|
||||||
related_artifacts,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolved_output_from_local(local: &VcirLocalOutput) -> AuditTraceResolvedOutput {
|
|
||||||
AuditTraceResolvedOutput {
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
output_type: local.output_type,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_type: local.source_object_type.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
source_ee_cert_hash: local.source_ee_cert_hash.clone(),
|
|
||||||
item_effective_until_rfc3339_utc: local.item_effective_until.rfc3339_utc.clone(),
|
|
||||||
payload_json: local.payload_json.clone(),
|
|
||||||
validation_path_hint: local.validation_path_hint.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_raw_ref(
|
|
||||||
store: &RocksStore,
|
|
||||||
sha256_hex: &str,
|
|
||||||
) -> Result<AuditTraceRawRef, AuditTraceError> {
|
|
||||||
let raw = store.get_raw_by_hash_entry(sha256_hex)?;
|
|
||||||
Ok(raw_ref_from_entry(sha256_hex, raw.as_ref()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn raw_ref_from_entry(sha256_hex: &str, entry: Option<&RawByHashEntry>) -> AuditTraceRawRef {
|
|
||||||
match entry {
|
|
||||||
Some(entry) => AuditTraceRawRef {
|
|
||||||
sha256_hex: sha256_hex.to_string(),
|
|
||||||
raw_present: true,
|
|
||||||
origin_uris: entry.origin_uris.clone(),
|
|
||||||
object_type: entry.object_type.clone(),
|
|
||||||
byte_len: Some(entry.bytes.len()),
|
|
||||||
},
|
|
||||||
None => AuditTraceRawRef {
|
|
||||||
sha256_hex: sha256_hex.to_string(),
|
|
||||||
raw_present: false,
|
|
||||||
origin_uris: Vec::new(),
|
|
||||||
object_type: None,
|
|
||||||
byte_len: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::audit::sha256_hex;
|
|
||||||
use crate::storage::{
|
|
||||||
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirChildEntry, VcirInstanceGate,
|
|
||||||
VcirRelatedArtifact, VcirSummary,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn sample_vcir(
|
|
||||||
manifest_rsync_uri: &str,
|
|
||||||
parent_manifest_rsync_uri: Option<&str>,
|
|
||||||
tal_id: &str,
|
|
||||||
local_output: Option<VcirLocalOutput>,
|
|
||||||
related_artifacts: Vec<VcirRelatedArtifact>,
|
|
||||||
) -> ValidatedCaInstanceResult {
|
|
||||||
let now = time::OffsetDateTime::now_utc();
|
|
||||||
let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
|
|
||||||
let local_outputs: Vec<VcirLocalOutput> = local_output.into_iter().collect();
|
|
||||||
ValidatedCaInstanceResult {
|
|
||||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
||||||
parent_manifest_rsync_uri: parent_manifest_rsync_uri.map(str::to_string),
|
|
||||||
tal_id: tal_id.to_string(),
|
|
||||||
ca_subject_name: format!("CN={manifest_rsync_uri}"),
|
|
||||||
ca_ski: "11".repeat(20),
|
|
||||||
issuer_ski: "22".repeat(20),
|
|
||||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(now),
|
|
||||||
current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
||||||
current_crl_rsync_uri: manifest_rsync_uri.replace(".mft", ".crl"),
|
|
||||||
validated_manifest_meta: ValidatedManifestMeta {
|
|
||||||
validated_manifest_number: vec![1],
|
|
||||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
|
||||||
validated_manifest_next_update: next.clone(),
|
|
||||||
},
|
|
||||||
instance_gate: VcirInstanceGate {
|
|
||||||
manifest_next_update: next.clone(),
|
|
||||||
current_crl_next_update: next.clone(),
|
|
||||||
self_ca_not_after: PackTime::from_utc_offset_datetime(
|
|
||||||
now + time::Duration::hours(2),
|
|
||||||
),
|
|
||||||
instance_effective_until: next,
|
|
||||||
},
|
|
||||||
child_entries: vec![VcirChildEntry {
|
|
||||||
child_manifest_rsync_uri: "rsync://example.test/child/child.mft".to_string(),
|
|
||||||
child_cert_rsync_uri: "rsync://example.test/parent/child.cer".to_string(),
|
|
||||||
child_cert_hash: sha256_hex(b"child-cert"),
|
|
||||||
child_ski: "33".repeat(20),
|
|
||||||
child_rsync_base_uri: "rsync://example.test/child/".to_string(),
|
|
||||||
child_publication_point_rsync_uri: "rsync://example.test/child/".to_string(),
|
|
||||||
child_rrdp_notification_uri: Some(
|
|
||||||
"https://example.test/child/notify.xml".to_string(),
|
|
||||||
),
|
|
||||||
child_effective_ip_resources: None,
|
|
||||||
child_effective_as_resources: None,
|
|
||||||
accepted_at_validation_time: PackTime::from_utc_offset_datetime(now),
|
|
||||||
}],
|
|
||||||
summary: VcirSummary {
|
|
||||||
local_vrp_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::Vrp)
|
|
||||||
.count() as u32,
|
|
||||||
local_aspa_count: local_outputs
|
|
||||||
.iter()
|
|
||||||
.filter(|output| output.output_type == VcirOutputType::Aspa)
|
|
||||||
.count() as u32,
|
|
||||||
child_count: 1,
|
|
||||||
accepted_object_count: related_artifacts.len() as u32,
|
|
||||||
rejected_object_count: 0,
|
|
||||||
},
|
|
||||||
local_outputs,
|
|
||||||
related_artifacts,
|
|
||||||
audit_summary: VcirAuditSummary {
|
|
||||||
failed_fetch_eligible: true,
|
|
||||||
last_failed_fetch_reason: None,
|
|
||||||
warning_count: 0,
|
|
||||||
audit_flags: Vec::new(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_local_output(manifest_rsync_uri: &str) -> VcirLocalOutput {
|
|
||||||
let now = time::OffsetDateTime::now_utc();
|
|
||||||
VcirLocalOutput {
|
|
||||||
output_id: sha256_hex(b"vrp-output"),
|
|
||||||
output_type: VcirOutputType::Vrp,
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
now + time::Duration::minutes(30),
|
|
||||||
),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
source_object_type: "roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"roa-raw"),
|
|
||||||
source_ee_cert_hash: sha256_hex(b"roa-ee"),
|
|
||||||
payload_json:
|
|
||||||
serde_json::json!({"asn": 64496, "prefix": "203.0.113.0/24", "max_length": 24})
|
|
||||||
.to_string(),
|
|
||||||
rule_hash: sha256_hex(b"roa-rule"),
|
|
||||||
validation_path_hint: vec![
|
|
||||||
manifest_rsync_uri.to_string(),
|
|
||||||
"rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
sha256_hex(b"roa-raw"),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_artifacts(manifest_rsync_uri: &str, roa_hash: &str) -> Vec<VcirRelatedArtifact> {
|
|
||||||
vec![
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::Manifest,
|
|
||||||
artifact_kind: VcirArtifactKind::Mft,
|
|
||||||
uri: Some(manifest_rsync_uri.to_string()),
|
|
||||||
sha256: sha256_hex(manifest_rsync_uri.as_bytes()),
|
|
||||||
object_type: Some("mft".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::CurrentCrl,
|
|
||||||
artifact_kind: VcirArtifactKind::Crl,
|
|
||||||
uri: Some(manifest_rsync_uri.replace(".mft", ".crl")),
|
|
||||||
sha256: sha256_hex(format!("{}-crl", manifest_rsync_uri).as_bytes()),
|
|
||||||
object_type: Some("crl".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
VcirRelatedArtifact {
|
|
||||||
artifact_role: VcirArtifactRole::SignedObject,
|
|
||||||
artifact_kind: VcirArtifactKind::Roa,
|
|
||||||
uri: Some("rsync://example.test/leaf/a.roa".to_string()),
|
|
||||||
sha256: roa_hash.to_string(),
|
|
||||||
object_type: Some("roa".to_string()),
|
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn put_raw(store: &RocksStore, bytes: &[u8], uri: &str, object_type: &str) {
|
|
||||||
let mut entry = RawByHashEntry::from_bytes(sha256_hex(bytes), bytes.to_vec());
|
|
||||||
entry.origin_uris.push(uri.to_string());
|
|
||||||
entry.object_type = Some(object_type.to_string());
|
|
||||||
entry.encoding = Some("der".to_string());
|
|
||||||
store
|
|
||||||
.put_raw_by_hash_entry(&entry)
|
|
||||||
.expect("put raw evidence");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_returns_leaf_to_root_chain_and_evidence_refs() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
|
|
||||||
let root_manifest = "rsync://example.test/root/root.mft";
|
|
||||||
let leaf_manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let local = sample_local_output(leaf_manifest);
|
|
||||||
let leaf_vcir = sample_vcir(
|
|
||||||
leaf_manifest,
|
|
||||||
Some(root_manifest),
|
|
||||||
"test-tal",
|
|
||||||
Some(local.clone()),
|
|
||||||
sample_artifacts(leaf_manifest, &local.source_object_hash),
|
|
||||||
);
|
|
||||||
let root_vcir = sample_vcir(
|
|
||||||
root_manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(root_manifest, &sha256_hex(b"root-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&leaf_vcir).expect("put leaf vcir");
|
|
||||||
store.put_vcir(&root_vcir).expect("put root vcir");
|
|
||||||
|
|
||||||
let rule_entry = AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: local.rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: leaf_manifest.to_string(),
|
|
||||||
source_object_uri: local.source_object_uri.clone(),
|
|
||||||
source_object_hash: local.source_object_hash.clone(),
|
|
||||||
output_id: local.output_id.clone(),
|
|
||||||
item_effective_until: local.item_effective_until.clone(),
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&rule_entry)
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
put_raw(&store, leaf_manifest.as_bytes(), leaf_manifest, "mft");
|
|
||||||
put_raw(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", leaf_manifest).as_bytes(),
|
|
||||||
&leaf_manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
put_raw(&store, b"roa-raw", &local.source_object_uri, "roa");
|
|
||||||
put_raw(&store, b"roa-ee", "rsync://example.test/leaf/a.ee", "cer");
|
|
||||||
put_raw(&store, root_manifest.as_bytes(), root_manifest, "mft");
|
|
||||||
put_raw(
|
|
||||||
&store,
|
|
||||||
format!("{}-crl", root_manifest).as_bytes(),
|
|
||||||
&root_manifest.replace(".mft", ".crl"),
|
|
||||||
"crl",
|
|
||||||
);
|
|
||||||
|
|
||||||
let trace = trace_rule_to_root(&store, AuditRuleKind::Roa, &local.rule_hash)
|
|
||||||
.expect("trace rule")
|
|
||||||
.expect("trace exists");
|
|
||||||
|
|
||||||
assert_eq!(trace.rule, rule_entry);
|
|
||||||
assert_eq!(trace.resolved_output.output_id, local.output_id);
|
|
||||||
assert_eq!(trace.chain_leaf_to_root.len(), 2);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[0].manifest_rsync_uri,
|
|
||||||
leaf_manifest
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[1].manifest_rsync_uri,
|
|
||||||
root_manifest
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
trace.chain_leaf_to_root[0]
|
|
||||||
.parent_manifest_rsync_uri
|
|
||||||
.as_deref(),
|
|
||||||
Some(root_manifest)
|
|
||||||
);
|
|
||||||
assert!(trace.source_object_raw.raw_present);
|
|
||||||
assert!(trace.source_ee_cert_raw.raw_present);
|
|
||||||
assert!(
|
|
||||||
trace.chain_leaf_to_root[0]
|
|
||||||
.related_artifacts
|
|
||||||
.iter()
|
|
||||||
.any(|artifact| {
|
|
||||||
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_returns_none_for_missing_rule_index() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
trace_rule_to_root(&store, AuditRuleKind::Roa, &sha256_hex(b"missing"))
|
|
||||||
.expect("missing trace ok")
|
|
||||||
.is_none()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_errors_when_index_points_to_missing_vcir() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let rule_hash = sha256_hex(b"missing-vcir-rule");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: "rsync://example.test/missing.mft".to_string(),
|
|
||||||
source_object_uri: "rsync://example.test/missing.roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"missing-source"),
|
|
||||||
output_id: sha256_hex(b"missing-output"),
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
AuditTraceError::MissingVcir { manifest_rsync_uri }
|
|
||||||
if manifest_rsync_uri == "rsync://example.test/missing.mft"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_rule_to_root_errors_when_vcir_local_output_is_missing() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let manifest = "rsync://example.test/leaf/leaf.mft";
|
|
||||||
let vcir = sample_vcir(
|
|
||||||
manifest,
|
|
||||||
None,
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(manifest, &sha256_hex(b"leaf-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&vcir).expect("put vcir");
|
|
||||||
let rule_hash = sha256_hex(b"missing-output-rule");
|
|
||||||
store
|
|
||||||
.put_audit_rule_index_entry(&AuditRuleIndexEntry {
|
|
||||||
kind: AuditRuleKind::Roa,
|
|
||||||
rule_hash: rule_hash.clone(),
|
|
||||||
manifest_rsync_uri: manifest.to_string(),
|
|
||||||
source_object_uri: "rsync://example.test/leaf/a.roa".to_string(),
|
|
||||||
source_object_hash: sha256_hex(b"leaf-object"),
|
|
||||||
output_id: sha256_hex(b"missing-output"),
|
|
||||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
|
||||||
time::OffsetDateTime::now_utc() + time::Duration::minutes(1),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
.expect("put rule index");
|
|
||||||
|
|
||||||
let err = trace_rule_to_root(&store, AuditRuleKind::Roa, &rule_hash).unwrap_err();
|
|
||||||
assert!(matches!(err, AuditTraceError::MissingLocalOutput { .. }));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn trace_vcir_chain_to_root_detects_parent_cycle() {
|
|
||||||
let store_dir = tempfile::tempdir().expect("store dir");
|
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
|
||||||
let a_manifest = "rsync://example.test/a.mft";
|
|
||||||
let b_manifest = "rsync://example.test/b.mft";
|
|
||||||
let a_vcir = sample_vcir(
|
|
||||||
a_manifest,
|
|
||||||
Some(b_manifest),
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(a_manifest, &sha256_hex(b"a-object")),
|
|
||||||
);
|
|
||||||
let b_vcir = sample_vcir(
|
|
||||||
b_manifest,
|
|
||||||
Some(a_manifest),
|
|
||||||
"test-tal",
|
|
||||||
None,
|
|
||||||
sample_artifacts(b_manifest, &sha256_hex(b"b-object")),
|
|
||||||
);
|
|
||||||
store.put_vcir(&a_vcir).expect("put a");
|
|
||||||
store.put_vcir(&b_vcir).expect("put b");
|
|
||||||
|
|
||||||
let err = trace_vcir_chain_to_root(&store, a_manifest).unwrap_err();
|
|
||||||
assert!(matches!(
|
|
||||||
err,
|
|
||||||
AuditTraceError::ParentCycle { manifest_rsync_uri }
|
|
||||||
if manifest_rsync_uri == a_manifest
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
290
src/bin/ccr_fixture_gen.rs
Normal file
290
src/bin/ccr_fixture_gen.rs
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use rocksdb::{DB, IteratorMode, Options};
|
|
||||||
use rpki::storage::{
|
|
||||||
ALL_COLUMN_FAMILY_NAMES, CF_AUDIT_RULE_INDEX, CF_RAW_BY_HASH, CF_RAW_OBJECTS,
|
|
||||||
CF_REPOSITORY_VIEW, CF_RRDP_OBJECT_INDEX, CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE,
|
|
||||||
CF_RRDP_URI_OWNER, CF_VCIR, column_family_descriptors,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum DbStatsMode {
|
|
||||||
Estimate,
|
|
||||||
Exact,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
enum CfGroup {
|
|
||||||
CurrentRepositoryView,
|
|
||||||
CurrentValidationState,
|
|
||||||
CurrentRrdpState,
|
|
||||||
LegacyCompatibility,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CfGroup {
|
|
||||||
fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::CurrentRepositoryView => "current_repository_view",
|
|
||||||
Self::CurrentValidationState => "current_validation_state",
|
|
||||||
Self::CurrentRrdpState => "current_rrdp_state",
|
|
||||||
Self::LegacyCompatibility => "legacy_compatibility",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Output groups:
|
|
||||||
- current_repository_view: repository_view + raw_by_hash
|
|
||||||
- current_validation_state: vcir + audit_rule_index
|
|
||||||
- current_rrdp_state: rrdp_state + rrdp_source + rrdp_source_member + rrdp_uri_owner
|
|
||||||
- legacy_compatibility: raw_objects + rrdp_object_index
|
|
||||||
"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 cf_group(cf_name: &str) -> CfGroup {
|
|
||||||
match cf_name {
|
|
||||||
CF_REPOSITORY_VIEW | CF_RAW_BY_HASH => CfGroup::CurrentRepositoryView,
|
|
||||||
CF_VCIR | CF_AUDIT_RULE_INDEX => CfGroup::CurrentValidationState,
|
|
||||||
CF_RRDP_STATE | CF_RRDP_SOURCE | CF_RRDP_SOURCE_MEMBER | CF_RRDP_URI_OWNER => {
|
|
||||||
CfGroup::CurrentRrdpState
|
|
||||||
}
|
|
||||||
CF_RAW_OBJECTS | CF_RRDP_OBJECT_INDEX => CfGroup::LegacyCompatibility,
|
|
||||||
_ => CfGroup::LegacyCompatibility,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn summarize_counts<'a>(
|
|
||||||
counts: impl IntoIterator<Item = (&'a str, u64)>,
|
|
||||||
) -> BTreeMap<CfGroup, u64> {
|
|
||||||
let mut grouped = BTreeMap::new();
|
|
||||||
for (cf_name, count) in counts {
|
|
||||||
*grouped.entry(cf_group(cf_name)).or_insert(0) += count;
|
|
||||||
}
|
|
||||||
grouped
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mode_label(mode: DbStatsMode) -> &'static str {
|
|
||||||
match mode {
|
|
||||||
DbStatsMode::Estimate => "estimate",
|
|
||||||
DbStatsMode::Exact => "exact",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 mode = DbStatsMode::Estimate;
|
|
||||||
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" => mode = DbStatsMode::Exact,
|
|
||||||
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, column_family_descriptors())?;
|
|
||||||
|
|
||||||
println!("db={}", db_path.display());
|
|
||||||
println!("mode={}", mode_label(mode));
|
|
||||||
|
|
||||||
let mut per_cf = Vec::with_capacity(ALL_COLUMN_FAMILY_NAMES.len());
|
|
||||||
let mut total: u64 = 0;
|
|
||||||
for &name in ALL_COLUMN_FAMILY_NAMES {
|
|
||||||
let n = match mode {
|
|
||||||
DbStatsMode::Exact => exact_keys(&db, name)?,
|
|
||||||
DbStatsMode::Estimate => estimate_keys(&db, name)?.unwrap_or(0),
|
|
||||||
};
|
|
||||||
total = total.saturating_add(n);
|
|
||||||
per_cf.push((name, n));
|
|
||||||
println!("{name}={n}");
|
|
||||||
}
|
|
||||||
println!("total={total}");
|
|
||||||
|
|
||||||
for (group, count) in summarize_counts(per_cf.iter().copied()) {
|
|
||||||
println!("group_{}={count}", group.as_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
let live = db.live_files()?;
|
|
||||||
println!("sst_files={}", live.len());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cf_group_classifies_current_and_legacy_keyspaces() {
|
|
||||||
assert_eq!(cf_group(CF_REPOSITORY_VIEW), CfGroup::CurrentRepositoryView);
|
|
||||||
assert_eq!(cf_group(CF_RAW_BY_HASH), CfGroup::CurrentRepositoryView);
|
|
||||||
assert_eq!(cf_group(CF_VCIR), CfGroup::CurrentValidationState);
|
|
||||||
assert_eq!(
|
|
||||||
cf_group(CF_AUDIT_RULE_INDEX),
|
|
||||||
CfGroup::CurrentValidationState
|
|
||||||
);
|
|
||||||
assert_eq!(cf_group(CF_RRDP_SOURCE), CfGroup::CurrentRrdpState);
|
|
||||||
assert_eq!(cf_group(CF_RRDP_URI_OWNER), CfGroup::CurrentRrdpState);
|
|
||||||
assert_eq!(cf_group(CF_RAW_OBJECTS), CfGroup::LegacyCompatibility);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn summarize_counts_accumulates_by_group() {
|
|
||||||
let grouped = summarize_counts([
|
|
||||||
(CF_REPOSITORY_VIEW, 5),
|
|
||||||
(CF_RAW_BY_HASH, 7),
|
|
||||||
(CF_VCIR, 11),
|
|
||||||
(CF_AUDIT_RULE_INDEX, 13),
|
|
||||||
(CF_RRDP_STATE, 17),
|
|
||||||
(CF_RRDP_SOURCE_MEMBER, 19),
|
|
||||||
(CF_RRDP_OBJECT_INDEX, 29),
|
|
||||||
]);
|
|
||||||
|
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentRepositoryView), Some(&12));
|
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentValidationState), Some(&24));
|
|
||||||
assert_eq!(grouped.get(&CfGroup::CurrentRrdpState), Some(&36));
|
|
||||||
assert_eq!(grouped.get(&CfGroup::LegacyCompatibility), Some(&29));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn usage_mentions_grouped_output_and_exact_mode() {
|
|
||||||
let text = usage();
|
|
||||||
assert!(text.contains("--exact"), "{text}");
|
|
||||||
assert!(text.contains("current_validation_state"), "{text}");
|
|
||||||
assert!(text.contains("legacy_compatibility"), "{text}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,346 +0,0 @@
|
|||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use rocksdb::{DB, IteratorMode, Options};
|
|
||||||
use rpki::storage::{
|
|
||||||
CF_RRDP_SOURCE, CF_RRDP_SOURCE_MEMBER, CF_RRDP_STATE, CF_RRDP_URI_OWNER,
|
|
||||||
RrdpSourceMemberRecord, RrdpSourceRecord, RrdpUriOwnerRecord, column_family_descriptors,
|
|
||||||
};
|
|
||||||
use rpki::sync::rrdp::RrdpState;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
enum DumpView {
|
|
||||||
LegacyState,
|
|
||||||
Source,
|
|
||||||
Members,
|
|
||||||
Owners,
|
|
||||||
All,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DumpView {
|
|
||||||
fn parse(value: &str) -> Result<Self, String> {
|
|
||||||
match value {
|
|
||||||
"legacy-state" => Ok(Self::LegacyState),
|
|
||||||
"source" => Ok(Self::Source),
|
|
||||||
"members" => Ok(Self::Members),
|
|
||||||
"owners" => Ok(Self::Owners),
|
|
||||||
"all" => Ok(Self::All),
|
|
||||||
other => Err(format!(
|
|
||||||
"invalid --view: {other} (expected one of: legacy-state, source, members, owners, all)"
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct DumpArgs {
|
|
||||||
db_path: PathBuf,
|
|
||||||
view: DumpView,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usage() -> String {
|
|
||||||
let bin = "rrdp_state_dump";
|
|
||||||
format!(
|
|
||||||
"\
|
|
||||||
Usage:
|
|
||||||
{bin} --db <path> [--view <legacy-state|source|members|owners|all>]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--db <path> RocksDB directory
|
|
||||||
--view <name> Dump one RRDP view; default is all
|
|
||||||
--help Show this help
|
|
||||||
"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_args(argv: &[String]) -> Result<DumpArgs, String> {
|
|
||||||
if argv.iter().any(|a| a == "--help" || a == "-h") {
|
|
||||||
return Err(usage());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut db_path: Option<PathBuf> = None;
|
|
||||||
let mut view = DumpView::All;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
"--view" => {
|
|
||||||
i += 1;
|
|
||||||
let v = argv.get(i).ok_or("--view requires a value")?;
|
|
||||||
view = DumpView::parse(v)?;
|
|
||||||
}
|
|
||||||
other => return Err(format!("unknown argument: {other}\n\n{}", usage())),
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DumpArgs {
|
|
||||||
db_path: db_path.ok_or_else(|| format!("--db is required\n\n{}", usage()))?,
|
|
||||||
view,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_db(path: &std::path::Path) -> Result<DB, Box<dyn std::error::Error>> {
|
|
||||||
let mut opts = Options::default();
|
|
||||||
opts.create_if_missing(false);
|
|
||||||
opts.create_missing_column_families(false);
|
|
||||||
Ok(DB::open_cf_descriptors(
|
|
||||||
&opts,
|
|
||||||
path,
|
|
||||||
column_family_descriptors(),
|
|
||||||
)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_legacy_state(db: &DB) -> Result<Vec<(String, RrdpState)>, Box<dyn std::error::Error>> {
|
|
||||||
let cf = db
|
|
||||||
.cf_handle(CF_RRDP_STATE)
|
|
||||||
.ok_or("missing column family: rrdp_state")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
|
||||||
let (k, v) = res?;
|
|
||||||
let notify_uri = String::from_utf8_lossy(&k).to_string();
|
|
||||||
let state = RrdpState::decode(&v)
|
|
||||||
.map_err(|e| format!("decode rrdp_state failed for {notify_uri}: {e}"))?;
|
|
||||||
out.push((notify_uri, state));
|
|
||||||
}
|
|
||||||
out.sort_by(|a, b| a.0.cmp(&b.0));
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_source_records(db: &DB) -> Result<Vec<RrdpSourceRecord>, Box<dyn std::error::Error>> {
|
|
||||||
let cf = db
|
|
||||||
.cf_handle(CF_RRDP_SOURCE)
|
|
||||||
.ok_or("missing column family: rrdp_source")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
|
||||||
let (_k, v) = res?;
|
|
||||||
let record: RrdpSourceRecord = serde_cbor::from_slice(&v)?;
|
|
||||||
record.validate_internal()?;
|
|
||||||
out.push(record);
|
|
||||||
}
|
|
||||||
out.sort_by(|a, b| a.notify_uri.cmp(&b.notify_uri));
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_source_member_records(
|
|
||||||
db: &DB,
|
|
||||||
) -> Result<Vec<RrdpSourceMemberRecord>, Box<dyn std::error::Error>> {
|
|
||||||
let cf = db
|
|
||||||
.cf_handle(CF_RRDP_SOURCE_MEMBER)
|
|
||||||
.ok_or("missing column family: rrdp_source_member")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
|
||||||
let (_k, v) = res?;
|
|
||||||
let record: RrdpSourceMemberRecord = serde_cbor::from_slice(&v)?;
|
|
||||||
record.validate_internal()?;
|
|
||||||
out.push(record);
|
|
||||||
}
|
|
||||||
out.sort_by(|a, b| {
|
|
||||||
a.notify_uri
|
|
||||||
.cmp(&b.notify_uri)
|
|
||||||
.then(a.rsync_uri.cmp(&b.rsync_uri))
|
|
||||||
});
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_uri_owner_records(
|
|
||||||
db: &DB,
|
|
||||||
) -> Result<Vec<RrdpUriOwnerRecord>, Box<dyn std::error::Error>> {
|
|
||||||
let cf = db
|
|
||||||
.cf_handle(CF_RRDP_URI_OWNER)
|
|
||||||
.ok_or("missing column family: rrdp_uri_owner")?;
|
|
||||||
let mut out = Vec::new();
|
|
||||||
for res in db.iterator_cf(cf, IteratorMode::Start) {
|
|
||||||
let (_k, v) = res?;
|
|
||||||
let record: RrdpUriOwnerRecord = serde_cbor::from_slice(&v)?;
|
|
||||||
record.validate_internal()?;
|
|
||||||
out.push(record);
|
|
||||||
}
|
|
||||||
out.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_legacy_state(entries: &[(String, RrdpState)]) {
|
|
||||||
println!("[legacy-state]");
|
|
||||||
println!("notify_uri\tserial\tsession_id");
|
|
||||||
for (notify_uri, state) in entries {
|
|
||||||
println!("{notify_uri}\t{}\t{}", state.serial, state.session_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_source_records(entries: &[RrdpSourceRecord]) {
|
|
||||||
println!("[source]");
|
|
||||||
println!("notify_uri\tlast_serial\tlast_session_id\tsync_state\tlast_snapshot_uri\tlast_error");
|
|
||||||
for record in entries {
|
|
||||||
println!(
|
|
||||||
"{}\t{}\t{}\t{:?}\t{}\t{}",
|
|
||||||
record.notify_uri,
|
|
||||||
record
|
|
||||||
.last_serial
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
.unwrap_or_else(|| "-".to_string()),
|
|
||||||
record.last_session_id.as_deref().unwrap_or("-"),
|
|
||||||
record.sync_state,
|
|
||||||
record.last_snapshot_uri.as_deref().unwrap_or("-"),
|
|
||||||
record.last_error.as_deref().unwrap_or("-"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_source_member_records(entries: &[RrdpSourceMemberRecord]) {
|
|
||||||
println!("[members]");
|
|
||||||
println!("notify_uri\trsync_uri\tpresent\thash\tsession_id\tserial");
|
|
||||||
for record in entries {
|
|
||||||
println!(
|
|
||||||
"{}\t{}\t{}\t{}\t{}\t{}",
|
|
||||||
record.notify_uri,
|
|
||||||
record.rsync_uri,
|
|
||||||
record.present,
|
|
||||||
record.current_hash.as_deref().unwrap_or("-"),
|
|
||||||
record.last_confirmed_session_id,
|
|
||||||
record.last_confirmed_serial,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_uri_owner_records(entries: &[RrdpUriOwnerRecord]) {
|
|
||||||
println!("[owners]");
|
|
||||||
println!("rsync_uri\tnotify_uri\towner_state\thash\tsession_id\tserial");
|
|
||||||
for record in entries {
|
|
||||||
println!(
|
|
||||||
"{}\t{}\t{:?}\t{}\t{}\t{}",
|
|
||||||
record.rsync_uri,
|
|
||||||
record.notify_uri,
|
|
||||||
record.owner_state,
|
|
||||||
record.current_hash.as_deref().unwrap_or("-"),
|
|
||||||
record.last_confirmed_session_id,
|
|
||||||
record.last_confirmed_serial,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let argv: Vec<String> = std::env::args().collect();
|
|
||||||
let args = parse_args(&argv).map_err(|e| -> Box<dyn std::error::Error> { e.into() })?;
|
|
||||||
let db = open_db(&args.db_path)?;
|
|
||||||
|
|
||||||
match args.view {
|
|
||||||
DumpView::LegacyState => print_legacy_state(&collect_legacy_state(&db)?),
|
|
||||||
DumpView::Source => print_source_records(&collect_source_records(&db)?),
|
|
||||||
DumpView::Members => print_source_member_records(&collect_source_member_records(&db)?),
|
|
||||||
DumpView::Owners => print_uri_owner_records(&collect_uri_owner_records(&db)?),
|
|
||||||
DumpView::All => {
|
|
||||||
print_legacy_state(&collect_legacy_state(&db)?);
|
|
||||||
print_source_records(&collect_source_records(&db)?);
|
|
||||||
print_source_member_records(&collect_source_member_records(&db)?);
|
|
||||||
print_uri_owner_records(&collect_uri_owner_records(&db)?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use rpki::storage::{PackTime, RocksStore, RrdpSourceSyncState, RrdpUriOwnerState};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_args_accepts_view_and_db() {
|
|
||||||
let args = parse_args(&[
|
|
||||||
"rrdp_state_dump".to_string(),
|
|
||||||
"--db".to_string(),
|
|
||||||
"db".to_string(),
|
|
||||||
"--view".to_string(),
|
|
||||||
"owners".to_string(),
|
|
||||||
])
|
|
||||||
.expect("parse args");
|
|
||||||
assert_eq!(args.db_path, PathBuf::from("db"));
|
|
||||||
assert_eq!(args.view, DumpView::Owners);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_args_rejects_invalid_view() {
|
|
||||||
let err = parse_args(&[
|
|
||||||
"rrdp_state_dump".to_string(),
|
|
||||||
"--db".to_string(),
|
|
||||||
"db".to_string(),
|
|
||||||
"--view".to_string(),
|
|
||||||
"nope".to_string(),
|
|
||||||
])
|
|
||||||
.unwrap_err();
|
|
||||||
assert!(err.contains("invalid --view"), "{err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn collect_rrdp_views_reads_legacy_and_current_records() {
|
|
||||||
let dir = tempfile::tempdir().expect("tempdir");
|
|
||||||
let store = RocksStore::open(dir.path()).expect("open store");
|
|
||||||
let legacy_state = RrdpState {
|
|
||||||
session_id: "session-1".to_string(),
|
|
||||||
serial: 42,
|
|
||||||
};
|
|
||||||
store
|
|
||||||
.put_rrdp_state(
|
|
||||||
"https://example.test/notify.xml",
|
|
||||||
&legacy_state.encode().expect("encode legacy state"),
|
|
||||||
)
|
|
||||||
.expect("put legacy state");
|
|
||||||
store
|
|
||||||
.put_rrdp_source_record(&RrdpSourceRecord {
|
|
||||||
notify_uri: "https://example.test/notify.xml".to_string(),
|
|
||||||
last_session_id: Some("session-1".to_string()),
|
|
||||||
last_serial: Some(42),
|
|
||||||
first_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
|
|
||||||
last_seen_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
|
|
||||||
last_sync_at: None,
|
|
||||||
sync_state: RrdpSourceSyncState::DeltaReady,
|
|
||||||
last_snapshot_uri: Some("https://example.test/snapshot.xml".to_string()),
|
|
||||||
last_snapshot_hash: None,
|
|
||||||
last_error: None,
|
|
||||||
})
|
|
||||||
.expect("put source record");
|
|
||||||
store
|
|
||||||
.put_rrdp_source_member_record(&RrdpSourceMemberRecord {
|
|
||||||
notify_uri: "https://example.test/notify.xml".to_string(),
|
|
||||||
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
|
|
||||||
current_hash: Some("11".repeat(32)),
|
|
||||||
object_type: Some("roa".to_string()),
|
|
||||||
present: true,
|
|
||||||
last_confirmed_session_id: "session-1".to_string(),
|
|
||||||
last_confirmed_serial: 42,
|
|
||||||
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
|
|
||||||
})
|
|
||||||
.expect("put member record");
|
|
||||||
store
|
|
||||||
.put_rrdp_uri_owner_record(&RrdpUriOwnerRecord {
|
|
||||||
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
|
|
||||||
notify_uri: "https://example.test/notify.xml".to_string(),
|
|
||||||
current_hash: Some("11".repeat(32)),
|
|
||||||
last_confirmed_session_id: "session-1".to_string(),
|
|
||||||
last_confirmed_serial: 42,
|
|
||||||
last_changed_at: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
|
|
||||||
owner_state: RrdpUriOwnerState::Active,
|
|
||||||
})
|
|
||||||
.expect("put owner record");
|
|
||||||
drop(store);
|
|
||||||
|
|
||||||
let db = open_db(dir.path()).expect("open db");
|
|
||||||
let legacy = collect_legacy_state(&db).expect("legacy dump");
|
|
||||||
let sources = collect_source_records(&db).expect("source dump");
|
|
||||||
let members = collect_source_member_records(&db).expect("members dump");
|
|
||||||
let owners = collect_uri_owner_records(&db).expect("owners dump");
|
|
||||||
|
|
||||||
assert_eq!(legacy.len(), 1);
|
|
||||||
assert_eq!(legacy[0].0, "https://example.test/notify.xml");
|
|
||||||
assert_eq!(legacy[0].1.serial, 42);
|
|
||||||
assert_eq!(sources.len(), 1);
|
|
||||||
assert_eq!(sources[0].sync_state, RrdpSourceSyncState::DeltaReady);
|
|
||||||
assert_eq!(members.len(), 1);
|
|
||||||
assert_eq!(members[0].rsync_uri, "rsync://example.test/repo/a.roa");
|
|
||||||
assert_eq!(owners.len(), 1);
|
|
||||||
assert_eq!(owners[0].owner_state, RrdpUriOwnerState::Active);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user