arm构建

This commit is contained in:
xiuting.xu 2026-07-06 11:01:30 +08:00
parent cc3518897f
commit b5cbc7fbee
34 changed files with 1321 additions and 1055 deletions

39
Cargo.lock generated
View File

@ -223,16 +223,6 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "bcder"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7c42c9913f68cf9390a225e81ad56a5c515347287eb98baa710090ca1de86d"
dependencies = [
"bytes",
"smallvec",
]
[[package]]
name = "bcrypt-pbkdf"
version = "0.10.0"
@ -2112,7 +2102,6 @@ dependencies = [
"hex",
"rand 0.10.0",
"rocksdb",
"rpki 0.19.2",
"russh",
"rustls",
"rustls-pemfile",
@ -2132,24 +2121,6 @@ dependencies = [
"x509-parser",
]
[[package]]
name = "rpki"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99010980abb0aee7a5c4b1c04d0f42f74f57ac168adb5d38a99b3517a4d72df9"
dependencies = [
"base64",
"bcder",
"bytes",
"chrono",
"futures-util",
"log",
"ring",
"tokio",
"untrusted",
"uuid",
]
[[package]]
name = "rsa"
version = "0.10.0-rc.17"
@ -2938,16 +2909,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"

View File

@ -18,7 +18,7 @@ asn1-rs = "0.7.1"
asn1-rs-derive = "0.6.0"
asn1 = "0.23.0"
arc-swap = "1.7.0"
chrono = "0.4.44"
chrono = { version = "0.4.44", features = ["serde"] }
chrono-tz = "0.10"
bytes = "1.11.1"
tokio = { version = "1.49.0", features = ["full"] }
@ -38,4 +38,3 @@ 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"] }
rpki_rs = { package = "rpki", version = "0.19.2", features = ["rtr", "crypto"] }

645
README.md
View File

@ -1,475 +1,306 @@
# RPKI RTR Server
默认运行平台Ubuntu/Linux
这是一个用 Rust 实现的 RPKI-to-Router (RTR) cache server。服务端从 CCR 文件中读取 RPKI 数据,生成 RTR cache snapshot 和 delta并通过 RTR 协议提供给路由器或调试客户端
## 目录
当前代码重点覆盖:
- [协议与规范](#协议与规范)
- [RTR](#rtr)
- [SLURM](#slurm)
- [CCR](#ccr)
- [RTR Server](#rtr-server)
- [环境变量](#环境变量)
- [说明](#说明)
- [快速启动](#快速启动)
- [Docker 启动](#docker-启动)
- [本地运行(推荐先用脚本)](#本地运行推荐先用脚本)
- [本地手动运行(最小示例)](#本地手动运行最小示例)
- [CCR 输入说明](#ccr-输入说明)
- [Client 使用](#client-使用)
- [作用与选择](#作用与选择)
- [rtr_debug_client本地](#rtr_debug_client本地)
- [rtr_debug_clientDocker](#rtr_debug_clientdocker)
- [rpki-rs-test-clientDocker](#rpki-rs-test-clientdocker)
- [FRRDocker黑盒客户端](#frrdocker黑盒客户端)
- [RTR Debug Client](#rtr-debug-client)
- [Client 启动示例](#client-启动示例)
- [存储模型与边界约束(多视图)](#存储模型与边界约束多视图)
- [唯一写入口](#唯一写入口)
- [恢复约束](#恢复约束)
- [边界防回归测试](#边界防回归测试)
- [Deploy Layout](#deploy-layout)
- [Quick Start](#quick-start)
- [Compose 入口](#compose-入口)
- [角色附加命令](#角色附加命令)
- RTR serverTCP 默认启用,可选启用 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。
## 协议与规范
## 协议参考
### RTR
- 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)
- [RFC 6810: The Resource Public Key Infrastructure (RPKI) to Router Protocol](https://www.rfc-editor.org/rfc/rfc6810.html)
- [RFC 8210: The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 1](https://www.rfc-editor.org/rfc/rfc8210.html)
- [Draft: The Resource Public Key Infrastructure (RPKI) to Router Protocol, Version 2](https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-25.html)
## 项目结构
### SLURM
| 路径 | 说明 |
| --- | --- |
| `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/` | 单元和集成测试 |
- [RFC 8416: Simplified Local Internet Number Resource Management with the RPKI (SLURM)](https://www.rfc-editor.org/rfc/rfc8416.html)
- [Draft: ASPA extensions for SLURM](https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-slurm-04.html)
## 构建与测试
### CCR
```bash
cargo build --bin rpki_rtr
cargo build --bin rtr_debug_client
cargo test
```
- [Draft: A Concise Cryptographic Representation of RPKI Signed Objects (CCR)](https://www.ietf.org/archive/id/draft-ietf-sidrops-rpki-ccr-02.html)
服务端 binary 名称是 `rpki_rtr`
## RTR Server
```bash
cargo run --bin rpki_rtr
```
RTR Server 运行时从 `CCR` 目录中扫描最新的 `.ccr` 文件作为输入源:
默认配置会监听 `0.0.0.0:323`,从 `./data` 查找 CCR 文件,并使用 `./rtr-db` 保存 RocksDB 数据。启动前需要确保 CCR 目录里存在可解析的 `.ccr` 文件。
- `VRP`
- `VAP (Validated ASPA Payload) / ASPA`
## 数据输入
相关实现位置
服务端启动和刷新时会读取 `RPKI_RTR_CCR_DIR` 指向的目录
- [`src/main.rs`](src/main.rs)
- [`src/source/ccr.rs`](src/source/ccr.rs)
### 环境变量
| 变量名 | 说明 | 默认值 | 示例 |
| --- | --- | --- | --- |
| `RPKI_RTR_ENABLE_TLS` | 是否额外启用 TLS 监听。支持 `true/false``1/0``yes/no``on/off`。 | `false` | `true` |
| `RPKI_RTR_TCP_ADDR` | TCP 监听地址。 | `0.0.0.0:323` | `0.0.0.0:323` |
| `RPKI_RTR_TLS_ADDR` | TLS 监听地址。 | `0.0.0.0:324` | `0.0.0.0:324` |
| `RPKI_RTR_DB_PATH` | RocksDB 路径。 | `./rtr-db` | `./rtr-db` |
| `RPKI_RTR_CCR_DIR` | CCR 目录路径;程序会扫描其中最新的 `.ccr` 文件。 | `./data` | `./data` |
| `RPKI_RTR_SLURM_DIR` | SLURM 目录路径;为空或未设置表示禁用 SLURM。 | `未设置(禁用)` | `./slurm` |
| `RPKI_RTR_TLS_CERT_PATH` | TLS 服务端证书路径。 | `./certs/server.crt` | `./certs/server-dns.crt` |
| `RPKI_RTR_TLS_KEY_PATH` | TLS 服务端私钥路径。 | `./certs/server.key` | `./certs/server-dns.key` |
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | 用于校验 router 客户端证书的 CA 证书路径。 | `./certs/client-ca.crt` | `./certs/client-ca.crt` |
| `RPKI_RTR_ENABLE_SSH` | 是否额外启用进程内原生 SSH 监听。支持 `true/false``1/0``yes/no``on/off`。 | `false` | `true` |
| `RPKI_RTR_SSH_ADDR` | SSH 监听地址。 | `0.0.0.0:22` | `0.0.0.0:22` |
| `RPKI_RTR_SSH_PORT` | SSH 监听端口(仅覆盖 `RPKI_RTR_SSH_ADDR` 中的端口)。 | `22` | `2022` |
| `RPKI_RTR_SSH_HOST_KEY_PATH` | OpenSSH host 私钥路径。 | `./certs/ssh_host_ed25519_key` | `./certs/ssh_host_ed25519_key` |
| `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH` | 允许接入的 router 公钥列表authorized_keys。 | `./certs/rtr-authorized_keys` | `./certs/rtr-authorized_keys` |
| `RPKI_RTR_SSH_USERNAME` | SSH 用户名白名单。 | `rpki-rtr` | `rpki-rtr` |
| `RPKI_RTR_SSH_SUBSYSTEM_NAME` | SSH 子系统名称。 | `rpki-rtr` | `rpki-rtr` |
| `RPKI_RTR_SSH_PASSWORD` | 可选的 SSH password 认证口令。未设置时仅允许 publickey设置后同时允许 publickey 与 password。 | `未设置` | `test-password` |
| `RPKI_RTR_MAX_DELTA` | 最多保留多少条 delta。 | `100` | `100` |
| `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE` | 是否启用“累计 delta 估算 wire size 不小于 snapshot 时,继续裁剪最老 delta”的策略。 | `false` | `false` |
| `RPKI_RTR_STRICT_CCR_VALIDATION` | 是否对 CCR 中的非法 VRP / VAP 采用严格模式;`true` 表示整份 CCR 拒绝,`false` 表示跳过非法项并告警。 | `false` | `false` |
| `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS` | 数据源刷新周期:扫描 CCR/SLURM 并尝试更新缓存的间隔,单位秒,必须 `>= 1`。 | `300` | `30` |
| `RPKI_RTR_REFRESH_INTERVAL_SECS` | 旧变量名(已兼容,建议迁移到 `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS`)。 | `300` | `300` |
| `RPKI_RTR_TIMING_REFRESH_SECS` | RTR EndOfData timing 的 `refresh` 字段,单位秒。 | `3600` | `300` |
| `RPKI_RTR_TIMING_RETRY_SECS` | RTR EndOfData timing 的 `retry` 字段,单位秒。 | `600` | `60` |
| `RPKI_RTR_TIMING_EXPIRE_SECS` | RTR EndOfData timing 的 `expire` 字段,单位秒(需大于 refresh/retry。 | `7200` | `1800` |
| `RPKI_RTR_MAX_CONNECTIONS` | 最大并发 RTR 客户端连接数。 | `512` | `512` |
| `RPKI_RTR_NOTIFY_QUEUE_SIZE` | Serial Notify 广播队列大小。 | `1024` | `1024` |
| `RPKI_RTR_TCP_KEEPALIVE_SECS` | TCP keepalive 时间,单位秒;设为 `0` 表示禁用。 | `60` | `60` |
| `RPKI_RTR_WARN_INSECURE_TCP` | 纯 TCP 模式下是否输出不安全警告。 | `true` | `true` |
| `RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN` | 严格模式TLS 服务端证书不包含 `subjectAltName dNSName` 时拒绝启动。 | `false` | `false` |
| `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS` | `rtr-runtime-*.json` 周期写入间隔,单位秒,必须 `>= 1`。 | `300` | `300` |
| `RPKI_RTR_REPORT_HISTORY_LIMIT` | `report/` 目录下每类报告文件的滚动保留数量。 | `10` | `10` |
| `RPKI_RTR_TIMEZONE` | 日志和 JSON report 时间使用的 IANA 时区。 | `Asia/Shanghai` | `Asia/Shanghai` |
| `RPKI_RTR_ADMIN_ADDR` | Runtime admin config HTTP 监听地址;空值表示不启动。 | 未设置(禁用) | `127.0.0.1:8323` |
| `RPKI_RTR_ADMIN_TOKEN` | Admin config 接口 Bearer token。非 loopback 监听地址必须设置。 | 未设置 | `change-me` |
### 说明
- 纯 TCP 模式只应部署在受信任、可控网络中。
- TLS 模式要求客户端证书认证。
- 当前输入源是 `CCR` 目录,不再是单独的文本 VRP / ASPA / Router Key 文件。
- 刷新时会重新扫描 `RPKI_RTR_CCR_DIR`,并选取文件名排序最新的 `.ccr` 文件。
- `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS`(旧名 `RPKI_RTR_REFRESH_INTERVAL_SECS`)控制的是“服务端多长时间扫描并更新数据源”,不是 RTR EndOfData 的 timing。
- RTR 客户端在 EndOfData 中看到的 `refresh/retry/expire``RPKI_RTR_TIMING_REFRESH_SECS``RPKI_RTR_TIMING_RETRY_SECS``RPKI_RTR_TIMING_EXPIRE_SECS` 控制。
- `RPKI_RTR_STRICT_CCR_VALIDATION=false`CCR 中的非法 VRP / VAP 会被跳过并输出 warning`true` 时整份 CCR 更新失败。
- `RPKI_RTR_TCP_KEEPALIVE_SECS=0` 表示关闭 keepalive非零值表示在整个连接生命周期内启用 keepalive。
- `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE=true` 时,除了 `RPKI_RTR_MAX_DELTA` 的固定条数裁剪外,还会在累计 delta 估算 wire size 不小于 snapshot 时继续删除最老 delta。
### Runtime Admin Config
`POST /admin/rtr/config` 用于在服务运行中动态修改一部分运行时配置。接口默认关闭;设置 `RPKI_RTR_ADMIN_ADDR` 后才会启动。完整接口说明见 [`docs/rtr-admin-api.md`](docs/rtr-admin-api.md)。
### Runtime Reports
服务会在 `report/` 目录下写入分类型 JSON 文件,并按
`RPKI_RTR_REPORT_HISTORY_LIMIT` 循环保留:
- `rtr-source-*.json`CCR/SLURM source、fingerprint、refresh 状态、数据质量、cache snapshot/delta 统计。
- `rtr-clients-*.json`client 连接数、连接方式统计。启动时写一次,连接数变化时更新。
- `rtr-runtime-*.json`:进程 RSS、服务状态、当前生效 runtime configuration。启动时写一次并按 `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS` 周期更新。
1. 如果该目录下存在包含 `.ccr` 文件的子目录,先按子目录名排序选择最新子目录。
2. 在选中的目录中按文件名排序选择最新 `.ccr` 文件。
3. 从 CCR 中解析 VRP 和 VAP/ASPA。
4. 如果配置了 `RPKI_RTR_SLURM_DIR`,加载其中启用的 `*.slurm` 文件并应用本地策略。
## 快速启动
### Docker 启动
### 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
```
更多 action、mode 和环境变量说明见 `deploy/README.md`
### 跨架构构建
示例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 logs -f rpki-rtr
docker compose -f deploy/server/docker-compose.yml down
```
### 本地运行(推荐先用脚本)
TLS/mTLS 示例:
```bash
docker compose -f deploy/server/docker-compose.yml up -d --build
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
docker compose -f deploy/server/docker-compose.yml down
docker compose -f deploy/server/docker-compose.tls.yml up -d --build
docker compose -f deploy/server/docker-compose.tls.yml down
```
### 本地手动运行(最小示例)
## 传输模式
纯 TCP
TCP 默认启用。TLS 和 SSH 是附加监听,不会替代 TCP。
```bash
docker compose -f deploy/server/docker-compose.yml up -d --build
```
TLS / mutual TLS
```bash
docker compose -f deploy/server/docker-compose.yml -f deploy/server/docker-compose.tls.yml up -d --build
```
## CCR 输入说明
当前会从 `RPKI_RTR_CCR_DIR` 指向的目录中扫描最新 `.ccr` 文件,并从中提取:
- `VRP`
- `VAP (Validated ASPA Payload) / ASPA`
测试样例可参考:
- [`data/20260324T000037Z-sng1.ccr`](data/20260324T000037Z-sng1.ccr)
## Client 使用
客户端的作用是从不同角度验证 RTR 服务端行为,覆盖“协议调试、自动化测试、黑盒互通”三类需求。
### 作用与选择
| Client | 主要作用 | 适用场景 |
| 模式 | 默认地址 | 启用方式 |
| --- | --- | --- |
| `rtr_debug_client` | 面向协议交互调试,便于手动发起 `reset/serial` 并观察响应 | 开发联调、定位会话/报文问题、快速复现 |
| `rpki-rs-test-client` | 基于 `rpki-rs` 客户端 API 做自动化验证 | CI/CD、回归测试、批量步骤校验 |
| `FRR` | 真实路由软件黑盒接入,验证与生产侧客户端互通 | 互操作验证、运维演练、端到端验证 |
| 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`
1. 先用 `rtr_debug_client` 快速确认服务可连通、协议版本和基础响应正常。
2. 再用 `rpki-rs-test-client` 做可重复的自动化步骤校验。
3. 最后用 `FRR` 做黑盒互通验证,确认真实客户端接入行为。
## 主要环境变量
### rtr_debug_clientDocker
布尔值支持 `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/server.crt` |
| `RPKI_RTR_TLS_KEY_PATH` | `./certs/server.key` |
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | `./certs/client-ca.crt` |
SSH 相关配置:
| 变量 | 默认值 |
| --- | --- |
| `RPKI_RTR_SSH_HOST_KEY_PATH` | `./certs/ssh_host_ed25519_key` |
| `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH` | `./certs/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
docker compose -f deploy/client/docker-compose.yml up --build
docker compose -f deploy/client/docker-compose.yml logs -f rtr-debug-client
docker compose -f deploy/client/docker-compose.yml down
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
docker compose -f deploy/client/docker-compose.clients.yml up -d
docker compose -f deploy/client/docker-compose.clients.yml logs -f
docker compose -f deploy/client/docker-compose.clients.yml down
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}'
```
### rpki-rs-test-clientDocker
## 调试客户端
`rtr_debug_client` 用于手动发起 Reset Query 或 Serial Query并观察服务端返回的 PDU。
TCP
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml up --build
docker compose -f deploy/rpki-rs-client/docker-compose.yml run --rm \
rpki-rs-test-client 127.0.0.1:323 2 reset --steps 1 --assert-min-records 1
docker compose -f deploy/rpki-rs-client/docker-compose.yml down
```
说明:
- 适合自动化场景:可作为流水线中的协议检查器。
- 适合回归校验:通过参数化步骤(`--steps`)和断言(如 `--assert-min-records`)稳定验证行为。
### FRRDocker黑盒客户端
```bash
docker compose -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 compose -f deploy/frr/docker-compose.yml down
```
说明:
- 适合真实互通验证FRR 更接近生产客户端行为。
- 适合观察业务结果:可直接查看缓存连接状态和前缀表结果。
## RTR Debug Client
调试用 RTR client 位于:
- [`src/bin/rtr_debug_client/main.rs`](src/bin/rtr_debug_client/main.rs)
说明文档位于:
- [`src/bin/rtr_debug_client/README.md`](src/bin/rtr_debug_client/README.md)
### Client 启动示例
连接纯 TCP RTR server
```sh
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 RTR server
TLS
```sh
```bash
cargo run --bin rtr_debug_client -- \
127.0.0.1:324 1 reset \
--tls \
--ca-cert ./certs/client-ca.crt \
--ca-cert tests/fixtures/tls/client-ca.crt \
--server-name localhost \
--client-cert ./certs/client-good.crt \
--client-key ./certs/client-good.key
--client-cert tests/fixtures/tls/client-good.crt \
--client-key tests/fixtures/tls/client-good.key
```
如果要在收到错误后继续自动轮询,可以加:
```sh
--keep-after-error
```
## 存储模型与边界约束(多视图)
当前 RTR cache 持久化采用三视图模型(按协议版本拆分):
- v0`RouteOrigin`
- v1`RouteOrigin + RouterKey`
- v2`RouteOrigin + RouterKey + Aspa`
RocksDB 中的核心状态按版本独立保存:
- `session_id_v0/v1/v2`
- `serial_v0/v1/v2`
- `current_v0/v1/v2`snapshot
- `delta_min_v0/v1/v2``delta_max_v0/v1/v2`
- delta key 采用 `D + version + serial_be`
### 唯一写入口
为保证 `session_id/serial/snapshot/delta_window` 的一致性,生产代码只允许通过:
- `RtrStore::save_cache_state_versioned(...)`
且该入口应仅由:
- `src/rtr/cache/store.rs`
发起调用(即由 cache 层统一批量原子写入)。
### 恢复约束
重启恢复时,按版本读取并校验:
- `availability`
- `snapshot_for_version(version)`
- `session_id_for_version(version)`
- `serial_for_version(version)`
- `delta_window_for_version(version)``load_delta_window_for_version(...)`
只有三视图状态完整且自洽时才走 store 恢复;否则回退到输入源加载流程。
### 边界防回归测试
新增边界测试用于限制写调用点,防止后续出现绕写路径:
- `tests/test_store_boundary.rs`
可单独执行:
```sh
cargo test --test test_store_boundary -- --nocapture
```
## Deploy Layout
`deploy/` 目录按角色拆分为四套部署与测试入口:
- `server/`: 本仓库 RTR Server`src/main.rs`)容器化部署
- `client/`: 本仓库 `rtr_debug_client` 容器化部署
- `rpki-rs-client/`: 基于外部 `rpki-rs` client API 的测试客户端容器化部署
- `frr/`: FRR 作为黑盒 RTR Client 的配置与 compose
### Quick Start
最短路径(本地容器环境):
SSH
```bash
docker compose -f deploy/server/docker-compose.yml up -d --build
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
docker compose -f deploy/server/docker-compose.yml down
cargo run --bin rtr_debug_client -- \
127.0.0.1:22 1 reset \
--ssh \
--ssh-user rpki-rtr \
--ssh-key certs/rtr-client.key \
--ssh-server-key certs/ssh_host_rsa_key.pub
```
### Compose 入口
SSH password auth 也已实现SSH 模式下 `--ssh-key``--ssh-password` 二选一,同时必须通过 `--ssh-known-hosts``--ssh-server-key` 做 host key 校验。
| 角色 | Compose 文件 |
更多参数见 `src/bin/rtr_debug_client/README.md`
## 部署入口
| 用途 | Compose 文件 |
| --- | --- |
| Server | `deploy/server/docker-compose.yml` |
| Debug Client单实例 | `deploy/client/docker-compose.yml` |
| Debug Client多实例 | `deploy/client/docker-compose.clients.yml` |
| rpki-rs Client | `deploy/rpki-rs-client/docker-compose.yml` |
| FRR Client | `deploy/frr/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` |
通用操作模板:
通用操作:
```bash
docker compose -f <compose-file> up -d --build
docker compose -f <compose-file> logs -f
docker compose -f <compose-file> down
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
```
### 角色附加命令
## 开发说明
rpki-rs Client覆盖默认参数运行
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml run --rm \
rpki-rs-test-client 127.0.0.1:323 2 reset --steps 1 --assert-min-records 1
```
FRR Client检查连接与前缀
```bash
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
```
更多部署细节:
- `deploy/server/DEPLOYMENT.md`
- `deploy/frr/README.md`
- `deploy/frr/README.zh.md`
## 传输模式说明TCP / TLS / 原生 SSH
当前 `rpki` 进程内支持三种传输:
- TCP默认开启
- TLS可选开启mTLS
- 原生 SSH可选开启进程内实现不再使用外部 `sshd Subsystem` 桥接)
### TCP 模式
默认监听:
- `RPKI_RTR_TCP_ADDR=0.0.0.0:323`
最小启动(仅 TCP
```sh
export RPKI_RTR_ENABLE_TLS=false
export RPKI_RTR_ENABLE_SSH=false
cargo run --bin rpki
```
说明:
- 仅建议部署在受信任、可控网络中。
### TLS 模式mTLS
相关环境变量:
- `RPKI_RTR_ENABLE_TLS=true`
- `RPKI_RTR_TLS_ADDR`(默认 `0.0.0.0:324`
- `RPKI_RTR_TLS_CERT_PATH`
- `RPKI_RTR_TLS_KEY_PATH`
- `RPKI_RTR_TLS_CLIENT_CA_PATH`
最小启动TCP + TLS
```sh
export RPKI_RTR_ENABLE_TLS=true
export RPKI_RTR_ENABLE_SSH=false
export RPKI_RTR_TLS_CERT_PATH=./certs/server-dns.crt
export RPKI_RTR_TLS_KEY_PATH=./certs/server-dns.key
export RPKI_RTR_TLS_CLIENT_CA_PATH=./certs/client-ca.crt
cargo run --bin rpki
```
### 原生 SSH 模式(进程内)
`draft-ietf-sidrops-8210bis-25` 对齐要点:
- 使用 SSHv2
- 使用 subsystem默认 `rpki-rtr`
- 使用 public key 认证
- 服务端拒绝 `none`
- 服务端默认不启用 `password`,配置 `RPKI_RTR_SSH_PASSWORD` 后可选启用draft 中为 MAY
相关环境变量:
- `RPKI_RTR_ENABLE_SSH=true`
- `RPKI_RTR_SSH_ADDR`(默认 `0.0.0.0:22`
- `RPKI_RTR_SSH_PORT`(默认 `22`,设置后会覆盖 `RPKI_RTR_SSH_ADDR` 的端口)
- `RPKI_RTR_SSH_HOST_KEY_PATH`OpenSSH host 私钥)
- `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH`(允许接入的 router 公钥列表)
- `RPKI_RTR_SSH_USERNAME`(默认 `rpki-rtr`
- `RPKI_RTR_SSH_SUBSYSTEM_NAME`(默认 `rpki-rtr`
- `RPKI_RTR_SSH_PASSWORD`(可选;设置后启用 password 认证)
密钥准备示例:
```sh
ssh-keygen -t ed25519 -N '' -f ./certs/ssh_host_ed25519_key
ssh-keygen -t ed25519 -N '' -f ./certs/rtr_client_ed25519_key
cp ./certs/rtr_client_ed25519_key.pub ./certs/rtr-authorized_keys
```
最小启动TCP + SSH
```sh
export RPKI_RTR_ENABLE_TLS=false
export RPKI_RTR_ENABLE_SSH=true
export RPKI_RTR_SSH_ADDR=0.0.0.0:22
# or only override the port:
# export RPKI_RTR_SSH_PORT=2022
export RPKI_RTR_SSH_HOST_KEY_PATH=./certs/ssh_host_ed25519_key
export RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH=./certs/rtr-authorized_keys
export RPKI_RTR_SSH_USERNAME=rpki-rtr
export RPKI_RTR_SSH_SUBSYSTEM_NAME=rpki-rtr
# 可选:启用 password 认证(同时仍支持 publickey
# export RPKI_RTR_SSH_PASSWORD=test-password
cargo run --bin rpki
```
连通性检查OpenSSH
```sh
ssh -i ./certs/rtr_client_ed25519_key -p 22 -s rpki-rtr@127.0.0.1 rpki-rtr
```
说明:
- 该命令主要用于验证 SSH 子系统通道可建立,不等价于完整 RTR 协议回归测试。
- 代码中的 RTR cache 按协议版本维护状态v0 只包含 Route Originv1 包含 Route Origin 和 Router Keyv2 包含 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`

View File

@ -4,11 +4,97 @@
- `server/`: 本仓库 RTR Server`src/main.rs`)容器化部署
- `client/`: 本仓库 `rtr_debug_client` 容器化部署
- `rpki-rs-client/`: 基于外部 `rpki-rs` client API 的测试客户端容器化部署
- `frr/`: FRR 作为黑盒 RTR Client 的配置与 compose
---
## 0) Build and Run Scripts
`deploy/server/build-run.sh` 只管理 server`deploy/client/build-run.sh` 只管理 debug client。两个脚本默认 action 都是 `up`,默认 mode 都是 `tcp`
常用命令:
以下命令默认从仓库根目录执行。
```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 up --clients multi
./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 Build, ARM Deploy
x86 开发机上用 Docker Buildx 分别构建 ARM server/client 镜像。不加 `--push` 时,脚本会把单平台镜像加载到本地 Docker随后可用 `docker save` 打包传到 ARM 机器。buildx 前会自动按目标平台预拉取 `rust:1.89-bookworm``debian:bookworm-slim`
```bash
docker buildx create --use --name rpki-builder
docker buildx inspect --bootstrap
./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
```
把 tar 包传到 ARM 部署机:
```bash
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
```
如果不使用脚本,也可以通过 compose 环境变量指定镜像:
```bash
export RPKI_RTR_SERVER_IMAGE=rpki-rtr:arm64
docker compose --env-file deploy/server/.env \
-f deploy/server/docker-compose.tcp.yml up -d --no-build
```
---
## 1) Server
路径:
@ -117,46 +203,7 @@ docker compose -f deploy/client/docker-compose.clients.yml logs -f
---
## 3) rpki-rs Client
路径:
- `deploy/rpki-rs-client/Dockerfile`
- `deploy/rpki-rs-client/docker-compose.yml`
单独 build 镜像:
```bash
docker build -f deploy/rpki-rs-client/Dockerfile -t rpki-rs-test-client:latest .
```
默认启动(自动 serial 测试):
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml up --build
```
覆盖默认参数运行:
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml run --rm \
rpki-rs-test-client 127.0.0.1:323 2 reset --steps 1 --assert-min-records 1
```
停止:
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml down
```
日志:
```bash
docker compose -f deploy/rpki-rs-client/docker-compose.yml logs -f rpki-rs-test-client
```
---
## 4) FRR Client
## 3) FRR Client
路径:
- `deploy/frr/docker-compose.yml`
@ -192,7 +239,7 @@ docker compose -f deploy/frr/docker-compose.yml logs -f frr-rpki-client
---
## 5) BIRD Client
## 4) BIRD Client
路径:
- `deploy/bird/Dockerfile`

237
deploy/client/build-run.sh Normal file
View 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

View File

@ -2,7 +2,7 @@ version: "3.9"
services:
rtr-client-1:
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
@ -11,7 +11,7 @@ services:
- rpki_net
rtr-client-2:
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
@ -20,7 +20,7 @@ services:
- rpki_net
rtr-client-3:
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
@ -29,7 +29,7 @@ services:
- rpki_net
rtr-client-4:
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
@ -38,7 +38,7 @@ services:
- rpki_net
rtr-client-5:
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command:
[
"${RPKI_RTR_SERVER_ADDR:-rpki-rtr-ssh:22}",

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command:
[
"${RPKI_RTR_SERVER_ADDR:-rpki-rtr-ssh:22}",

View File

@ -3,7 +3,7 @@ services:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command:
[
"${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tls:324}",

View File

@ -3,7 +3,7 @@ services:
build:
context: ../..
dockerfile: deploy/client/Dockerfile
image: rpki-rtr-debug-client:latest
image: ${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client:latest}
command: ["${RPKI_RTR_SERVER_ADDR:-rpki-rtr-tcp:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
volumes:
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs

View File

@ -16,7 +16,6 @@ RPKI_RTR_DB_PATH=/app/rtr-db
RPKI_RTR_REPORT_DIR=/app/report
# Core runtime knobs.
RPKI_RTR_STRICT_CCR_VALIDATION=false
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=300
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300
RPKI_RTR_REPORT_HISTORY_LIMIT=10

View File

@ -35,7 +35,7 @@ The container runs `rpki` directly as PID 1.
## Runtime Configuration via `.env`
- Core: `RPKI_RTR_STRICT_CCR_VALIDATION`, `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`
- 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_KEYS_VOLUME`, `RPKI_RTR_SSH_CERTS_HOST_DIR`

View File

@ -2,6 +2,8 @@ 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
@ -75,7 +77,6 @@ ENV RPKI_RTR_ENABLE_TLS=false \
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300 \
RPKI_RTR_REPORT_HISTORY_LIMIT=10 \
RPKI_RTR_REFRESH_INTERVAL_SECS=300 \
RPKI_RTR_STRICT_CCR_VALIDATION=false \
RPKI_RTR_ADMIN_ADDR="" \
RPKI_RTR_ADMIN_TOKEN=""

215
deploy/server/build-run.sh Normal file
View 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

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/server/Dockerfile
image: rpki-rtr:latest
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
container_name: rpki-rtr-ssh
restart: no
ports:
@ -32,7 +32,6 @@ services:
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_STRICT_CCR_VALIDATION: "${RPKI_RTR_STRICT_CCR_VALIDATION:-false}"
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}"

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/server/Dockerfile
image: rpki-rtr:latest
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
container_name: rpki-rtr-tcp
restart: no
ports:
@ -22,7 +22,6 @@ services:
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_STRICT_CCR_VALIDATION: "${RPKI_RTR_STRICT_CCR_VALIDATION:-false}"
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}"

View File

@ -3,7 +3,7 @@ services:
build:
context: ../..
dockerfile: deploy/server/Dockerfile
image: rpki-rtr:latest
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
container_name: rpki-rtr-tls
restart: no
ports:
@ -26,7 +26,6 @@ services:
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_STRICT_CCR_VALIDATION: "${RPKI_RTR_STRICT_CCR_VALIDATION:-false}"
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}"

View File

@ -5,7 +5,7 @@ services:
build:
context: ../..
dockerfile: deploy/server/Dockerfile
image: rpki-rtr:latest
image: ${RPKI_RTR_SERVER_IMAGE:-rpki-rtr:latest}
container_name: rpki-rtr
restart: no
ports:
@ -25,7 +25,6 @@ services:
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_STRICT_CCR_VALIDATION: "${RPKI_RTR_STRICT_CCR_VALIDATION:-false}"
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}"

View File

@ -105,7 +105,6 @@ curl http://127.0.0.1:8323/admin/rtr/config \
"source_refresh_interval_seconds": 300,
"runtime_report_interval_seconds": 300,
"report_history_limit": 10,
"strict_ccr_validation": false,
"timezone": "Asia/Shanghai",
"timing": {
"refresh": 3600,
@ -127,7 +126,6 @@ curl http://127.0.0.1:8323/admin/rtr/config \
| `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`。 |
| `strict_ccr_validation` | boolean | 是否严格校验 CCR 中的非法 VRP/VAP影响下一次 source refresh。 |
| `timezone` | string | IANA 时区名,例如 `Asia/Shanghai``UTC``Europe/London`。 |
| `timing.refresh` | integer | RTR EndOfData `refresh`,单位秒。 |
| `timing.retry` | integer | RTR EndOfData `retry`,单位秒。 |
@ -153,7 +151,6 @@ curl -X POST http://127.0.0.1:8323/admin/rtr/config \
"source_refresh_interval_seconds": 300,
"runtime_report_interval_seconds": 300,
"report_history_limit": 10,
"strict_ccr_validation": false,
"timezone": "Asia/Shanghai",
"timing": {
"refresh": 3600,

View File

@ -12,7 +12,6 @@ export RPKI_RTR_DB_PATH
export RPKI_RTR_CCR_DIR
: "${RPKI_RTR_MAX_DELTA:=100}"
: "${RPKI_RTR_STRICT_CCR_VALIDATION:=false}"
: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:=300}"
: "${RPKI_RTR_TIMING_REFRESH_SECS:=3600}"
: "${RPKI_RTR_TIMING_RETRY_SECS:=600}"
@ -20,7 +19,6 @@ export RPKI_RTR_CCR_DIR
: "${RPKI_RTR_MAX_CONNECTIONS:=512}"
: "${RPKI_RTR_NOTIFY_QUEUE_SIZE:=1024}"
export RPKI_RTR_MAX_DELTA
export RPKI_RTR_STRICT_CCR_VALIDATION
export RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS
export RPKI_RTR_TIMING_REFRESH_SECS
export RPKI_RTR_TIMING_RETRY_SECS

View File

@ -21,7 +21,6 @@ export RPKI_RTR_TLS_KEY_PATH
export RPKI_RTR_TLS_CLIENT_CA_PATH
: "${RPKI_RTR_MAX_DELTA:=100}"
: "${RPKI_RTR_STRICT_CCR_VALIDATION:=false}"
: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:=300}"
: "${RPKI_RTR_TIMING_REFRESH_SECS:=3600}"
: "${RPKI_RTR_TIMING_RETRY_SECS:=600}"
@ -29,7 +28,6 @@ export RPKI_RTR_TLS_CLIENT_CA_PATH
: "${RPKI_RTR_MAX_CONNECTIONS:=512}"
: "${RPKI_RTR_NOTIFY_QUEUE_SIZE:=1024}"
export RPKI_RTR_MAX_DELTA
export RPKI_RTR_STRICT_CCR_VALIDATION
export RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS
export RPKI_RTR_TIMING_REFRESH_SECS
export RPKI_RTR_TIMING_RETRY_SECS

View File

@ -10,10 +10,8 @@ export RPKI_RTR_TLS_ADDR
: "${RPKI_RTR_DB_PATH:=./rtr-db}"
: "${RPKI_RTR_CCR_DIR:=./data}"
: "${RPKI_RTR_STRICT_CCR_VALIDATION:=false}"
export RPKI_RTR_DB_PATH
export RPKI_RTR_CCR_DIR
export RPKI_RTR_STRICT_CCR_VALIDATION
: "${RPKI_RTR_TLS_CERT_PATH:=./certs/server-dns.crt}"
: "${RPKI_RTR_TLS_KEY_PATH:=./certs/server-dns.key}"

View File

@ -15,7 +15,6 @@ use rpki::source::ccr::{
struct Cli {
ccr_path: PathBuf,
slurm_path: PathBuf,
strict_ccr: bool,
dump_payloads: bool,
}
@ -44,7 +43,7 @@ fn main() -> Result<()> {
.with_context(|| format!("failed to load CCR snapshot: {}", cli.ccr_path.display()))?;
let slurm = load_slurm(&cli.slurm_path)?;
let conversion = snapshot_to_payloads_with_options(&snapshot, cli.strict_ccr)?;
let conversion = snapshot_to_payloads_with_options(&snapshot, true)?;
let payloads = slurm.apply(&conversion.payloads);
let (input_vrp_count, input_vap_count) = count_vrps_and_vaps(&conversion.payloads);
let (output_vrp_count, output_vap_count) = count_vrps_and_vaps(&payloads);
@ -78,13 +77,11 @@ fn load_slurm(path: &PathBuf) -> Result<SlurmFile> {
}
fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
let mut strict_ccr = false;
let mut dump_payloads = false;
let mut positionals = Vec::new();
for arg in args {
match arg.as_str() {
"--strict-ccr" => strict_ccr = true,
"--dump-payloads" => dump_payloads = true,
"-h" | "--help" => {
print_help();
@ -102,7 +99,6 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
ccr_path: find_latest_ccr_file("data")
.context("failed to find latest .ccr in ./data for default run")?,
slurm_path: PathBuf::from("data/example.slurm"),
strict_ccr,
dump_payloads,
});
}
@ -117,14 +113,13 @@ fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
Ok(Cli {
ccr_path: PathBuf::from(&positionals[0]),
slurm_path: PathBuf::from(&positionals[1]),
strict_ccr,
dump_payloads,
})
}
fn print_help() {
eprintln!(
"Usage: cargo run --bin slurm_apply_client -- [--strict-ccr] [--dump-payloads] <snapshot.ccr> <policy.slurm>"
"Usage: cargo run --bin slurm_apply_client -- [--dump-payloads] <snapshot.ccr> <policy.slurm>"
);
eprintln!();
eprintln!("Reads a CCR snapshot, converts it into payloads, applies SLURM, and prints JSON.");

View File

@ -1,28 +1,17 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use anyhow::Result;
use arc_swap::ArcSwap;
use chrono::Utc;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{info, warn};
use tracing::info;
use rpki::rtr::admin::{
AdminState, LogTailConfig, RuntimeConfigHandle, SourceReloadHandle, SourceReloadResult,
spawn_admin_config_server,
AdminState, LogTailConfig, RuntimeConfigHandle, SourceReloadHandle, spawn_admin_config_server,
};
use rpki::rtr::cache::{RtrCache, SharedRtrCache, Snapshot};
use rpki::rtr::config::{AppConfig, RuntimeConfig, log_startup_config};
use rpki::rtr::report::{ReportConfiguration, ReportContext, current_rss_mib};
use rpki::rtr::server::{RtrNotifier, RtrService, RtrServiceStats, RunningRtrService};
use rpki::rtr::store::RtrStore;
use rpki::rtr::bootstrap::{init_shared_cache, open_store, start_servers};
use rpki::rtr::config::{AppConfig, log_startup_config};
use rpki::rtr::report::{ReportConfiguration, ReportContext};
use rpki::rtr::runtime::spawn_refresh_task;
use rpki::rtr::server::RtrService;
use rpki::slurm::admin::SlurmAdmin;
use rpki::source::pipeline::{
PayloadLoadConfig, SourceFingerprint, latest_sources_fingerprint,
load_payloads_from_latest_sources_with_report,
};
#[tokio::main]
async fn main() -> Result<()> {
@ -36,7 +25,6 @@ async fn main() -> Result<()> {
config.report_history_limit,
config.max_delta,
config.prune_delta_by_snapshot_size,
config.strict_ccr_validation,
config.timezone,
(
config.timing.refresh,
@ -92,488 +80,6 @@ async fn main() -> Result<()> {
Ok(())
}
fn open_store(config: &AppConfig) -> Result<RtrStore> {
info!("opening RTR store: {}", config.db_path);
RtrStore::open(&config.db_path)
}
fn init_shared_cache(
config: &AppConfig,
store: &RtrStore,
report_context: &ReportContext,
) -> Result<SharedRtrCache> {
let payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
strict_ccr_validation: config.strict_ccr_validation,
};
let source_to_delta_started = Instant::now();
let report_context_for_loader = report_context.clone();
let initial_cache = RtrCache::default().init(
store,
config.max_delta,
config.prune_delta_by_snapshot_size,
config.timing,
|| {
let load = load_payloads_from_latest_sources_with_report(&payload_load_config)?;
info!(
"RTR source-to-delta timing: phase=startup_load_complete, ccr_dir={}, payload_count={}, elapsed_ms={}",
payload_load_config.ccr_dir,
load.payloads.len(),
source_to_delta_started.elapsed().as_millis()
);
report_context_for_loader.record_refresh_success(
Utc::now(),
source_to_delta_started.elapsed().as_millis(),
true,
load.source,
load.quality,
);
Ok(load.payloads)
},
)?;
info!(
"RTR source-to-delta timing: phase=startup_cache_init_complete, ccr_dir={}, serials={:?}, elapsed_ms={}",
payload_load_config.ccr_dir,
initial_cache.serials(),
source_to_delta_started.elapsed().as_millis()
);
let shared_cache: SharedRtrCache = Arc::new(ArcSwap::from_pointee(initial_cache));
let cache = shared_cache.load_full();
info!(
"cache initialized: session_ids={:?}, serials={:?}",
cache.session_ids(),
cache.serials()
);
Ok(shared_cache)
}
fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService {
if config.enable_tls && config.enable_ssh {
info!("starting TCP, TLS and SSH RTR servers");
service.spawn_tcp_tls_and_ssh_from_pem_and_openssh(
config.tcp_addr,
config.tls_addr,
config.ssh_addr,
&config.tls_cert_path,
&config.tls_key_path,
&config.tls_client_ca_path,
&config.ssh_host_key_path,
&config.ssh_authorized_keys_path,
&config.ssh_username,
&config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(),
)
} else if config.enable_tls {
info!("starting TCP and TLS RTR servers");
service.spawn_tcp_and_tls_from_pem(
config.tcp_addr,
config.tls_addr,
&config.tls_cert_path,
&config.tls_key_path,
&config.tls_client_ca_path,
)
} else if config.enable_ssh {
info!("starting TCP and SSH RTR servers");
service.spawn_tcp_and_ssh_from_openssh(
config.tcp_addr,
config.ssh_addr,
&config.ssh_host_key_path,
&config.ssh_authorized_keys_path,
&config.ssh_username,
&config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(),
)
} else {
info!("starting TCP RTR server");
service.spawn_tcp_only(config.tcp_addr)
}
}
fn spawn_refresh_task(
config: &AppConfig,
runtime_config: RuntimeConfigHandle,
mut source_reload_rx: mpsc::Receiver<rpki::rtr::admin::SourceReloadCommand>,
shared_cache: SharedRtrCache,
store: RtrStore,
notifier: RtrNotifier,
service_stats: RtrServiceStats,
report_context: ReportContext,
) -> JoinHandle<()> {
let report_dir = PathBuf::from(&config.report_dir);
let mut payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
strict_ccr_validation: config.strict_ccr_validation,
};
let initial_runtime_config = runtime_config.current();
tokio::spawn(async move {
let mut active_runtime_config = initial_runtime_config;
let mut config_rx = runtime_config.subscribe();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
active_runtime_config.source_refresh_interval_seconds,
));
let mut last_fingerprint: Option<SourceFingerprint> = None;
report_context.write_source_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
report_context.write_clients_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
report_context.write_runtime_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
let mut runtime_interval = tokio::time::interval_at(
tokio::time::Instant::now()
+ std::time::Duration::from_secs(
active_runtime_config.runtime_report_interval_seconds,
),
std::time::Duration::from_secs(active_runtime_config.runtime_report_interval_seconds),
);
runtime_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut client_change_rx = service_stats.subscribe_connection_changes();
loop {
tokio::select! {
changed = config_rx.changed() => {
match changed {
Ok(()) => {
let next_config = config_rx.borrow().clone();
apply_runtime_config_update(
&mut active_runtime_config,
next_config,
&shared_cache,
&store,
&report_context,
&mut payload_load_config,
&mut interval,
&mut runtime_interval,
);
report_context.write_runtime_or_warn(&report_dir, "admin_config_changed", &shared_cache, &notifier, &service_stats);
}
Err(_) => {
warn!("RTR runtime config change channel closed");
}
}
continue;
}
changed = client_change_rx.changed() => {
match changed {
Ok(()) => {
report_context.write_clients_or_warn(&report_dir, "clients_changed", &shared_cache, &notifier, &service_stats);
}
Err(_) => {
warn!("RTR client connection change channel closed");
}
}
continue;
}
_ = runtime_interval.tick() => {
log_cache_memory_stats("periodic_observe", &shared_cache, &notifier);
report_context.write_runtime_or_warn(&report_dir, "runtime_periodic", &shared_cache, &notifier, &service_stats);
continue;
}
_ = interval.tick() => {}
command = source_reload_rx.recv() => {
let Some(command) = command else {
warn!("RTR source reload admin channel closed");
continue;
};
let result = perform_source_refresh(
command.phase,
command.force,
&payload_load_config,
&shared_cache,
&store,
&notifier,
&service_stats,
&report_context,
&report_dir,
&mut last_fingerprint,
);
let _ = command.respond_to.send(result.map_err(|err| err.to_string()));
continue;
}
}
let _ = perform_source_refresh(
"refresh_complete",
false,
&payload_load_config,
&shared_cache,
&store,
&notifier,
&service_stats,
&report_context,
&report_dir,
&mut last_fingerprint,
);
}
})
}
#[allow(clippy::too_many_arguments)]
fn perform_source_refresh(
phase: &'static str,
force: bool,
payload_load_config: &PayloadLoadConfig,
shared_cache: &SharedRtrCache,
store: &RtrStore,
notifier: &RtrNotifier,
service_stats: &RtrServiceStats,
report_context: &ReportContext,
report_dir: &PathBuf,
last_fingerprint: &mut Option<SourceFingerprint>,
) -> Result<SourceReloadResult> {
let source_to_delta_started = Instant::now();
let attempted_at = Utc::now();
let current_fingerprint = match latest_sources_fingerprint(payload_load_config) {
Ok(fp) => fp,
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!(
"failed to fingerprint CCR/SLURM sources from {}: {:?} (source_to_delta_elapsed_ms={})",
payload_load_config.ccr_dir,
err,
source_to_delta_started.elapsed().as_millis()
);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
report_context.record_source_fingerprint(current_fingerprint.clone());
if !force && last_fingerprint.as_ref() == Some(&current_fingerprint) {
report_context
.record_refresh_unchanged(attempted_at, source_to_delta_started.elapsed().as_millis());
info!(
"RTR source refresh skipped: source files unchanged (ccr_path={}, slurm_file_count={}, elapsed_ms={})",
current_fingerprint.ccr.path,
current_fingerprint.slurm_files.len(),
source_to_delta_started.elapsed().as_millis()
);
log_cache_memory_stats("refresh_skipped_unchanged", shared_cache, notifier);
report_context.write_source_or_warn(
report_dir,
"refresh_skipped_unchanged",
shared_cache,
notifier,
service_stats,
);
return Ok(SourceReloadResult {
phase,
changed: false,
skipped_unchanged: true,
payload_count: None,
serials: shared_cache.load_full().serials(),
});
}
let load = match load_payloads_from_latest_sources_with_report(payload_load_config) {
Ok(load) => load,
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!(
"failed to reload CCR/SLURM payloads from {}: {:?} (source_to_delta_elapsed_ms={})",
payload_load_config.ccr_dir,
err,
source_to_delta_started.elapsed().as_millis()
);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
let source = load.source;
let quality = load.quality;
let payloads = load.payloads;
let payload_count = payloads.len();
let source_snapshot = Snapshot::from_payloads(payloads);
let old_cache = shared_cache.load_full();
let old_serial = old_cache.serial_for_version(2);
let mut next_cache = old_cache.as_ref().clone();
let updated = match next_cache.update_with_snapshot(source_snapshot, store) {
Ok(()) => {
let new_serial = next_cache.serial_for_version(2);
shared_cache.store(std::sync::Arc::new(next_cache));
if new_serial != old_serial {
info!(
"RTR cache refresh applied: ccr_dir={}, payload_count={}, old_serial={}, new_serial={}",
payload_load_config.ccr_dir, payload_count, old_serial, new_serial
);
true
} else {
info!(
"RTR cache refresh found no change: ccr_dir={}, payload_count={}, serial={}",
payload_load_config.ccr_dir, payload_count, old_serial
);
false
}
}
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!("RTR cache update failed: {:?}", err);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
report_context.record_refresh_success(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
updated,
source,
quality,
);
info!(
"RTR source-to-delta timing: phase={}, ccr_dir={}, payload_count={}, changed={}, elapsed_ms={}",
phase,
payload_load_config.ccr_dir,
payload_count,
updated,
source_to_delta_started.elapsed().as_millis()
);
if updated {
let listener_count = notifier.notify_cache_updated();
info!(
"RTR cache updated, notify signal emitted to session listeners: listener_count={}",
listener_count
);
}
log_cache_memory_stats(phase, shared_cache, notifier);
report_context.write_source_or_warn(report_dir, phase, shared_cache, notifier, service_stats);
last_fingerprint.replace(current_fingerprint);
Ok(SourceReloadResult {
phase,
changed: updated,
skipped_unchanged: false,
payload_count: Some(payload_count),
serials: shared_cache.load_full().serials(),
})
}
fn apply_runtime_config_update(
active: &mut RuntimeConfig,
next: RuntimeConfig,
shared_cache: &SharedRtrCache,
store: &RtrStore,
report_context: &ReportContext,
payload_load_config: &mut PayloadLoadConfig,
refresh_interval: &mut tokio::time::Interval,
runtime_interval: &mut tokio::time::Interval,
) {
let old = active.clone();
*active = next.clone();
payload_load_config.strict_ccr_validation = next.strict_ccr_validation;
report_context.update_runtime_config(&next);
{
let old_cache = shared_cache.load_full();
let mut next_cache = old_cache.as_ref().clone();
next_cache.update_runtime_config(
next.max_delta,
next.prune_delta_by_snapshot_size,
next.timing(),
store,
);
shared_cache.store(std::sync::Arc::new(next_cache));
}
if old.source_refresh_interval_seconds != next.source_refresh_interval_seconds {
*refresh_interval = tokio::time::interval(std::time::Duration::from_secs(
next.source_refresh_interval_seconds,
));
refresh_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
}
if old.runtime_report_interval_seconds != next.runtime_report_interval_seconds {
*runtime_interval = tokio::time::interval_at(
tokio::time::Instant::now()
+ std::time::Duration::from_secs(next.runtime_report_interval_seconds),
std::time::Duration::from_secs(next.runtime_report_interval_seconds),
);
runtime_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
}
info!(
"RTR runtime config applied: max_delta={}, prune_delta_by_snapshot_size={}, source_refresh_interval_seconds={}, runtime_report_interval_seconds={}, report_history_limit={}, strict_ccr_validation={}, timezone={}, timing=({}, {}, {})",
next.max_delta,
next.prune_delta_by_snapshot_size,
next.source_refresh_interval_seconds,
next.runtime_report_interval_seconds,
next.report_history_limit,
next.strict_ccr_validation,
next.timezone,
next.timing.refresh,
next.timing.retry,
next.timing.expire
);
}
fn log_cache_memory_stats(phase: &str, shared_cache: &SharedRtrCache, notifier: &RtrNotifier) {
let cache = shared_cache.load_full();
let stats = cache.memory_stats();
let rss_mib = current_rss_mib();
info!(
"RTR memory observe: phase={}, listener_count={}, serials={:?}, snapshot_payload_counts={:?}, delta_lengths={:?}, delta_payload_counts={:?}, rss_mib={:?}",
phase,
notifier.listener_count(),
stats.serials,
stats.snapshot_payload_counts,
stats.delta_lengths,
stats.delta_payload_counts,
rss_mib
);
}
async fn wait_for_shutdown() -> Result<()> {
tokio::signal::ctrl_c().await?;
info!("shutdown signal received");

115
src/rtr/bootstrap.rs Normal file
View File

@ -0,0 +1,115 @@
use std::sync::Arc;
use std::time::Instant;
use anyhow::Result;
use arc_swap::ArcSwap;
use chrono::Utc;
use tracing::info;
use crate::rtr::cache::{RtrCache, SharedRtrCache};
use crate::rtr::config::AppConfig;
use crate::rtr::report::ReportContext;
use crate::rtr::server::{RtrService, RunningRtrService};
use crate::rtr::store::RtrStore;
use crate::source::pipeline::{PayloadLoadConfig, load_payloads_from_latest_sources_with_report};
pub fn open_store(config: &AppConfig) -> Result<RtrStore> {
info!("opening RTR store: {}", config.db_path);
RtrStore::open(&config.db_path)
}
pub fn init_shared_cache(
config: &AppConfig,
store: &RtrStore,
report_context: &ReportContext,
) -> Result<SharedRtrCache> {
let payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
};
let source_to_delta_started = Instant::now();
let report_context_for_loader = report_context.clone();
let initial_cache = RtrCache::default().init(
store,
config.max_delta,
config.prune_delta_by_snapshot_size,
config.timing,
|| {
let load = load_payloads_from_latest_sources_with_report(&payload_load_config)?;
info!(
"RTR source-to-delta timing: phase=startup_load_complete, ccr_dir={}, payload_count={}, elapsed_ms={}",
payload_load_config.ccr_dir,
load.payloads.len(),
source_to_delta_started.elapsed().as_millis()
);
report_context_for_loader.record_refresh_success(
Utc::now(),
source_to_delta_started.elapsed().as_millis(),
true,
load.source,
load.quality,
);
Ok(load.payloads)
},
)?;
info!(
"RTR source-to-delta timing: phase=startup_cache_init_complete, ccr_dir={}, serials={:?}, elapsed_ms={}",
payload_load_config.ccr_dir,
initial_cache.serials(),
source_to_delta_started.elapsed().as_millis()
);
let shared_cache: SharedRtrCache = Arc::new(ArcSwap::from_pointee(initial_cache));
let cache = shared_cache.load_full();
info!(
"cache initialized: session_ids={:?}, serials={:?}",
cache.session_ids(),
cache.serials()
);
Ok(shared_cache)
}
pub fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService {
if config.enable_tls && config.enable_ssh {
info!("starting TCP, TLS and SSH RTR servers");
service.spawn_tcp_tls_and_ssh_from_pem_and_openssh(
config.tcp_addr,
config.tls_addr,
config.ssh_addr,
&config.tls_cert_path,
&config.tls_key_path,
&config.tls_client_ca_path,
&config.ssh_host_key_path,
&config.ssh_authorized_keys_path,
&config.ssh_username,
&config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(),
)
} else if config.enable_tls {
info!("starting TCP and TLS RTR servers");
service.spawn_tcp_and_tls_from_pem(
config.tcp_addr,
config.tls_addr,
&config.tls_cert_path,
&config.tls_key_path,
&config.tls_client_ca_path,
)
} else if config.enable_ssh {
info!("starting TCP and SSH RTR servers");
service.spawn_tcp_and_ssh_from_openssh(
config.tcp_addr,
config.ssh_addr,
&config.ssh_host_key_path,
&config.ssh_authorized_keys_path,
&config.ssh_username,
&config.ssh_subsystem_name,
config.ssh_auth_mode,
config.ssh_password.as_deref(),
)
} else {
info!("starting TCP RTR server");
service.spawn_tcp_only(config.tcp_addr)
}
}

View File

@ -34,7 +34,6 @@ pub struct AppConfig {
pub max_delta: u8,
pub prune_delta_by_snapshot_size: bool,
pub strict_ccr_validation: bool,
pub source_refresh_interval: Duration,
pub report_dir: String,
pub runtime_report_interval: Duration,
@ -54,7 +53,6 @@ pub struct RuntimeConfig {
pub source_refresh_interval_seconds: u64,
pub runtime_report_interval_seconds: u64,
pub report_history_limit: usize,
pub strict_ccr_validation: bool,
pub timezone: String,
pub timing: RuntimeTimingConfig,
}
@ -73,7 +71,6 @@ pub struct RuntimeConfigPatch {
pub source_refresh_interval_seconds: Option<u64>,
pub runtime_report_interval_seconds: Option<u64>,
pub report_history_limit: Option<usize>,
pub strict_ccr_validation: Option<bool>,
pub timezone: Option<String>,
pub timing: Option<RuntimeTimingConfigPatch>,
}
@ -109,7 +106,6 @@ impl Default for AppConfig {
max_delta: 100,
prune_delta_by_snapshot_size: false,
strict_ccr_validation: false,
source_refresh_interval: Duration::from_secs(300),
report_dir: "./report".to_string(),
runtime_report_interval: Duration::from_secs(300),
@ -227,9 +223,6 @@ impl AppConfig {
config.prune_delta_by_snapshot_size =
parse_bool(&value, "RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")?;
}
if let Some(value) = env_var("RPKI_RTR_STRICT_CCR_VALIDATION")? {
config.strict_ccr_validation = parse_bool(&value, "RPKI_RTR_STRICT_CCR_VALIDATION")?;
}
if let Some(value) = env_var("RPKI_RTR_REPORT_DIR")? {
config.report_dir = value;
}
@ -371,7 +364,6 @@ impl AppConfig {
source_refresh_interval_seconds: self.source_refresh_interval.as_secs(),
runtime_report_interval_seconds: self.runtime_report_interval.as_secs(),
report_history_limit: self.report_history_limit,
strict_ccr_validation: self.strict_ccr_validation,
timezone: format_timezone(self.timezone),
timing: RuntimeTimingConfig::from(self.timing),
}
@ -417,9 +409,6 @@ impl RuntimeConfig {
}
next.report_history_limit = value;
}
if let Some(value) = patch.strict_ccr_validation {
next.strict_ccr_validation = value;
}
if let Some(value) = patch.timezone {
next.timezone = format_timezone(parse_timezone(&value, "timezone")?);
}
@ -499,7 +488,7 @@ pub fn log_startup_config(config: &AppConfig) {
config.slurm_dir.as_deref().unwrap_or("disabled")
);
info!("max_delta={}", config.max_delta);
info!("strict_ccr_validation={}", config.strict_ccr_validation);
info!("strict_ccr_validation=true");
info!(
"source_refresh_interval_secs={}",
config.source_refresh_interval.as_secs()

View File

@ -1,10 +1,12 @@
pub mod admin;
pub mod bootstrap;
pub mod cache;
pub mod config;
pub mod error_type;
pub mod payload;
pub mod pdu;
pub mod report;
pub mod runtime;
pub mod server;
pub mod session;
pub mod state;

View File

@ -23,7 +23,6 @@ pub struct ReportConfiguration {
report_history_limit: usize,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
strict_ccr_validation: bool,
timezone: String,
timing: TimingReport,
}
@ -42,7 +41,6 @@ impl ReportConfiguration {
report_history_limit: usize,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
strict_ccr_validation: bool,
timezone: Tz,
timing: (u32, u32, u32),
) -> Self {
@ -52,7 +50,6 @@ impl ReportConfiguration {
report_history_limit,
max_delta,
prune_delta_by_snapshot_size,
strict_ccr_validation,
timezone: format_timezone(timezone),
timing: TimingReport {
refresh: timing.0,
@ -169,7 +166,6 @@ impl ReportContext {
config.report_history_limit,
config.max_delta,
config.prune_delta_by_snapshot_size,
config.strict_ccr_validation,
timezone,
(
config.timing.refresh,

394
src/rtr/runtime.rs Normal file
View File

@ -0,0 +1,394 @@
use std::path::PathBuf;
use std::time::Instant;
use anyhow::Result;
use chrono::Utc;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{info, warn};
use crate::rtr::admin::{RuntimeConfigHandle, SourceReloadCommand, SourceReloadResult};
use crate::rtr::cache::{SharedRtrCache, Snapshot};
use crate::rtr::config::{AppConfig, RuntimeConfig};
use crate::rtr::report::{ReportContext, current_rss_mib};
use crate::rtr::server::{RtrNotifier, RtrServiceStats};
use crate::rtr::store::RtrStore;
use crate::source::pipeline::{
PayloadLoadConfig, SourceFingerprint, latest_sources_fingerprint,
load_payloads_from_latest_sources_with_report,
};
pub fn spawn_refresh_task(
config: &AppConfig,
runtime_config: RuntimeConfigHandle,
mut source_reload_rx: mpsc::Receiver<SourceReloadCommand>,
shared_cache: SharedRtrCache,
store: RtrStore,
notifier: RtrNotifier,
service_stats: RtrServiceStats,
report_context: ReportContext,
) -> JoinHandle<()> {
let report_dir = PathBuf::from(&config.report_dir);
let payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
};
let initial_runtime_config = runtime_config.current();
tokio::spawn(async move {
let mut active_runtime_config = initial_runtime_config;
let mut config_rx = runtime_config.subscribe();
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
active_runtime_config.source_refresh_interval_seconds,
));
let mut last_fingerprint: Option<SourceFingerprint> = None;
report_context.write_source_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
report_context.write_clients_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
report_context.write_runtime_or_warn(
&report_dir,
"startup",
&shared_cache,
&notifier,
&service_stats,
);
let mut runtime_interval = tokio::time::interval_at(
tokio::time::Instant::now()
+ std::time::Duration::from_secs(
active_runtime_config.runtime_report_interval_seconds,
),
std::time::Duration::from_secs(active_runtime_config.runtime_report_interval_seconds),
);
runtime_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut client_change_rx = service_stats.subscribe_connection_changes();
loop {
tokio::select! {
changed = config_rx.changed() => {
match changed {
Ok(()) => {
let next_config = config_rx.borrow().clone();
apply_runtime_config_update(
&mut active_runtime_config,
next_config,
&shared_cache,
&store,
&report_context,
&mut interval,
&mut runtime_interval,
);
report_context.write_runtime_or_warn(&report_dir, "admin_config_changed", &shared_cache, &notifier, &service_stats);
}
Err(_) => {
warn!("RTR runtime config change channel closed");
}
}
continue;
}
changed = client_change_rx.changed() => {
match changed {
Ok(()) => {
report_context.write_clients_or_warn(&report_dir, "clients_changed", &shared_cache, &notifier, &service_stats);
}
Err(_) => {
warn!("RTR client connection change channel closed");
}
}
continue;
}
_ = runtime_interval.tick() => {
log_cache_memory_stats("periodic_observe", &shared_cache, &notifier);
report_context.write_runtime_or_warn(&report_dir, "runtime_periodic", &shared_cache, &notifier, &service_stats);
continue;
}
_ = interval.tick() => {}
command = source_reload_rx.recv() => {
let Some(command) = command else {
warn!("RTR source reload admin channel closed");
continue;
};
let result = perform_source_refresh(
command.phase,
command.force,
&payload_load_config,
&shared_cache,
&store,
&notifier,
&service_stats,
&report_context,
&report_dir,
&mut last_fingerprint,
);
let _ = command.respond_to.send(result.map_err(|err| err.to_string()));
continue;
}
}
let _ = perform_source_refresh(
"refresh_complete",
false,
&payload_load_config,
&shared_cache,
&store,
&notifier,
&service_stats,
&report_context,
&report_dir,
&mut last_fingerprint,
);
}
})
}
#[allow(clippy::too_many_arguments)]
fn perform_source_refresh(
phase: &'static str,
force: bool,
payload_load_config: &PayloadLoadConfig,
shared_cache: &SharedRtrCache,
store: &RtrStore,
notifier: &RtrNotifier,
service_stats: &RtrServiceStats,
report_context: &ReportContext,
report_dir: &PathBuf,
last_fingerprint: &mut Option<SourceFingerprint>,
) -> Result<SourceReloadResult> {
let source_to_delta_started = Instant::now();
let attempted_at = Utc::now();
let current_fingerprint = match latest_sources_fingerprint(payload_load_config) {
Ok(fp) => fp,
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!(
"failed to fingerprint CCR/SLURM sources from {}: {:?} (source_to_delta_elapsed_ms={})",
payload_load_config.ccr_dir,
err,
source_to_delta_started.elapsed().as_millis()
);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
report_context.record_source_fingerprint(current_fingerprint.clone());
if !force && last_fingerprint.as_ref() == Some(&current_fingerprint) {
report_context
.record_refresh_unchanged(attempted_at, source_to_delta_started.elapsed().as_millis());
info!(
"RTR source refresh skipped: source files unchanged (ccr_path={}, slurm_file_count={}, elapsed_ms={})",
current_fingerprint.ccr.path,
current_fingerprint.slurm_files.len(),
source_to_delta_started.elapsed().as_millis()
);
log_cache_memory_stats("refresh_skipped_unchanged", shared_cache, notifier);
report_context.write_source_or_warn(
report_dir,
"refresh_skipped_unchanged",
shared_cache,
notifier,
service_stats,
);
return Ok(SourceReloadResult {
phase,
changed: false,
skipped_unchanged: true,
payload_count: None,
serials: shared_cache.load_full().serials(),
});
}
let load = match load_payloads_from_latest_sources_with_report(payload_load_config) {
Ok(load) => load,
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!(
"failed to reload CCR/SLURM payloads from {}: {:?} (source_to_delta_elapsed_ms={})",
payload_load_config.ccr_dir,
err,
source_to_delta_started.elapsed().as_millis()
);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
let source = load.source;
let quality = load.quality;
let payloads = load.payloads;
let payload_count = payloads.len();
let source_snapshot = Snapshot::from_payloads(payloads);
let old_cache = shared_cache.load_full();
let old_serial = old_cache.serial_for_version(2);
let mut next_cache = old_cache.as_ref().clone();
let updated = match next_cache.update_with_snapshot(source_snapshot, store) {
Ok(()) => {
let new_serial = next_cache.serial_for_version(2);
shared_cache.store(std::sync::Arc::new(next_cache));
if new_serial != old_serial {
info!(
"RTR cache refresh applied: ccr_dir={}, payload_count={}, old_serial={}, new_serial={}",
payload_load_config.ccr_dir, payload_count, old_serial, new_serial
);
true
} else {
info!(
"RTR cache refresh found no change: ccr_dir={}, payload_count={}, serial={}",
payload_load_config.ccr_dir, payload_count, old_serial
);
false
}
}
Err(err) => {
report_context.record_refresh_failure(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
&err,
);
warn!("RTR cache update failed: {:?}", err);
report_context.write_source_or_warn(
report_dir,
"refresh_failed",
shared_cache,
notifier,
service_stats,
);
return Err(err);
}
};
report_context.record_refresh_success(
attempted_at,
source_to_delta_started.elapsed().as_millis(),
updated,
source,
quality,
);
info!(
"RTR source-to-delta timing: phase={}, ccr_dir={}, payload_count={}, changed={}, elapsed_ms={}",
phase,
payload_load_config.ccr_dir,
payload_count,
updated,
source_to_delta_started.elapsed().as_millis()
);
if updated {
let listener_count = notifier.notify_cache_updated();
info!(
"RTR cache updated, notify signal emitted to session listeners: listener_count={}",
listener_count
);
}
log_cache_memory_stats(phase, shared_cache, notifier);
report_context.write_source_or_warn(report_dir, phase, shared_cache, notifier, service_stats);
last_fingerprint.replace(current_fingerprint);
Ok(SourceReloadResult {
phase,
changed: updated,
skipped_unchanged: false,
payload_count: Some(payload_count),
serials: shared_cache.load_full().serials(),
})
}
fn apply_runtime_config_update(
active: &mut RuntimeConfig,
next: RuntimeConfig,
shared_cache: &SharedRtrCache,
store: &RtrStore,
report_context: &ReportContext,
refresh_interval: &mut tokio::time::Interval,
runtime_interval: &mut tokio::time::Interval,
) {
let old = active.clone();
*active = next.clone();
report_context.update_runtime_config(&next);
{
let old_cache = shared_cache.load_full();
let mut next_cache = old_cache.as_ref().clone();
next_cache.update_runtime_config(
next.max_delta,
next.prune_delta_by_snapshot_size,
next.timing(),
store,
);
shared_cache.store(std::sync::Arc::new(next_cache));
}
if old.source_refresh_interval_seconds != next.source_refresh_interval_seconds {
*refresh_interval = tokio::time::interval(std::time::Duration::from_secs(
next.source_refresh_interval_seconds,
));
refresh_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
}
if old.runtime_report_interval_seconds != next.runtime_report_interval_seconds {
*runtime_interval = tokio::time::interval_at(
tokio::time::Instant::now()
+ std::time::Duration::from_secs(next.runtime_report_interval_seconds),
std::time::Duration::from_secs(next.runtime_report_interval_seconds),
);
runtime_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
}
info!(
"RTR runtime config applied: max_delta={}, prune_delta_by_snapshot_size={}, source_refresh_interval_seconds={}, runtime_report_interval_seconds={}, report_history_limit={}, timezone={}, timing=({}, {}, {})",
next.max_delta,
next.prune_delta_by_snapshot_size,
next.source_refresh_interval_seconds,
next.runtime_report_interval_seconds,
next.report_history_limit,
next.timezone,
next.timing.refresh,
next.timing.retry,
next.timing.expire
);
}
fn log_cache_memory_stats(phase: &str, shared_cache: &SharedRtrCache, notifier: &RtrNotifier) {
let cache = shared_cache.load_full();
let stats = cache.memory_stats();
let rss_mib = current_rss_mib();
info!(
"RTR memory observe: phase={}, listener_count={}, serials={:?}, snapshot_payload_counts={:?}, delta_lengths={:?}, delta_payload_counts={:?}, rss_mib={:?}",
phase,
notifier.listener_count(),
stats.serials,
stats.snapshot_payload_counts,
stats.delta_lengths,
stats.delta_payload_counts,
rss_mib
);
}

View File

@ -15,7 +15,6 @@ use crate::source::ccr::{
pub struct PayloadLoadConfig {
pub ccr_dir: String,
pub slurm_dir: Option<String>,
pub strict_ccr_validation: bool,
}
#[derive(Debug, Clone, Eq, PartialEq)]
@ -107,7 +106,7 @@ pub fn load_payloads_from_latest_sources_with_report(
aspa: snapshot.vaps.len(),
};
let produced_at = snapshot.produced_at.clone();
let conversion = snapshot_to_payloads_with_options(&snapshot, config.strict_ccr_validation)?;
let conversion = snapshot_to_payloads_with_options(&snapshot, true)?;
let invalid = PayloadTypeCounts {
total: conversion.invalid_vrps.len() + conversion.invalid_vaps.len(),
vrp: conversion.invalid_vrps.len(),
@ -147,13 +146,12 @@ pub fn load_payloads_from_latest_sources_with_report(
let after_slurm = PayloadTypeCounts::from_payloads(&payloads);
info!(
"loaded latest CCR snapshot: file={}, produced_at={:?}, vrp_count={}, vap_count={}, payload_count={}, strict_ccr_validation={}",
"loaded latest CCR snapshot: file={}, produced_at={:?}, vrp_count={}, vap_count={}, payload_count={}, strict_ccr_validation=true",
latest.display(),
produced_at,
snapshot.vrps.len(),
snapshot.vaps.len(),
before_slurm.total,
config.strict_ccr_validation
before_slurm.total
);
Ok(PayloadLoadResult {

View File

@ -36,7 +36,6 @@ fn load_payloads_rejects_entire_slurm_set_when_any_file_is_invalid() {
let config = PayloadLoadConfig {
ccr_dir: data_dir(),
slurm_dir: Some(slurm_dir.path().to_string_lossy().to_string()),
strict_ccr_validation: false,
};
let err = load_payloads_from_latest_sources(&config).unwrap_err();
@ -50,7 +49,6 @@ fn load_report_contains_source_and_quality_details() {
let config = PayloadLoadConfig {
ccr_dir: data_dir(),
slurm_dir: None,
strict_ccr_validation: false,
};
let result = load_payloads_from_latest_sources_with_report(&config).unwrap();
@ -85,7 +83,6 @@ fn load_report_contains_slurm_metadata_and_rule_counts() {
let config = PayloadLoadConfig {
ccr_dir: data_dir(),
slurm_dir: Some(slurm_dir.path().to_string_lossy().to_string()),
strict_ccr_validation: false,
};
let result = load_payloads_from_latest_sources_with_report(&config).unwrap();

View File

@ -39,7 +39,6 @@ fn runtime_config_patch_validates_and_preserves_unspecified_values() {
source_refresh_interval_seconds: Some(60),
runtime_report_interval_seconds: Some(120),
report_history_limit: Some(20),
strict_ccr_validation: Some(true),
timezone: Some("UTC".to_string()),
timing: Some(RuntimeTimingConfigPatch {
refresh: Some(1800),
@ -54,7 +53,6 @@ fn runtime_config_patch_validates_and_preserves_unspecified_values() {
assert_eq!(next.source_refresh_interval_seconds, 60);
assert_eq!(next.runtime_report_interval_seconds, 120);
assert_eq!(next.report_history_limit, 20);
assert!(next.strict_ccr_validation);
assert_eq!(next.timezone, "UTC");
assert_eq!(next.timing.refresh, 1800);
assert_eq!(next.timing.retry, current.timing.retry);

View File

@ -20,7 +20,6 @@ fn test_configuration(report_history_limit: usize) -> ReportConfiguration {
report_history_limit,
100,
false,
false,
chrono_tz::Asia::Shanghai,
(3600, 600, 7200),
)