Compare commits
No commits in common. "dev_1.3_xuxt" and "main" have entirely different histories.
dev_1.3_xu
...
main
@ -1,9 +0,0 @@
|
|||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.idea
|
|
||||||
target
|
|
||||||
rtr-db
|
|
||||||
tests
|
|
||||||
specs
|
|
||||||
scripts
|
|
||||||
README.md
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,5 +1,2 @@
|
|||||||
target/
|
target/
|
||||||
rtr-db/
|
Cargo.lock
|
||||||
.idea/
|
|
||||||
logs/
|
|
||||||
report/
|
|
||||||
|
|||||||
3528
Cargo.lock
generated
3528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@ -1,12 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rpki"
|
name = "rpki"
|
||||||
version = "1.3.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "rpki_rtr"
|
|
||||||
path = "src/main_rtr.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
der-parser = "10.0.0"
|
der-parser = "10.0.0"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
@ -17,24 +13,3 @@ url = "2.5.8"
|
|||||||
asn1-rs = "0.7.1"
|
asn1-rs = "0.7.1"
|
||||||
asn1-rs-derive = "0.6.0"
|
asn1-rs-derive = "0.6.0"
|
||||||
asn1 = "0.23.0"
|
asn1 = "0.23.0"
|
||||||
arc-swap = "1.7.0"
|
|
||||||
chrono = { version = "0.4.44", features = ["serde"] }
|
|
||||||
chrono-tz = "0.10"
|
|
||||||
bytes = "1.11.1"
|
|
||||||
tokio = { version = "1.49.0", features = ["full"] }
|
|
||||||
rand = "0.10.0"
|
|
||||||
rocksdb = { version = "0.21.0", default-features = false }
|
|
||||||
serde = { version = "1", features = ["derive", "rc"] }
|
|
||||||
serde_json = "1"
|
|
||||||
base64 = "0.22"
|
|
||||||
anyhow = "1"
|
|
||||||
tracing = "0.1.44"
|
|
||||||
sha2 = "0.10"
|
|
||||||
tempfile = "3"
|
|
||||||
tokio-rustls = "0.26"
|
|
||||||
rustls = "0.23"
|
|
||||||
rustls-pemfile = "2"
|
|
||||||
rustls-pki-types = "1.14.0"
|
|
||||||
socket2 = "0.5"
|
|
||||||
russh = { version = "0.60.0", default-features = false, features = ["ring", "rsa"] }
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|
||||||
|
|||||||
287
README.client.md
287
README.client.md
@ -1,287 +0,0 @@
|
|||||||
# RTR Client 使用说明
|
|
||||||
|
|
||||||
本文说明仓库内三类 RTR Client 的用途、启动方式和验证命令:
|
|
||||||
|
|
||||||
- `deploy/client`: 本仓库自带的 `rtr_debug_client`,适合做 RTR 协议调试、连通性检查和单次查询。
|
|
||||||
- `deploy/frr`: FRR 作为黑盒 RTR Client,适合验证真实路由软件是否能消费 server 下发的 VRP。
|
|
||||||
- `deploy/bird`: BIRD 作为黑盒 RTR Client,适合验证 TCP 或 SSH transport 下的 RPKI/ROA/ASPA 数据导入。
|
|
||||||
|
|
||||||
以下命令默认从仓库根目录执行。
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [前置条件](#前置条件)
|
|
||||||
- [rtr_debug_client](#rtr_debug_client)
|
|
||||||
- [Docker attach 交互命令](#docker-attach-交互命令)
|
|
||||||
- [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 --summary-only
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker 镜像的 `entrypoint.sh` 会把 stdout/stderr 重定向到 `logs/client/*.stdout.log` 和 `logs/client/*.stderr.log`。因此 `docker attach` 后可以输入交互命令,但命令响应和收到的 PDU 仍然主要从日志文件查看:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f logs/client/*.stdout.log
|
|
||||||
tail -f logs/client/*.stderr.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker attach 交互命令
|
|
||||||
|
|
||||||
如果 debug client 已经通过 compose 启动,可以 attach 到容器后输入命令:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker attach --sig-proxy=false <rtr-debug-client-container>
|
|
||||||
```
|
|
||||||
|
|
||||||
脱离 attach 会话时使用 `Ctrl-P` 后接 `Ctrl-Q`,避免 `Ctrl-C` 直接停止客户端进程。
|
|
||||||
|
|
||||||
可用交互命令如下:
|
|
||||||
|
|
||||||
| 命令 | 作用 |
|
|
||||||
| --- | --- |
|
|
||||||
| `help` | 显示可用命令列表。 |
|
|
||||||
| `state` | 打印当前客户端状态,包括 `session_id`、`serial`、`refresh/retry/expire`、输出模式、自动轮询状态等。 |
|
|
||||||
| `version` | 查看当前 RTR 协议版本。 |
|
|
||||||
| `version <n>` | 修改后续查询使用的 RTR 协议版本,例如 `version 2`。 |
|
|
||||||
| `reset` | 发送 Reset Query,通常用于让 server 重新下发完整数据集。 |
|
|
||||||
| `serial` | 使用当前已记录的 `session_id` 和 `serial` 发送 Serial Query。需要客户端已经从 EndOfData 中拿到状态。 |
|
|
||||||
| `serial <session_id> <serial>` | 显式指定参数发送 Serial Query,例如 `serial 31320 156`。 |
|
|
||||||
| `timeout` | 查看当前读取 PDU 的超时时间。 |
|
|
||||||
| `timeout <secs>` | 修改读取 PDU 的超时时间。超时只打印提示,连接保持打开。 |
|
|
||||||
| `poll` | 查看当前自动轮询间隔、来源、refresh hint、错误状态和暂停状态。 |
|
|
||||||
| `poll <secs>` | 修改自动轮询间隔。 |
|
|
||||||
| `poll pause` | 暂停自动轮询。 |
|
|
||||||
| `poll resume` | 恢复自动轮询。 |
|
|
||||||
| `output` | 查看当前输出模式,以及 summary 模式下跳过的 payload PDU 数量。 |
|
|
||||||
| `output verbose` | 打印所有收到的 PDU,包括 IPv4 Prefix、IPv6 Prefix、Router Key、ASPA 等 payload PDU。 |
|
|
||||||
| `output summary` | 只打印摘要,跳过 payload PDU 详情。compose 默认使用该模式。 |
|
|
||||||
| `quit` / `exit` | 退出客户端进程。 |
|
|
||||||
|
|
||||||
常用调试流程:
|
|
||||||
|
|
||||||
```text
|
|
||||||
output verbose
|
|
||||||
reset
|
|
||||||
state
|
|
||||||
serial
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 能力一致。
|
|
||||||
372
README.md
372
README.md
@ -1,373 +1,11 @@
|
|||||||
# 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), [draft-ietf-sidrops-8210bis-26](https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-26.html)
|
|
||||||
- SLURM: [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)(已过期,Expired 2026-05-20)
|
|
||||||
- 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
|
||||||
```
|
```
|
||||||
|
|
||||||
服务端 binary 名称是 `rpki_rtr`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo run --bin rpki_rtr
|
|
||||||
```
|
|
||||||
|
|
||||||
默认配置会监听 `0.0.0.0:323`,从 `./data` 查找 CCR 文件,并使用 `./rtr-db` 保存 RocksDB 数据。启动前需要确保 CCR 目录里存在可解析的 `.ccr` 文件。
|
|
||||||
|
|
||||||
## 数据输入
|
|
||||||
|
|
||||||
服务端启动和刷新时会读取 `RPKI_RTR_CCR_DIR` 指向的目录:
|
|
||||||
|
|
||||||
1. 如果该目录下存在包含 `.ccr` 文件的子目录,先按子目录名排序选择最新子目录。
|
|
||||||
2. 在选中的目录中按文件名排序选择最新 `.ccr` 文件。
|
|
||||||
3. 从 CCR 中解析 VRP 和 VAP/ASPA。
|
|
||||||
4. 如果配置了 `RPKI_RTR_SLURM_DIR`,加载其中启用的 `*.slurm` 文件并应用本地策略。
|
|
||||||
|
|
||||||
## 快速启动
|
|
||||||
|
|
||||||
### 前置数据生成
|
|
||||||
|
|
||||||
TLS/mTLS 或 SSH transport 需要本地证书和密钥。开发和 Docker 联调可用脚本生成 compose 默认引用的最小文件集:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/10_generate-certs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
如需覆盖已有 `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,23 +0,0 @@
|
|||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [
|
|
||||||
{
|
|
||||||
"prefix": "24.0.0.0/8",
|
|
||||||
"comment": "Filter many VRPs in current CCR sample"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": [
|
|
||||||
{
|
|
||||||
"customerAsn": 80,
|
|
||||||
"comment": "Filter one ASPA known to exist in current CCR sample"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
# Build-time image knob.
|
|
||||||
RPKI_BIRD_VERSION=3.2.1
|
|
||||||
|
|
||||||
# TCP mode target endpoint.
|
|
||||||
RPKI_BIRD_RPKI_HOST=rpki-rtr
|
|
||||||
RPKI_BIRD_RPKI_PORT=323
|
|
||||||
|
|
||||||
# SSH mode target endpoint.
|
|
||||||
RPKI_BIRD_SSH_RPKI_HOST=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_PORT=22
|
|
||||||
|
|
||||||
# Config template paths in container.
|
|
||||||
RPKI_BIRD_CONFIG_TEMPLATE_PATH=/config/bird.conf.template
|
|
||||||
RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH=/config/bird.conf.ssh.template
|
|
||||||
|
|
||||||
# Observation and output knobs.
|
|
||||||
RPKI_BIRD_OBSERVE_PROTO=rpki_tcp
|
|
||||||
RPKI_BIRD_OBSERVE_MODE=interval
|
|
||||||
RPKI_BIRD_OBSERVE_DEBOUNCE_SECS=1
|
|
||||||
RPKI_BIRD_OBSERVE_INTERVAL=30
|
|
||||||
RPKI_BIRD_OBSERVE_ASPA_TABLE=rtr_aspa
|
|
||||||
RPKI_BIRD_OBSERVE_ROA4_TABLE=rtr_roa_v4
|
|
||||||
RPKI_BIRD_OBSERVE_ROA6_TABLE=rtr_roa_v6
|
|
||||||
RPKI_BIRD_OBSERVE_ASPA_COUNT=3
|
|
||||||
RPKI_BIRD_OBSERVE_ROA4_COUNT=3
|
|
||||||
RPKI_BIRD_OBSERVE_ROA6_COUNT=3
|
|
||||||
RPKI_BIRD_SHOW_ASPA=1
|
|
||||||
RPKI_BIRD_SHOW_ROA4=1
|
|
||||||
RPKI_BIRD_SHOW_ROA6=1
|
|
||||||
|
|
||||||
# Host volume mounts.
|
|
||||||
RPKI_BIRD_LOG_HOST_DIR=../../logs/bird
|
|
||||||
RPKI_BIRD_SSH_CERTS_HOST_DIR=../../certs/ssh
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
FROM debian:bookworm-slim AS builder
|
|
||||||
|
|
||||||
ARG BIRD_VERSION=3.2.1
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
build-essential \
|
|
||||||
bison \
|
|
||||||
flex \
|
|
||||||
m4 \
|
|
||||||
perl \
|
|
||||||
ca-certificates \
|
|
||||||
wget \
|
|
||||||
xz-utils \
|
|
||||||
libreadline-dev \
|
|
||||||
libncurses-dev \
|
|
||||||
libssh-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN wget -O bird.tar.gz "https://bird.nic.cz/download/bird-${BIRD_VERSION}.tar.gz" \
|
|
||||||
&& tar -xzf bird.tar.gz \
|
|
||||||
&& mv "bird-${BIRD_VERSION}" bird
|
|
||||||
|
|
||||||
WORKDIR /build/bird
|
|
||||||
|
|
||||||
RUN ./configure \
|
|
||||||
--prefix=/usr \
|
|
||||||
--sysconfdir=/etc/bird \
|
|
||||||
--localstatedir=/run \
|
|
||||||
&& make -j"$(nproc)" \
|
|
||||||
&& make install
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates \
|
|
||||||
gettext-base \
|
|
||||||
tzdata \
|
|
||||||
netcat-openbsd \
|
|
||||||
libreadline8 \
|
|
||||||
libncurses6 \
|
|
||||||
libtinfo6 \
|
|
||||||
libssh-4 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY --from=builder /usr/sbin/bird /usr/sbin/bird
|
|
||||||
COPY --from=builder /usr/sbin/birdc /usr/sbin/birdc
|
|
||||||
COPY --from=builder /etc/bird /etc/bird
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh \
|
|
||||||
&& mkdir -p /run/bird
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
@ -1,90 +0,0 @@
|
|||||||
# BIRD Minimal RTR Client Config
|
|
||||||
|
|
||||||
This folder provides a minimal BIRD setup for black-box interop testing
|
|
||||||
against this repository's RTR server defaults.
|
|
||||||
|
|
||||||
Server defaults in this repo:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- SSH: `0.0.0.0:22` (when enabled on server)
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `Dockerfile`: builds a minimal BIRD2 runtime image.
|
|
||||||
- `bird.conf.example`: sample `/etc/bird/bird.conf`.
|
|
||||||
- `bird.conf.ssh.example`: sample SSH transport `/etc/bird/bird.conf`.
|
|
||||||
- `entrypoint.sh`: starts BIRD in foreground mode.
|
|
||||||
- `docker-compose.yml`: one-click local TCP test client.
|
|
||||||
- `docker-compose.ssh.yml`: compose override for SSH transport.
|
|
||||||
|
|
||||||
By default, the container uses event-driven observation and prints snapshots
|
|
||||||
only when BIRD reports RPKI-related changes.
|
|
||||||
|
|
||||||
## Docker quick start
|
|
||||||
|
|
||||||
From repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Use another terminal to inspect:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
If protocol state is `up`, the RTR client path is working.
|
|
||||||
|
|
||||||
Detached mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH quick start
|
|
||||||
|
|
||||||
Start server in SSH mode first:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
Start BIRD client with SSH override:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose \
|
|
||||||
-f deploy/bird/docker-compose.yml \
|
|
||||||
-f deploy/bird/docker-compose.ssh.yml \
|
|
||||||
up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
In detached mode, observe with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- This setup renders `bird.conf.template` at startup and uses `RPKI_HOST` / `RPKI_PORT`
|
|
||||||
for the RTR endpoint.
|
|
||||||
- `network_mode: host` expects your RTR server to be reachable at
|
|
||||||
`host.docker.internal:323` from the container.
|
|
||||||
- Observation is controlled by env vars:
|
|
||||||
`OBSERVE_MODE` (`event` by default, `interval` as fallback),
|
|
||||||
`OBSERVE_DEBOUNCE_SECS` (default `1`),
|
|
||||||
`OBSERVE_INTERVAL` (seconds, used when `OBSERVE_MODE=interval`),
|
|
||||||
and `OBSERVE_PROTO`.
|
|
||||||
- SSH mode mounts `../../certs/ssh` into `/config/ssh` and expects:
|
|
||||||
`rtr-client.key` and `ssh_host_rsa_key.pub`.
|
|
||||||
- Entrypoint auto-generates `/run/bird/known_hosts` from
|
|
||||||
`/config/ssh/ssh_host_rsa_key.pub` for BIRD SSH host-key verification.
|
|
||||||
- If your environment does not support Docker host networking, switch to a
|
|
||||||
bridge network and replace `remote` addresses accordingly.
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
# BIRD 最小化 RTR 客户端配置
|
|
||||||
|
|
||||||
本目录提供最小化 BIRD 配置,用于与本仓库 RTR Server 做黑盒互通测试。
|
|
||||||
|
|
||||||
本仓库默认 RTR 监听地址:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- SSH: `0.0.0.0:22`(仅在 server SSH 模式启用时)
|
|
||||||
|
|
||||||
## 文件说明
|
|
||||||
|
|
||||||
- `Dockerfile`: 构建最小 BIRD 运行镜像(包含 SSH transport 支持)。
|
|
||||||
- `bird.conf.example`: `/etc/bird/bird.conf` 的 TCP 示例。
|
|
||||||
- `bird.conf.ssh.example`: `/etc/bird/bird.conf` 的 SSH transport 示例。
|
|
||||||
- `entrypoint.sh`: 前台启动 BIRD,并周期输出协议快照。
|
|
||||||
- `docker-compose.yml`: TCP 一键启动。
|
|
||||||
- `docker-compose.ssh.yml`: SSH 覆盖配置。
|
|
||||||
|
|
||||||
容器默认每 30 秒向日志输出一次 RPKI 协议状态快照。
|
|
||||||
|
|
||||||
## Docker 快速启动(TCP)
|
|
||||||
|
|
||||||
在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
另开终端查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
如果协议状态显示 `up`,说明 RTR 客户端链路正常。
|
|
||||||
|
|
||||||
后台模式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml up -d --build
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/bird/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH 快速启动
|
|
||||||
|
|
||||||
先启动 server 的 SSH 模式:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.ssh.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
再启动 BIRD SSH 客户端:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose \
|
|
||||||
-f deploy/bird/docker-compose.yml \
|
|
||||||
-f deploy/bird/docker-compose.ssh.yml \
|
|
||||||
up --build
|
|
||||||
```
|
|
||||||
|
|
||||||
查看日志:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker logs -f bird-rpki-client
|
|
||||||
```
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 当前 compose 使用 `network_mode: host`,默认通过 `host.docker.internal` 访问 server。
|
|
||||||
- 观测频率由环境变量控制:`OBSERVE_INTERVAL`(秒,默认 `30`)和 `OBSERVE_PROTO`。
|
|
||||||
- SSH 模式会将 `../../certs/ssh` 挂载到容器 `/config/ssh`,并使用:
|
|
||||||
`rtr-client.key`、`ssh_host_rsa_key.pub`。
|
|
||||||
- 入口脚本会基于 `/config/ssh/ssh_host_rsa_key.pub` 自动生成
|
|
||||||
`/run/bird/known_hosts`,用于 BIRD 的 SSH 主机密钥校验。
|
|
||||||
- 如果你运行在 Docker Desktop(非 Linux 原生 host network 场景),建议改为自定义 bridge 网络并将 `remote` 地址改成可达的 server 容器名或宿主机地址。
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
debug protocols all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 22;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 323;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 323;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "host.docker.internal" port 22;
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_ssh {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport ssh {
|
|
||||||
user "rpki-rtr";
|
|
||||||
bird private key "/config/ssh/rtr-client.key";
|
|
||||||
remote public key "/run/bird/known_hosts";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
log stderr all;
|
|
||||||
router id 192.0.2.2;
|
|
||||||
|
|
||||||
roa4 table rtr_roa_v4;
|
|
||||||
roa6 table rtr_roa_v6;
|
|
||||||
aspa table rtr_aspa;
|
|
||||||
|
|
||||||
protocol device {
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol rpki rpki_tcp {
|
|
||||||
roa4 { table rtr_roa_v4; };
|
|
||||||
roa6 { table rtr_roa_v6; };
|
|
||||||
aspa { table rtr_aspa; };
|
|
||||||
|
|
||||||
remote "${RPKI_HOST}" port ${RPKI_PORT};
|
|
||||||
|
|
||||||
min version 2;
|
|
||||||
max version 2;
|
|
||||||
|
|
||||||
refresh 3600;
|
|
||||||
retry 600;
|
|
||||||
expire 7200;
|
|
||||||
|
|
||||||
transport tcp {
|
|
||||||
authentication none;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
services:
|
|
||||||
bird-rpki-client:
|
|
||||||
environment:
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_SSH_CONFIG_TEMPLATE_PATH:-/config/bird.conf.ssh.template}"
|
|
||||||
RPKI_HOST: "${RPKI_BIRD_SSH_RPKI_HOST:-rpki-rtr}"
|
|
||||||
RPKI_PORT: "${RPKI_RTR_SSH_PORT:-22}"
|
|
||||||
OBSERVE_PROTO: "rpki_ssh"
|
|
||||||
volumes:
|
|
||||||
- ./bird.conf.ssh.template:/config/bird.conf.ssh.template:ro
|
|
||||||
- ${RPKI_BIRD_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/config/ssh:ro
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
services:
|
|
||||||
bird-rpki-client:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
BIRD_VERSION: "${RPKI_BIRD_VERSION:-3.2.1}"
|
|
||||||
container_name: bird-rpki-client
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH: "${RPKI_BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
|
||||||
|
|
||||||
RPKI_HOST: "${RPKI_BIRD_RPKI_HOST:-rpki-rtr}"
|
|
||||||
RPKI_PORT: "${RPKI_BIRD_RPKI_PORT:-323}"
|
|
||||||
|
|
||||||
OBSERVE_PROTO: "${RPKI_BIRD_OBSERVE_PROTO:-rpki_tcp}"
|
|
||||||
OBSERVE_MODE: "${RPKI_BIRD_OBSERVE_MODE:-interval}"
|
|
||||||
OBSERVE_DEBOUNCE_SECS: "${RPKI_BIRD_OBSERVE_DEBOUNCE_SECS:-1}"
|
|
||||||
OBSERVE_INTERVAL: "${RPKI_BIRD_OBSERVE_INTERVAL:-30}"
|
|
||||||
|
|
||||||
OBSERVE_ASPA_TABLE: "${RPKI_BIRD_OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
|
||||||
OBSERVE_ROA4_TABLE: "${RPKI_BIRD_OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
|
||||||
OBSERVE_ROA6_TABLE: "${RPKI_BIRD_OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
|
||||||
|
|
||||||
OBSERVE_ASPA_COUNT: "${RPKI_BIRD_OBSERVE_ASPA_COUNT:-3}"
|
|
||||||
OBSERVE_ROA4_COUNT: "${RPKI_BIRD_OBSERVE_ROA4_COUNT:-3}"
|
|
||||||
OBSERVE_ROA6_COUNT: "${RPKI_BIRD_OBSERVE_ROA6_COUNT:-3}"
|
|
||||||
|
|
||||||
SHOW_ASPA: "${RPKI_BIRD_SHOW_ASPA:-1}"
|
|
||||||
SHOW_ROA4: "${RPKI_BIRD_SHOW_ROA4:-1}"
|
|
||||||
SHOW_ROA6: "${RPKI_BIRD_SHOW_ROA6:-1}"
|
|
||||||
volumes:
|
|
||||||
- ./bird.conf.template:/config/bird.conf.template:ro
|
|
||||||
- ${RPKI_BIRD_LOG_HOST_DIR:-../../logs/bird}:/app/logs
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /run/bird
|
|
||||||
|
|
||||||
SOCK_PATH="/run/bird/bird.ctl"
|
|
||||||
PROTO="${OBSERVE_PROTO:-rpki_tcp}"
|
|
||||||
INTERVAL="${OBSERVE_INTERVAL:-30}"
|
|
||||||
MODE="${OBSERVE_MODE:-event}"
|
|
||||||
DEBOUNCE_SECS="${OBSERVE_DEBOUNCE_SECS:-1}"
|
|
||||||
RPKI_HOST="${RPKI_HOST:-host.docker.internal}"
|
|
||||||
RPKI_PORT="${RPKI_PORT:-323}"
|
|
||||||
|
|
||||||
BIRD_CONFIG_PATH="${BIRD_CONFIG_PATH:-/config/bird.conf}"
|
|
||||||
BIRD_CONFIG_TEMPLATE_PATH="${BIRD_CONFIG_TEMPLATE_PATH:-/config/bird.conf.template}"
|
|
||||||
|
|
||||||
ASPA_TABLE="${OBSERVE_ASPA_TABLE:-rtr_aspa}"
|
|
||||||
ROA4_TABLE="${OBSERVE_ROA4_TABLE:-rtr_roa_v4}"
|
|
||||||
ROA6_TABLE="${OBSERVE_ROA6_TABLE:-rtr_roa_v6}"
|
|
||||||
|
|
||||||
ASPA_COUNT="${OBSERVE_ASPA_COUNT:-3}"
|
|
||||||
ROA4_COUNT="${OBSERVE_ROA4_COUNT:-3}"
|
|
||||||
ROA6_COUNT="${OBSERVE_ROA6_COUNT:-3}"
|
|
||||||
|
|
||||||
SHOW_ASPA="${SHOW_ASPA:-1}"
|
|
||||||
SHOW_ROA4="${SHOW_ROA4:-1}"
|
|
||||||
SHOW_ROA6="${SHOW_ROA6:-1}"
|
|
||||||
SSH_HOST_PUBKEY_PATH="${SSH_HOST_PUBKEY_PATH:-/config/ssh/ssh_host_rsa_key.pub}"
|
|
||||||
SSH_KNOWN_HOSTS_PATH="${SSH_KNOWN_HOSTS_PATH:-/run/bird/known_hosts}"
|
|
||||||
|
|
||||||
LOG_DIR="${LOG_DIR:-/app/logs}"
|
|
||||||
LOG_NAME="${LOG_NAME:-${HOSTNAME:-bird-rpki-client}}"
|
|
||||||
STDOUT_LOG="${LOG_DIR}/${LOG_NAME}.stdout.log"
|
|
||||||
STDERR_LOG="${LOG_DIR}/${LOG_NAME}.stderr.log"
|
|
||||||
|
|
||||||
mkdir -p "$LOG_DIR"
|
|
||||||
exec >>"$STDOUT_LOG" 2>>"$STDERR_LOG"
|
|
||||||
|
|
||||||
if [ -r "$BIRD_CONFIG_TEMPLATE_PATH" ]; then
|
|
||||||
RENDERED_CONFIG_PATH="/run/bird/bird.generated.conf"
|
|
||||||
envsubst '${RPKI_HOST} ${RPKI_PORT}' < "$BIRD_CONFIG_TEMPLATE_PATH" > "$RENDERED_CONFIG_PATH"
|
|
||||||
BIRD_CONFIG_PATH="$RENDERED_CONFIG_PATH"
|
|
||||||
echo "[entrypoint] rendered config : $BIRD_CONFIG_TEMPLATE_PATH -> $BIRD_CONFIG_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ensure_ssh_known_hosts() {
|
|
||||||
if [ -s "$SSH_KNOWN_HOSTS_PATH" ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -r "$SSH_HOST_PUBKEY_PATH" ]; then
|
|
||||||
echo "[entrypoint] WARNING: SSH host key file not found: $SSH_HOST_PUBKEY_PATH"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
set -- $(awk 'NF >= 2 { print $1, $2; exit }' "$SSH_HOST_PUBKEY_PATH")
|
|
||||||
if [ $# -ne 2 ]; then
|
|
||||||
echo "[entrypoint] WARNING: invalid SSH host key format in $SSH_HOST_PUBKEY_PATH"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
key_type="$1"
|
|
||||||
key_data="$2"
|
|
||||||
|
|
||||||
if echo "$key_type" | grep -q '^ssh-'; then
|
|
||||||
{
|
|
||||||
echo "$RPKI_HOST $key_type $key_data"
|
|
||||||
echo "[$RPKI_HOST]:$RPKI_PORT $key_type $key_data"
|
|
||||||
} > "$SSH_KNOWN_HOSTS_PATH"
|
|
||||||
else
|
|
||||||
cp "$SSH_HOST_PUBKEY_PATH" "$SSH_KNOWN_HOSTS_PATH"
|
|
||||||
fi
|
|
||||||
|
|
||||||
chmod 600 "$SSH_KNOWN_HOSTS_PATH" || true
|
|
||||||
echo "[entrypoint] generated known_hosts: $SSH_KNOWN_HOSTS_PATH"
|
|
||||||
}
|
|
||||||
|
|
||||||
print_first_n_objects() {
|
|
||||||
table_name="$1"
|
|
||||||
max_objects="$2"
|
|
||||||
|
|
||||||
birdc -s "$SOCK_PATH" show route table "$table_name" all 2>/dev/null | awk -v max="$max_objects" '
|
|
||||||
BEGIN {
|
|
||||||
count = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# 直接跳过空行
|
|
||||||
/^[[:space:]]*$/ {
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 保留 birdc 的表头
|
|
||||||
/^BIRD / {
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
/^Table / {
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 非缩进且不是表头,视为一个新对象的开始
|
|
||||||
/^[^[:space:]]/ {
|
|
||||||
count++
|
|
||||||
if (count > max) {
|
|
||||||
exit
|
|
||||||
}
|
|
||||||
print
|
|
||||||
next
|
|
||||||
}
|
|
||||||
|
|
||||||
# 缩进内容,只有在已进入前 max 个对象时才打印
|
|
||||||
{
|
|
||||||
if (count > 0 && count <= max) {
|
|
||||||
print
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' || true
|
|
||||||
}
|
|
||||||
|
|
||||||
print_snapshot() {
|
|
||||||
echo "==== $(date +"%Y-%m-%dT%H:%M:%S%:z") RPKI snapshot ($PROTO) ===="
|
|
||||||
birdc -s "$SOCK_PATH" show protocols all "$PROTO" || true
|
|
||||||
|
|
||||||
if [ "$SHOW_ASPA" = "1" ]; then
|
|
||||||
echo "---- ASPA table ($ASPA_TABLE, first ${ASPA_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ASPA_TABLE" "$ASPA_COUNT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHOW_ROA4" = "1" ]; then
|
|
||||||
echo "---- ROA4 table ($ROA4_TABLE, first ${ROA4_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ROA4_TABLE" "$ROA4_COUNT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$SHOW_ROA6" = "1" ]; then
|
|
||||||
echo "---- ROA6 table ($ROA6_TABLE, first ${ROA6_COUNT} objects) ----"
|
|
||||||
print_first_n_objects "$ROA6_TABLE" "$ROA6_COUNT"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
is_rpki_related_event() {
|
|
||||||
line="$1"
|
|
||||||
echo "$line" | grep -Eiq "$PROTO|$ASPA_TABLE|$ROA4_TABLE|$ROA6_TABLE|rpki|rtr"
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "[entrypoint] starting bird"
|
|
||||||
echo "[entrypoint] config : $BIRD_CONFIG_PATH"
|
|
||||||
echo "[entrypoint] observe proto : $PROTO"
|
|
||||||
echo "[entrypoint] observe mode : $MODE"
|
|
||||||
echo "[entrypoint] observe interval : $INTERVAL (used when mode=interval)"
|
|
||||||
echo "[entrypoint] debounce secs : $DEBOUNCE_SECS (used when mode=event)"
|
|
||||||
echo "[entrypoint] target : $RPKI_HOST:$RPKI_PORT"
|
|
||||||
echo "[entrypoint] show aspa : $SHOW_ASPA ($ASPA_TABLE, first $ASPA_COUNT objects)"
|
|
||||||
echo "[entrypoint] show roa4 : $SHOW_ROA4 ($ROA4_TABLE, first $ROA4_COUNT objects)"
|
|
||||||
echo "[entrypoint] show roa6 : $SHOW_ROA6 ($ROA6_TABLE, first $ROA6_COUNT objects)"
|
|
||||||
|
|
||||||
if [ "$PROTO" = "rpki_ssh" ] || grep -q 'transport[[:space:]]\+ssh' "$BIRD_CONFIG_PATH"; then
|
|
||||||
ensure_ssh_known_hosts
|
|
||||||
fi
|
|
||||||
|
|
||||||
if nc -zvw3 "$RPKI_HOST" "$RPKI_PORT"; then
|
|
||||||
echo "[entrypoint] TCP connectivity to $RPKI_HOST:$RPKI_PORT OK"
|
|
||||||
else
|
|
||||||
echo "[entrypoint] WARNING: cannot connect to $RPKI_HOST:$RPKI_PORT before BIRD starts"
|
|
||||||
fi
|
|
||||||
|
|
||||||
bird -f -c "$BIRD_CONFIG_PATH" -s "$SOCK_PATH" &
|
|
||||||
BIRD_PID="$!"
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
|
|
||||||
case "$INTERVAL" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
INTERVAL=0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
case "$MODE" in
|
|
||||||
event)
|
|
||||||
case "$DEBOUNCE_SECS" in
|
|
||||||
''|*[!0-9]*)
|
|
||||||
DEBOUNCE_SECS=1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "[entrypoint] waiting for BIRD monitor events and only printing on RPKI-related changes"
|
|
||||||
LAST_PRINT_TS=0
|
|
||||||
|
|
||||||
if birdc -s "$SOCK_PATH" monitor all 2>/dev/null | while IFS= read -r line; do
|
|
||||||
[ -n "$line" ] || continue
|
|
||||||
if ! is_rpki_related_event "$line"; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
now_ts="$(date +%s)"
|
|
||||||
if [ $((now_ts - LAST_PRINT_TS)) -lt "$DEBOUNCE_SECS" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
LAST_PRINT_TS="$now_ts"
|
|
||||||
echo "[monitor] $line"
|
|
||||||
print_snapshot
|
|
||||||
done; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
echo "[entrypoint] WARNING: birdc monitor failed, fallback to interval polling"
|
|
||||||
MODE="interval"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [ "$MODE" = "interval" ] && [ "$INTERVAL" -gt 0 ]; then
|
|
||||||
while kill -0 "$BIRD_PID" 2>/dev/null; do
|
|
||||||
print_snapshot
|
|
||||||
sleep "$INTERVAL"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
wait "$BIRD_PID"
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# Target RTR server addresses for client compose files
|
|
||||||
# build-run.sh auto-detects amd64/arm64 unless IMAGE_ARCH/RPKI_RTR_IMAGE_ARCH is set before running it.
|
|
||||||
# Direct docker compose uses RPKI_RTR_IMAGE_ARCH from this file; set it to arm64 on ARM hosts.
|
|
||||||
RPKI_RTR_CLIENT_IMAGE_BASE=rpki-rtr-debug-client
|
|
||||||
RPKI_RTR_CLIENT_IMAGE_TAG=v1.3
|
|
||||||
RPKI_RTR_IMAGE_ARCH=arm64
|
|
||||||
# RPKI_RTR_TARGET_PLATFORM=linux/amd64
|
|
||||||
# RPKI_RTR_CLIENT_IMAGE=rpki-rtr-debug-client-amd64:v1.3
|
|
||||||
|
|
||||||
# TCP example: 10.0.0.12:323
|
|
||||||
# TLS example: rpki.example.com:324
|
|
||||||
# SSH example: 10.0.0.12:22
|
|
||||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
|
||||||
RPKI_RTR_TLS_SERVER_ADDR=rpki-rtr:324
|
|
||||||
RPKI_RTR_SSH_SERVER_ADDR=rpki-rtr:22
|
|
||||||
|
|
||||||
# RTR protocol version used as client command second argument (supported: 0,1,2)
|
|
||||||
RPKI_RTR_PROTOCOL_VERSION=2
|
|
||||||
|
|
||||||
# TLS server name used by --server-name in TLS mode
|
|
||||||
# Must match server certificate SAN dNSName.
|
|
||||||
RPKI_RTR_TLS_SERVER_NAME=localhost
|
|
||||||
RPKI_RTR_TLS_CA_CERT_PATH=/app/certs/client-ca.crt
|
|
||||||
RPKI_RTR_TLS_CLIENT_CERT_PATH=/app/certs/client-good.crt
|
|
||||||
RPKI_RTR_TLS_CLIENT_KEY_PATH=/app/certs/client-good.key
|
|
||||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
|
||||||
|
|
||||||
# Shared client logs mount on host.
|
|
||||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=../../logs/client
|
|
||||||
|
|
||||||
# SSH mode examples:
|
|
||||||
# RPKI_RTR_SSH_SERVER_ADDR=10.0.0.12:2222
|
|
||||||
RPKI_RTR_CLIENT_KEYS_VOLUME=../../certs/ssh:/app/certs:ro
|
|
||||||
RPKI_RTR_CLIENT_KEY_PATH=/app/certs/rtr-client.key
|
|
||||||
RPKI_RTR_SSH_SERVER_PUBKEY_PATH=/app/certs/ssh_host_rsa_key.pub
|
|
||||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_PASSWORD=
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
FROM rust:1.89-bookworm AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends clang libclang-dev pkg-config \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN cargo build --release --bin rtr_debug_client
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates tzdata \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ENV TZ=Asia/Shanghai
|
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
|
||||||
COPY --chmod=755 deploy/client/entrypoint.sh /usr/local/bin/rtr-debug-client-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/rtr-debug-client-entrypoint.sh"]
|
|
||||||
@ -1,314 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
||||||
CALLER_TARGET_PLATFORM="${TARGET_PLATFORM:-${RPKI_RTR_TARGET_PLATFORM:-}}"
|
|
||||||
CALLER_IMAGE_ARCH="${IMAGE_ARCH:-${RPKI_RTR_IMAGE_ARCH:-}}"
|
|
||||||
CALLER_CLIENT_IMAGE="${CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-}}"
|
|
||||||
|
|
||||||
load_env_defaults() {
|
|
||||||
local line key value
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] || return
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
||||||
line="${line%$'\r'}"
|
|
||||||
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ "${line}" == *"="* ]] || continue
|
|
||||||
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
key="${key#"${key%%[![:space:]]*}"}"
|
|
||||||
key="${key%"${key##*[![:space:]]}"}"
|
|
||||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
||||||
|
|
||||||
if [[ -z "${!key+x}" ]]; then
|
|
||||||
export "${key}=${value}"
|
|
||||||
fi
|
|
||||||
done < "${SCRIPT_DIR}/.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_image_arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
aarch64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) die "unsupported host architecture '$(uname -m)'; set RPKI_RTR_IMAGE_ARCH=amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_for_image_arch() {
|
|
||||||
case "$1" in
|
|
||||||
amd64) printf '%s\n' linux/amd64 ;;
|
|
||||||
arm64) printf '%s\n' linux/arm64 ;;
|
|
||||||
*) die "unsupported image arch '$1', expected amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
image_arch_from_platform() {
|
|
||||||
case "$1" in
|
|
||||||
linux/amd64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
linux/arm64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
load_env_defaults
|
|
||||||
|
|
||||||
MODE="${MODE:-tcp}"
|
|
||||||
ACTION="up"
|
|
||||||
DETACH="${DETACH:-1}"
|
|
||||||
BUILD="${BUILD:-1}"
|
|
||||||
CLIENTS="${CLIENTS:-single}"
|
|
||||||
TARGET_PLATFORM="${CALLER_TARGET_PLATFORM}"
|
|
||||||
IMAGE_ARCH="${CALLER_IMAGE_ARCH}"
|
|
||||||
CLIENT_IMAGE="${CALLER_CLIENT_IMAGE}"
|
|
||||||
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: host architecture)
|
|
||||||
--image-arch VALUE
|
|
||||||
Image architecture suffix for default tag: amd64|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
|
|
||||||
;;
|
|
||||||
--image-arch)
|
|
||||||
[[ $# -ge 2 ]] || die "--image-arch requires a value"
|
|
||||||
IMAGE_ARCH="$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
|
|
||||||
|
|
||||||
if [[ -z "${TARGET_PLATFORM}" ]]; then
|
|
||||||
[[ -n "${IMAGE_ARCH}" ]] || IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
TARGET_PLATFORM="$(platform_for_image_arch "${IMAGE_ARCH}")"
|
|
||||||
elif [[ -z "${IMAGE_ARCH}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" != *,* ]] && IMAGE_ARCH="$(image_arch_from_platform "${TARGET_PLATFORM}")"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${CLIENT_IMAGE}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* ]]; then
|
|
||||||
CLIENT_IMAGE="${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}"
|
|
||||||
else
|
|
||||||
CLIENT_IMAGE="${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${IMAGE_ARCH}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "${CLIENTS}" == "single" || "${CLIENTS}" == "multi" ]] || die "unsupported clients '${CLIENTS}', expected single|multi"
|
|
||||||
export RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}"
|
|
||||||
if [[ "${ACTION}" == "buildx" ]]; then
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
else
|
|
||||||
has_compose || die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${REPO_ROOT}"
|
|
||||||
|
|
||||||
case "${ACTION}" in
|
|
||||||
up)
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
build_client
|
|
||||||
;;
|
|
||||||
buildx)
|
|
||||||
buildx_client
|
|
||||||
;;
|
|
||||||
rebuild)
|
|
||||||
build_client --no-cache
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
down|stop)
|
|
||||||
compose_run down
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
compose_run down
|
|
||||||
up_client
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
compose_run logs -f
|
|
||||||
;;
|
|
||||||
ps)
|
|
||||||
compose_run ps
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-client-1:
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-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_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-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_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-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_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-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_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--ssh",
|
|
||||||
"--ssh-user",
|
|
||||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
|
||||||
"--ssh-password",
|
|
||||||
"${RPKI_RTR_SSH_PASSWORD}",
|
|
||||||
"--ssh-server-key",
|
|
||||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_SSH_SERVER_ADDR:-rpki-rtr:22}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--ssh",
|
|
||||||
"--ssh-user",
|
|
||||||
"${RPKI_RTR_SSH_USERNAME:-rpki-rtr}",
|
|
||||||
"--ssh-key",
|
|
||||||
"${RPKI_RTR_CLIENT_KEY_PATH:-/app/certs/rtr-client.key}",
|
|
||||||
"--ssh-server-key",
|
|
||||||
"${RPKI_RTR_SSH_SERVER_PUBKEY_PATH:-/app/certs/ssh_host_rsa_key.pub}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_KEYS_VOLUME:-../../certs/ssh:/app/certs:ro}
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command:
|
|
||||||
[
|
|
||||||
"${RPKI_RTR_TLS_SERVER_ADDR:-rpki-rtr:324}",
|
|
||||||
"${RPKI_RTR_PROTOCOL_VERSION:-2}",
|
|
||||||
"reset",
|
|
||||||
"--tls",
|
|
||||||
"--ca-cert",
|
|
||||||
"${RPKI_RTR_TLS_CA_CERT_PATH:-/app/certs/client-ca.crt}",
|
|
||||||
"--server-name",
|
|
||||||
"${RPKI_RTR_TLS_SERVER_NAME:-localhost}",
|
|
||||||
"--client-cert",
|
|
||||||
"${RPKI_RTR_TLS_CLIENT_CERT_PATH:-/app/certs/client-good.crt}",
|
|
||||||
"--client-key",
|
|
||||||
"${RPKI_RTR_TLS_CLIENT_KEY_PATH:-/app/certs/client-good.key}",
|
|
||||||
"--keep-after-error",
|
|
||||||
"--summary-only"
|
|
||||||
]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
services:
|
|
||||||
rtr-debug-client:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/client/Dockerfile
|
|
||||||
image: ${RPKI_RTR_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE_BASE:-rpki-rtr-debug-client}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_CLIENT_IMAGE_TAG:-latest}}
|
|
||||||
command: ["${RPKI_RTR_TCP_SERVER_ADDR:-rpki-rtr:323}", "${RPKI_RTR_PROTOCOL_VERSION:-2}", "reset", "--keep-after-error", "--summary-only"]
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CLIENT_LOG_HOST_DIR:-../../logs/client}:/app/logs
|
|
||||||
restart: no
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /app/logs
|
|
||||||
|
|
||||||
log_name="${HOSTNAME:-rtr-debug-client}"
|
|
||||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
|
||||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
|
||||||
|
|
||||||
exec /usr/local/bin/rtr_debug_client "$@" >>"$stdout_log" 2>>"$stderr_log"
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
# RTR server endpoint used by deploy/frr/frr.conf.template.
|
|
||||||
RPKI_FRR_RPKI_HOST=127.0.0.1
|
|
||||||
RPKI_FRR_RPKI_PORT=323
|
|
||||||
RPKI_FRR_RPKI_PREFERENCE=1
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
# FRR Minimal RTR Client Config
|
|
||||||
|
|
||||||
中文文档: [README.zh.md](./README.zh.md)
|
|
||||||
|
|
||||||
This folder provides a minimal FRR setup for black-box interop testing
|
|
||||||
against this repository's RTR server defaults.
|
|
||||||
|
|
||||||
Server defaults in this repo:
|
|
||||||
- TCP: `0.0.0.0:323`
|
|
||||||
- TLS: `0.0.0.0:324`
|
|
||||||
|
|
||||||
Reference:
|
|
||||||
- `src/main.rs`
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `daemons.example`: sample `/etc/frr/daemons`
|
|
||||||
- `frr.conf.example`: static sample `/etc/frr/frr.conf`
|
|
||||||
- `frr.conf.template`: Docker template used to generate `/etc/frr/frr.conf`
|
|
||||||
- `entrypoint.sh`: renders `frr.conf.template` from environment variables before starting FRR
|
|
||||||
- `.env`: default Docker environment values
|
|
||||||
|
|
||||||
## How to apply on an FRR host
|
|
||||||
|
|
||||||
1. Copy `daemons.example` to `/etc/frr/daemons`.
|
|
||||||
2. Copy `frr.conf.example` to `/etc/frr/frr.conf`.
|
|
||||||
3. Restart FRR:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart frr
|
|
||||||
```
|
|
||||||
|
|
||||||
## Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vtysh -c "show rpki configuration"
|
|
||||||
vtysh -c "show rpki cache-server"
|
|
||||||
vtysh -c "show rpki cache-connection"
|
|
||||||
vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
If `show rpki cache-connection` is connected and `show rpki prefix-table`
|
|
||||||
contains VRPs, the RTR client path is working.
|
|
||||||
|
|
||||||
## Docker quick start
|
|
||||||
|
|
||||||
From repository root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
The Docker example defaults to `127.0.0.1:323`. Override the RTR server
|
|
||||||
endpoint with environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_FRR_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
Stop:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- This setup targets RTR over TCP (`rpki cache tcp`).
|
|
||||||
- Docker mode renders `frr.conf.template` into `/etc/frr/frr.conf` at startup.
|
|
||||||
- Keep protocol-level conformance checks in Rust tests and
|
|
||||||
`src/bin/rtr_debug_client`.
|
|
||||||
- `network_mode: host` is kept for the minimal FRR container example, but the
|
|
||||||
RTR server address is controlled by `RPKI_FRR_RPKI_HOST` and
|
|
||||||
`RPKI_FRR_RPKI_PORT`.
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
# FRR 最小化 RTR 客户端配置
|
|
||||||
|
|
||||||
本目录提供一个 FRR 最小配置,用于和本仓库 RTR Server 做黑盒互通测试。
|
|
||||||
|
|
||||||
本仓库默认 RTR 监听地址:
|
|
||||||
- TCP:`0.0.0.0:323`
|
|
||||||
- TLS:`0.0.0.0:324`
|
|
||||||
|
|
||||||
参考实现:
|
|
||||||
- `src/main.rs`
|
|
||||||
|
|
||||||
## 文件说明
|
|
||||||
|
|
||||||
- `daemons.example`:示例 `/etc/frr/daemons`
|
|
||||||
- `frr.conf.example`:静态示例 `/etc/frr/frr.conf`
|
|
||||||
- `frr.conf.template`:Docker 启动时生成 `/etc/frr/frr.conf` 的模板
|
|
||||||
- `entrypoint.sh`:启动 FRR 前根据环境变量渲染 `frr.conf.template`
|
|
||||||
- `.env`:Docker 示例使用的默认环境变量
|
|
||||||
|
|
||||||
## 在 FRR 主机上应用
|
|
||||||
|
|
||||||
1. 复制 `daemons.example` 到 `/etc/frr/daemons`
|
|
||||||
2. 复制 `frr.conf.example` 到 `/etc/frr/frr.conf`
|
|
||||||
3. 重启 FRR:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart frr
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
vtysh -c "show rpki configuration"
|
|
||||||
vtysh -c "show rpki cache-server"
|
|
||||||
vtysh -c "show rpki cache-connection"
|
|
||||||
vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
当 `show rpki cache-connection` 显示已连接,且 `show rpki prefix-table` 出现 VRP 时,说明 RTR 客户端链路工作正常。
|
|
||||||
|
|
||||||
## Docker 快速启动
|
|
||||||
|
|
||||||
在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki cache-connection"
|
|
||||||
docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|
||||||
```
|
|
||||||
|
|
||||||
Docker 示例默认连接 `127.0.0.1:323`。如果 RTR Server 在其他地址,可以通过环境变量覆盖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
RPKI_FRR_RPKI_HOST=10.0.0.12 \
|
|
||||||
RPKI_FRR_RPKI_PORT=323 \
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
停止:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file deploy/frr/.env -f deploy/frr/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## 可测试范围
|
|
||||||
|
|
||||||
- FRR 作为 RTR Client 与本仓库 Server 的 TCP 建链能力
|
|
||||||
- FRR 侧基础会话状态可见性(`cache-server` / `cache-connection`)
|
|
||||||
- VRP 下发与导入是否成功(`prefix-table` 是否有条目)
|
|
||||||
- 基于服务端数据变化触发的前缀表更新(可通过替换 `data` 后观察)
|
|
||||||
- 黑盒互通回归:用于确认“路由器客户端视角”功能可用
|
|
||||||
|
|
||||||
## 不覆盖范围
|
|
||||||
|
|
||||||
- 不替代协议级单元测试/集成测试(PDU 细节、异常路径、边界条件)
|
|
||||||
- 不替代 `src/bin/rtr_debug_client` 的逐报文调试能力
|
|
||||||
- 默认示例以 TCP 为主,TLS/mTLS 需按你的证书与 FRR 配置单独扩展
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 当前示例主要针对 RTR over TCP(`rpki cache tcp`)
|
|
||||||
- Docker 模式会在启动时把 `frr.conf.template` 渲染为 `/etc/frr/frr.conf`
|
|
||||||
- `network_mode: host` 仍用于最小化 FRR 容器示例,但 RTR Server 地址由 `RPKI_FRR_RPKI_HOST` 和 `RPKI_FRR_RPKI_PORT` 控制
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
# Minimal FRR daemons config for RPKI testing
|
|
||||||
|
|
||||||
zebra=yes
|
|
||||||
bgpd=yes
|
|
||||||
|
|
||||||
# Enable bgpd RPKI module
|
|
||||||
bgpd_options=" -A 127.0.0.1 -M rpki"
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
frr-rpki-client:
|
|
||||||
image: quay.io/frrouting/frr:10.2.2
|
|
||||||
container_name: frr-rpki-client
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
privileged: true
|
|
||||||
environment:
|
|
||||||
RPKI_FRR_RPKI_HOST: "${RPKI_FRR_RPKI_HOST:-127.0.0.1}"
|
|
||||||
RPKI_FRR_RPKI_PORT: "${RPKI_FRR_RPKI_PORT:-323}"
|
|
||||||
RPKI_FRR_RPKI_PREFERENCE: "${RPKI_FRR_RPKI_PREFERENCE:-1}"
|
|
||||||
entrypoint: ["/bin/sh", "/config/entrypoint.sh"]
|
|
||||||
volumes:
|
|
||||||
- ./daemons.example:/etc/frr/daemons:ro
|
|
||||||
- ./frr.conf.template:/config/frr.conf.template:ro
|
|
||||||
- ./entrypoint.sh:/config/entrypoint.sh:ro
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
: "${RPKI_FRR_RPKI_HOST:=127.0.0.1}"
|
|
||||||
: "${RPKI_FRR_RPKI_PORT:=323}"
|
|
||||||
: "${RPKI_FRR_RPKI_PREFERENCE:=1}"
|
|
||||||
: "${RPKI_FRR_CONFIG_TEMPLATE_PATH:=/config/frr.conf.template}"
|
|
||||||
: "${RPKI_FRR_CONFIG_PATH:=/etc/frr/frr.conf}"
|
|
||||||
|
|
||||||
escape_sed() {
|
|
||||||
printf '%s' "$1" | sed 's/[&|\\]/\\&/g'
|
|
||||||
}
|
|
||||||
|
|
||||||
host="$(escape_sed "${RPKI_FRR_RPKI_HOST}")"
|
|
||||||
port="$(escape_sed "${RPKI_FRR_RPKI_PORT}")"
|
|
||||||
preference="$(escape_sed "${RPKI_FRR_RPKI_PREFERENCE}")"
|
|
||||||
|
|
||||||
sed \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_HOST}|${host}|g" \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_PORT}|${port}|g" \
|
|
||||||
-e "s|\${RPKI_FRR_RPKI_PREFERENCE}|${preference}|g" \
|
|
||||||
"${RPKI_FRR_CONFIG_TEMPLATE_PATH}" > "${RPKI_FRR_CONFIG_PATH}"
|
|
||||||
|
|
||||||
exec /usr/lib/frr/docker-start
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
frr version 10.2
|
|
||||||
frr defaults traditional
|
|
||||||
hostname rpki-lab
|
|
||||||
service integrated-vtysh-config
|
|
||||||
!
|
|
||||||
debug rpki
|
|
||||||
!
|
|
||||||
rpki
|
|
||||||
rpki polling_period 10
|
|
||||||
rpki timeout 10
|
|
||||||
rpki retry_interval 10
|
|
||||||
rpki expire_interval 7200
|
|
||||||
rpki cache tcp 127.0.0.1 323 preference 1
|
|
||||||
exit
|
|
||||||
!
|
|
||||||
router bgp 65001
|
|
||||||
bgp router-id 192.0.2.1
|
|
||||||
!
|
|
||||||
address-family ipv4 unicast
|
|
||||||
exit-address-family
|
|
||||||
!
|
|
||||||
line vty
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
frr version 10.2
|
|
||||||
frr defaults traditional
|
|
||||||
hostname rpki-lab
|
|
||||||
service integrated-vtysh-config
|
|
||||||
!
|
|
||||||
debug rpki
|
|
||||||
!
|
|
||||||
rpki
|
|
||||||
rpki polling_period 10
|
|
||||||
rpki timeout 10
|
|
||||||
rpki retry_interval 10
|
|
||||||
rpki expire_interval 7200
|
|
||||||
rpki cache tcp ${RPKI_FRR_RPKI_HOST} ${RPKI_FRR_RPKI_PORT} preference ${RPKI_FRR_RPKI_PREFERENCE}
|
|
||||||
exit
|
|
||||||
!
|
|
||||||
router bgp 65001
|
|
||||||
bgp router-id 192.0.2.1
|
|
||||||
!
|
|
||||||
address-family ipv4 unicast
|
|
||||||
exit-address-family
|
|
||||||
!
|
|
||||||
line vty
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
# Core runtime knobs.
|
|
||||||
# build-run.sh auto-detects amd64/arm64 unless IMAGE_ARCH/RPKI_RTR_IMAGE_ARCH is set before running it.
|
|
||||||
# Direct docker compose uses RPKI_RTR_IMAGE_ARCH from this file; set it to arm64 on ARM hosts.
|
|
||||||
RPKI_RTR_SERVER_IMAGE_BASE=rpki-rtr
|
|
||||||
RPKI_RTR_SERVER_IMAGE_TAG=v1.3
|
|
||||||
RPKI_RTR_IMAGE_ARCH=arm64
|
|
||||||
# RPKI_RTR_TARGET_PLATFORM=linux/amd64
|
|
||||||
# RPKI_RTR_SERVER_IMAGE=rpki-rtr-amd64:v1.3
|
|
||||||
|
|
||||||
# Data source directories on host.
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=../../data
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=../../data
|
|
||||||
|
|
||||||
# In-container data source directories.
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=../../report
|
|
||||||
|
|
||||||
# In-container runtime paths.
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report
|
|
||||||
|
|
||||||
# Persistent directories on host.
|
|
||||||
RPKI_RTR_DB_HOST_DIR=../../rtr-db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=../../logs/server
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=300
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10
|
|
||||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
|
||||||
RPKI_RTR_MAX_DELTA=10
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS=100000
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=128
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=qwert
|
|
||||||
RUST_LOG=info
|
|
||||||
|
|
||||||
# TLS mode knobs.
|
|
||||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH=false
|
|
||||||
RPKI_RTR_TLS_CERT_PATH=/app/certs/server-dns.crt
|
|
||||||
RPKI_RTR_TLS_KEY_PATH=/app/certs/server-dns.key
|
|
||||||
RPKI_RTR_TLS_CLIENT_CA_PATH=/app/certs/client-ca.crt
|
|
||||||
|
|
||||||
RPKI_RTR_TLS_CERTS_HOST_DIR=../../certs/tls
|
|
||||||
|
|
||||||
# SSH mode knobs.
|
|
||||||
RPKI_RTR_SSH_HOST_PORT=2222
|
|
||||||
RPKI_RTR_SSH_CONTAINER_PORT=22
|
|
||||||
RPKI_RTR_SSH_AUTH_MODE=key
|
|
||||||
RPKI_RTR_SSH_USERNAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_SUBSYSTEM_NAME=rpki-rtr
|
|
||||||
RPKI_RTR_SSH_HOST_KEY_PATH=/app/certs/ssh_host_rsa_key
|
|
||||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH=/app/certs/rtr-authorized_keys
|
|
||||||
|
|
||||||
RPKI_RTR_SSH_CERTS_HOST_DIR=../../certs/ssh
|
|
||||||
@ -1,99 +0,0 @@
|
|||||||
# Deployment (Docker Compose)
|
|
||||||
|
|
||||||
This project runs `src/main.rs` as a long-running server that:
|
|
||||||
|
|
||||||
1. loads latest `.ccr` from a configured directory,
|
|
||||||
2. applies optional SLURM filtering,
|
|
||||||
3. starts RTR server.
|
|
||||||
|
|
||||||
The container runs `rpki` directly as PID 1.
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `deploy/server/Dockerfile`
|
|
||||||
- `deploy/server/docker-compose.yml`
|
|
||||||
|
|
||||||
## Runtime Paths in Container
|
|
||||||
|
|
||||||
- CCR directory: `/app/data`
|
|
||||||
- RocksDB directory: `/app/rtr-db`
|
|
||||||
- SLURM directory: `/app/slurm`
|
|
||||||
- Report directory: `/app/report`
|
|
||||||
- TLS cert directory (optional): `/app/certs`
|
|
||||||
|
|
||||||
## Path Configuration via `.env`
|
|
||||||
|
|
||||||
- `RPKI_RTR_CCR_HOST_DIR`: host CCR directory mounted into container
|
|
||||||
- `RPKI_RTR_SLURM_HOST_DIR`: host SLURM directory mounted into container
|
|
||||||
- `RPKI_RTR_CCR_DIR`: in-container CCR directory path
|
|
||||||
- `RPKI_RTR_SLURM_DIR`: in-container SLURM directory path
|
|
||||||
- `RPKI_RTR_DB_HOST_DIR`: host RocksDB directory
|
|
||||||
- `RPKI_RTR_LOG_HOST_DIR`: host log directory
|
|
||||||
- `RPKI_RTR_REPORT_HOST_DIR`: host directory receiving split RTR JSON reports
|
|
||||||
- `RPKI_RTR_DB_PATH`: in-container RocksDB directory
|
|
||||||
- `RPKI_RTR_REPORT_DIR`: in-container report directory
|
|
||||||
|
|
||||||
## Runtime Configuration via `.env`
|
|
||||||
|
|
||||||
- Core: `RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS`, `RPKI_RTR_MAX_DELTA`, `RPKI_RTR_MAX_CONCURRENT_HANDSHAKES`, `RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, `RPKI_RTR_REPORT_HISTORY_LIMIT`, `RPKI_RTR_TIMEZONE`, `RPKI_RTR_ADMIN_ADDR`, `RPKI_RTR_ADMIN_TOKEN`, `RUST_LOG`
|
|
||||||
- TCP mode: `RPKI_RTR_MAX_CONNECTIONS`
|
|
||||||
- TLS mode: `RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH`, `RPKI_RTR_TLS_CERT_PATH`, `RPKI_RTR_TLS_KEY_PATH`, `RPKI_RTR_TLS_CLIENT_CA_PATH`, `RPKI_RTR_TLS_CERTS_HOST_DIR`
|
|
||||||
- SSH mode: `RPKI_RTR_SSH_HOST_PORT`, `RPKI_RTR_SSH_CONTAINER_PORT`, `RPKI_RTR_SSH_AUTH_MODE`, `RPKI_RTR_SSH_USERNAME`, `RPKI_RTR_SSH_SUBSYSTEM_NAME`, `RPKI_RTR_SSH_HOST_KEY_PATH`, `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH`, `RPKI_RTR_SSH_CERTS_HOST_DIR`
|
|
||||||
|
|
||||||
## Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stop
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
|
|
||||||
```
|
|
||||||
|
|
||||||
The admin API can also stream the redirected log file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200" \
|
|
||||||
-H "Authorization: Bearer $RPKI_RTR_ADMIN_TOKEN"
|
|
||||||
```
|
|
||||||
|
|
||||||
It reads `/app/logs/${HOSTNAME}.stdout.log` or `.stderr.log` by default. Set
|
|
||||||
`RPKI_RTR_LOG_DIR` and `RPKI_RTR_LOG_NAME` to override that lookup.
|
|
||||||
|
|
||||||
## Runtime Report
|
|
||||||
|
|
||||||
The server writes split JSON reports. Each report file uses a local-time
|
|
||||||
timestamp suffix and each category keeps `RPKI_RTR_REPORT_HISTORY_LIMIT` files,
|
|
||||||
defaulting to 10.
|
|
||||||
|
|
||||||
- `rtr-source-*.json`: CCR and SLURM source metadata, latest refresh status,
|
|
||||||
data quality counts, cache snapshot counts, and delta counts. Written on
|
|
||||||
startup and source refresh events.
|
|
||||||
- `rtr-clients-*.json`: active client connection counts and counts by transport
|
|
||||||
(`tcp`, `tls`, `ssh`). Written on startup, whenever the active connection
|
|
||||||
count changes.
|
|
||||||
- `rtr-runtime-*.json`: service start time, uptime, process RSS, and
|
|
||||||
non-sensitive runtime configuration. Written on startup and every
|
|
||||||
`RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS`, defaulting to 300 seconds.
|
|
||||||
|
|
||||||
Timestamps in logs and report JSON files use `RPKI_RTR_TIMEZONE`, which
|
|
||||||
defaults to `Asia/Shanghai`. Use IANA timezone names such as `Asia/Shanghai`,
|
|
||||||
`Europe/London`, `America/New_York`, or `UTC`; `Shanghai` is accepted as a
|
|
||||||
convenience alias for `Asia/Shanghai`.
|
|
||||||
|
|
||||||
## Runtime Admin Config
|
|
||||||
|
|
||||||
The admin endpoint is disabled by default. Set `RPKI_RTR_ADMIN_ADDR` to enable
|
|
||||||
`POST /admin/rtr/config`. If the address is not loopback, `RPKI_RTR_ADMIN_TOKEN`
|
|
||||||
must also be set and requests must include `Authorization: Bearer <token>`.
|
|
||||||
|
|
||||||
The endpoint accepts partial JSON updates. See `docs/rtr-admin-api.md` for the
|
|
||||||
complete request/response schema, examples, and runtime apply semantics.
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
FROM rust:1.89-bookworm AS builder
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
ENV CARGO_BUILD_JOBS=2
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
|
||||||
Suites: bookworm bookworm-updates
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
|
||||||
Suites: bookworm-security
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
|
||||||
-o Acquire::Retries=10 \
|
|
||||||
-o Acquire::http::Timeout=60 \
|
|
||||||
build-essential \
|
|
||||||
cmake \
|
|
||||||
pkg-config \
|
|
||||||
clang \
|
|
||||||
libclang-dev \
|
|
||||||
libssl-dev \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
COPY Cargo.toml Cargo.lock ./
|
|
||||||
COPY src ./src
|
|
||||||
|
|
||||||
RUN cargo build --release --bin rpki_rtr
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
|
||||||
|
|
||||||
RUN set -eux; \
|
|
||||||
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian
|
|
||||||
Suites: bookworm bookworm-updates
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
|
|
||||||
Types: deb
|
|
||||||
URIs: http://mirrors.tuna.tsinghua.edu.cn/debian-security
|
|
||||||
Suites: bookworm-security
|
|
||||||
Components: main
|
|
||||||
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
|
|
||||||
EOF
|
|
||||||
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --fix-missing --no-install-recommends \
|
|
||||||
-o Acquire::Retries=10 \
|
|
||||||
-o Acquire::http::Timeout=60 \
|
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/rpki_rtr /usr/local/bin/rpki_rtr
|
|
||||||
COPY --chmod=755 deploy/server/entrypoint.sh /usr/local/bin/rpki-rtr-entrypoint.sh
|
|
||||||
|
|
||||||
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs /app/report
|
|
||||||
|
|
||||||
ENV RPKI_RTR_ENABLE_TLS=false \
|
|
||||||
RPKI_RTR_TCP_ADDR=0.0.0.0:323 \
|
|
||||||
RPKI_RTR_TLS_ADDR=0.0.0.0:324 \
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db \
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data \
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm \
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report \
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=300 \
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=10 \
|
|
||||||
RPKI_RTR_REFRESH_INTERVAL_SECS=300 \
|
|
||||||
RPKI_RTR_ADMIN_ADDR="" \
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=""
|
|
||||||
|
|
||||||
EXPOSE 323 324
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/rpki-rtr-entrypoint.sh"]
|
|
||||||
@ -1,292 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
|
||||||
CALLER_TARGET_PLATFORM="${TARGET_PLATFORM:-${RPKI_RTR_TARGET_PLATFORM:-}}"
|
|
||||||
CALLER_IMAGE_ARCH="${IMAGE_ARCH:-${RPKI_RTR_IMAGE_ARCH:-}}"
|
|
||||||
CALLER_SERVER_IMAGE="${SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-}}"
|
|
||||||
|
|
||||||
load_env_defaults() {
|
|
||||||
local line key value
|
|
||||||
[[ -f "${SCRIPT_DIR}/.env" ]] || return
|
|
||||||
|
|
||||||
while IFS= read -r line || [[ -n "${line}" ]]; do
|
|
||||||
line="${line%$'\r'}"
|
|
||||||
[[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue
|
|
||||||
[[ "${line}" == *"="* ]] || continue
|
|
||||||
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
key="${key#"${key%%[![:space:]]*}"}"
|
|
||||||
key="${key%"${key##*[![:space:]]}"}"
|
|
||||||
[[ "${key}" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue
|
|
||||||
|
|
||||||
if [[ -z "${!key+x}" ]]; then
|
|
||||||
export "${key}=${value}"
|
|
||||||
fi
|
|
||||||
done < "${SCRIPT_DIR}/.env"
|
|
||||||
}
|
|
||||||
|
|
||||||
detect_image_arch() {
|
|
||||||
case "$(uname -m)" in
|
|
||||||
x86_64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
aarch64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) die "unsupported host architecture '$(uname -m)'; set RPKI_RTR_IMAGE_ARCH=amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
platform_for_image_arch() {
|
|
||||||
case "$1" in
|
|
||||||
amd64) printf '%s\n' linux/amd64 ;;
|
|
||||||
arm64) printf '%s\n' linux/arm64 ;;
|
|
||||||
*) die "unsupported image arch '$1', expected amd64 or arm64" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
image_arch_from_platform() {
|
|
||||||
case "$1" in
|
|
||||||
linux/amd64|amd64) printf '%s\n' amd64 ;;
|
|
||||||
linux/arm64|arm64) printf '%s\n' arm64 ;;
|
|
||||||
*) return 1 ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
load_env_defaults
|
|
||||||
|
|
||||||
MODE="${MODE:-tcp}"
|
|
||||||
ACTION="up"
|
|
||||||
DETACH="${DETACH:-1}"
|
|
||||||
BUILD="${BUILD:-1}"
|
|
||||||
TARGET_PLATFORM="${CALLER_TARGET_PLATFORM}"
|
|
||||||
IMAGE_ARCH="${CALLER_IMAGE_ARCH}"
|
|
||||||
SERVER_IMAGE="${CALLER_SERVER_IMAGE}"
|
|
||||||
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: host architecture)
|
|
||||||
--image-arch VALUE
|
|
||||||
Image architecture suffix for default tag: amd64|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
|
|
||||||
;;
|
|
||||||
--image-arch)
|
|
||||||
[[ $# -ge 2 ]] || die "--image-arch requires a value"
|
|
||||||
IMAGE_ARCH="$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
|
|
||||||
|
|
||||||
if [[ -z "${TARGET_PLATFORM}" ]]; then
|
|
||||||
[[ -n "${IMAGE_ARCH}" ]] || IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
TARGET_PLATFORM="$(platform_for_image_arch "${IMAGE_ARCH}")"
|
|
||||||
elif [[ -z "${IMAGE_ARCH}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" != *,* ]] && IMAGE_ARCH="$(image_arch_from_platform "${TARGET_PLATFORM}")"; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
IMAGE_ARCH="$(detect_image_arch)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -z "${SERVER_IMAGE}" ]]; then
|
|
||||||
if [[ "${TARGET_PLATFORM}" == *,* ]]; then
|
|
||||||
SERVER_IMAGE="${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}"
|
|
||||||
else
|
|
||||||
SERVER_IMAGE="${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${IMAGE_ARCH}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}"
|
|
||||||
if [[ "${ACTION}" == "buildx" ]]; then
|
|
||||||
has_buildx || die "docker buildx is not available"
|
|
||||||
else
|
|
||||||
has_compose || die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${REPO_ROOT}"
|
|
||||||
|
|
||||||
case "${ACTION}" in
|
|
||||||
up)
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
build)
|
|
||||||
compose_run build
|
|
||||||
;;
|
|
||||||
buildx)
|
|
||||||
buildx_server
|
|
||||||
;;
|
|
||||||
rebuild)
|
|
||||||
compose_run build --no-cache
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
down|stop)
|
|
||||||
compose_run down
|
|
||||||
;;
|
|
||||||
restart)
|
|
||||||
compose_run down
|
|
||||||
up_server
|
|
||||||
;;
|
|
||||||
logs)
|
|
||||||
compose_run logs -f
|
|
||||||
;;
|
|
||||||
ps)
|
|
||||||
compose_run ps
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-ssh
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "${RPKI_RTR_SSH_HOST_PORT:-2222}:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "true"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_SSH_ADDR: "0.0.0.0:${RPKI_RTR_SSH_CONTAINER_PORT:-22}"
|
|
||||||
RPKI_RTR_SSH_HOST_KEY_PATH: "${RPKI_RTR_SSH_HOST_KEY_PATH:-/app/certs/ssh_host_rsa_key}"
|
|
||||||
RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "${RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH:-/app/certs/rtr-authorized_keys}"
|
|
||||||
RPKI_RTR_SSH_USERNAME: "${RPKI_RTR_SSH_USERNAME:-rpki-rtr}"
|
|
||||||
RPKI_RTR_SSH_SUBSYSTEM_NAME: "${RPKI_RTR_SSH_SUBSYSTEM_NAME:-rpki-rtr}"
|
|
||||||
# SSH auth mode: key | password | both
|
|
||||||
RPKI_RTR_SSH_AUTH_MODE: "${RPKI_RTR_SSH_AUTH_MODE:-key}"
|
|
||||||
# Optional: enable password authentication in addition to publickey
|
|
||||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_SSH_CERTS_HOST_DIR:-../../certs/ssh}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-tcp
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "false"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-60}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS: "${RPKI_RTR_MAX_CONNECTIONS:-100000}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr-tls
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
# - "323:323"
|
|
||||||
- "324:324"
|
|
||||||
- "8323:8323"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "true"
|
|
||||||
RPKI_RTR_ENABLE_SSH: "false"
|
|
||||||
# RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
|
||||||
RPKI_RTR_TLS_CERT_PATH: "${RPKI_RTR_TLS_CERT_PATH:-/app/certs/server-dns.crt}"
|
|
||||||
RPKI_RTR_TLS_KEY_PATH: "${RPKI_RTR_TLS_KEY_PATH:-/app/certs/server-dns.key}"
|
|
||||||
RPKI_RTR_TLS_CLIENT_CA_PATH: "${RPKI_RTR_TLS_CLIENT_CA_PATH:-/app/certs/client-ca.crt}"
|
|
||||||
RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "${RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH:-false}"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_TLS_CERTS_HOST_DIR:-../../certs/tls}:/app/certs:ro
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
version: "3.9"
|
|
||||||
|
|
||||||
services:
|
|
||||||
rpki-rtr:
|
|
||||||
build:
|
|
||||||
context: ../..
|
|
||||||
dockerfile: deploy/server/Dockerfile
|
|
||||||
image: ${RPKI_RTR_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE_BASE:-rpki-rtr}-${RPKI_RTR_IMAGE_ARCH:-amd64}:${RPKI_RTR_SERVER_IMAGE_TAG:-latest}}
|
|
||||||
container_name: rpki-rtr
|
|
||||||
restart: no
|
|
||||||
ports:
|
|
||||||
- "323:323"
|
|
||||||
- "324:324"
|
|
||||||
- "8323:8323"
|
|
||||||
# SSH mode example:
|
|
||||||
# - "22:22"
|
|
||||||
environment:
|
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
|
||||||
RPKI_RTR_TLS_ADDR: "0.0.0.0:324"
|
|
||||||
RPKI_RTR_DB_PATH: "${RPKI_RTR_DB_PATH:-/app/rtr-db}"
|
|
||||||
RPKI_RTR_CCR_DIR: "${RPKI_RTR_CCR_DIR:-/app/data}"
|
|
||||||
RPKI_RTR_SLURM_DIR: "${RPKI_RTR_SLURM_DIR:-/app/slurm}"
|
|
||||||
RPKI_RTR_REPORT_DIR: "${RPKI_RTR_REPORT_DIR:-/app/report}"
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS: "${RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT: "${RPKI_RTR_REPORT_HISTORY_LIMIT:-10}"
|
|
||||||
RPKI_RTR_TIMEZONE: "${RPKI_RTR_TIMEZONE:-Asia/Shanghai}"
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "${RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS:-300}"
|
|
||||||
RPKI_RTR_MAX_DELTA: "${RPKI_RTR_MAX_DELTA:-10}"
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "${RPKI_RTR_MAX_CONCURRENT_HANDSHAKES:-128}"
|
|
||||||
RPKI_RTR_ADMIN_ADDR: "${RPKI_RTR_ADMIN_ADDR:-}"
|
|
||||||
RPKI_RTR_ADMIN_TOKEN: "${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
RUST_LOG: "${RUST_LOG:-info}"
|
|
||||||
# SSH mode example:
|
|
||||||
# RPKI_RTR_ENABLE_SSH: "true"
|
|
||||||
# RPKI_RTR_SSH_ADDR: "0.0.0.0:22"
|
|
||||||
# RPKI_RTR_SSH_PORT: "22"
|
|
||||||
# RPKI_RTR_SSH_HOST_KEY_PATH: "/app/certs/ssh_host_rsa_key"
|
|
||||||
# RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH: "/app/certs/rtr-authorized_keys"
|
|
||||||
# RPKI_RTR_SSH_USERNAME: "rpki-rtr"
|
|
||||||
# RPKI_RTR_SSH_SUBSYSTEM_NAME: "rpki-rtr"
|
|
||||||
# Optional: enable password auth in addition to publickey
|
|
||||||
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
|
||||||
volumes:
|
|
||||||
- ${RPKI_RTR_CCR_HOST_DIR:-../../data}:${RPKI_RTR_CCR_DIR:-/app/data}:ro
|
|
||||||
- ${RPKI_RTR_DB_HOST_DIR:-../../rtr-db}:${RPKI_RTR_DB_PATH:-/app/rtr-db}
|
|
||||||
- ${RPKI_RTR_SLURM_HOST_DIR:-../../data}:${RPKI_RTR_SLURM_DIR:-/app/slurm}
|
|
||||||
- ${RPKI_RTR_LOG_HOST_DIR:-../../logs/server}:/app/logs
|
|
||||||
- ${RPKI_RTR_REPORT_HOST_DIR:-../../report}:${RPKI_RTR_REPORT_DIR:-/app/report}
|
|
||||||
# TLS mode example:
|
|
||||||
# - ../../certs/ssh:/app/certs:ro
|
|
||||||
networks:
|
|
||||||
- rpki_net
|
|
||||||
|
|
||||||
networks:
|
|
||||||
rpki_net:
|
|
||||||
name: rpki_net
|
|
||||||
driver: bridge
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
mkdir -p /app/logs
|
|
||||||
|
|
||||||
log_name="${HOSTNAME:-rpki-rtr}"
|
|
||||||
stdout_log="/app/logs/${log_name}.stdout.log"
|
|
||||||
stderr_log="/app/logs/${log_name}.stderr.log"
|
|
||||||
|
|
||||||
exec /usr/local/bin/rpki_rtr "$@" >>"$stdout_log" 2>>"$stderr_log"
|
|
||||||
@ -1,310 +0,0 @@
|
|||||||
# RTR Admin API
|
|
||||||
|
|
||||||
本文档描述 RTR server 当前提供的 HTTP 管理接口。
|
|
||||||
|
|
||||||
## 启用方式
|
|
||||||
|
|
||||||
Admin API 默认关闭。设置 `RPKI_RTR_ADMIN_ADDR` 后启用:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_ADMIN_ADDR=127.0.0.1:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
如果在 Docker 中需要从宿主机访问,容器内建议监听:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=change-me
|
|
||||||
```
|
|
||||||
|
|
||||||
并在 compose 中映射端口:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
ports:
|
|
||||||
- "8323:8323"
|
|
||||||
```
|
|
||||||
|
|
||||||
安全规则:
|
|
||||||
|
|
||||||
- `RPKI_RTR_ADMIN_ADDR` 为空时,不启动 Admin API。
|
|
||||||
- 非 loopback 地址,例如 `0.0.0.0:8323`,必须配置 `RPKI_RTR_ADMIN_TOKEN`,否则 admin server 会拒绝启动。
|
|
||||||
- 设置 token 后,请求必须带 `Authorization: Bearer <token>`。
|
|
||||||
|
|
||||||
## 通用 Headers
|
|
||||||
|
|
||||||
```http
|
|
||||||
Authorization: Bearer change-me
|
|
||||||
Content-Type: application/json
|
|
||||||
```
|
|
||||||
|
|
||||||
GET 请求不需要 `Content-Type`。
|
|
||||||
|
|
||||||
## GET /admin/rtr/health
|
|
||||||
|
|
||||||
用于检查 Admin API 是否可达,以及当前启用了哪些管理能力。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/health \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config_api": true,
|
|
||||||
"source_reload_api": true,
|
|
||||||
"slurm_api": true,
|
|
||||||
"logs_api": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/logs/tail
|
|
||||||
|
|
||||||
实时读取 RTR 服务日志,效果类似 `tail -f`。默认读取 stdout 日志,先返回最近 200 行,然后持续输出新增内容。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -N "http://127.0.0.1:8323/admin/rtr/logs/tail?stream=stdout&lines=200&follow=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
Query 参数:
|
|
||||||
|
|
||||||
| 参数 | 默认值 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `stream` | `stdout` | 可选 `stdout` 或 `stderr`。 |
|
|
||||||
| `lines` | `200` | 首次返回的历史行数,范围会限制在 `1..=5000`。 |
|
|
||||||
| `follow` | `true` | `true` 时保持连接并持续输出新增日志;`false` 时只返回当前 tail 内容。 |
|
|
||||||
|
|
||||||
日志文件路径按容器 entrypoint 的规则解析:
|
|
||||||
|
|
||||||
- 目录:`RPKI_RTR_LOG_DIR`,默认 `/app/logs`。
|
|
||||||
- 文件名前缀:`RPKI_RTR_LOG_NAME`,未设置时使用 `HOSTNAME`,再未设置时为 `rpki-rtr`。
|
|
||||||
- stdout 文件:`<name>.stdout.log`。
|
|
||||||
- stderr 文件:`<name>.stderr.log`。
|
|
||||||
|
|
||||||
## GET /admin/rtr/config
|
|
||||||
|
|
||||||
查询当前运行中的 runtime 配置。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config": {
|
|
||||||
"max_delta": 10,
|
|
||||||
"prune_delta_by_snapshot_size": true,
|
|
||||||
"source_refresh_interval_seconds": 300,
|
|
||||||
"runtime_report_interval_seconds": 300,
|
|
||||||
"report_history_limit": 10,
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"timing": {
|
|
||||||
"refresh": 3600,
|
|
||||||
"retry": 600,
|
|
||||||
"expire": 7200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/config
|
|
||||||
|
|
||||||
运行中动态修改 RTR server 的部分 runtime 配置。请求体是 JSON object,支持部分更新,只需要传要修改的字段。
|
|
||||||
|
|
||||||
| 字段 | 类型 | 说明 |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `max_delta` | integer | 每个 RTR 版本最多保留的 delta 条数,必须 `>= 1`。 |
|
|
||||||
| `prune_delta_by_snapshot_size` | boolean | 是否按 snapshot wire size 裁剪 delta window。 |
|
|
||||||
| `source_refresh_interval_seconds` | integer | CCR/SLURM source refresh 间隔,单位秒,必须 `>= 1`。 |
|
|
||||||
| `runtime_report_interval_seconds` | integer | `rtr-runtime-*.json` 周期写入间隔,单位秒,必须 `>= 1`。 |
|
|
||||||
| `report_history_limit` | integer | 每类 report 文件滚动保留数量,必须 `>= 1`。 |
|
|
||||||
| `timezone` | string | IANA 时区名,例如 `Asia/Shanghai`、`UTC`、`Europe/London`。 |
|
|
||||||
| `timing.refresh` | integer | RTR EndOfData `refresh`,单位秒。 |
|
|
||||||
| `timing.retry` | integer | RTR EndOfData `retry`,单位秒。 |
|
|
||||||
| `timing.expire` | integer | RTR EndOfData `expire`,必须大于 `refresh` 和 `retry`。 |
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:8323/admin/rtr/config \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer change-me" \
|
|
||||||
-d '{"max_delta": 6}'
|
|
||||||
```
|
|
||||||
|
|
||||||
成功响应:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"config": {
|
|
||||||
"max_delta": 6,
|
|
||||||
"prune_delta_by_snapshot_size": true,
|
|
||||||
"source_refresh_interval_seconds": 300,
|
|
||||||
"runtime_report_interval_seconds": 300,
|
|
||||||
"report_history_limit": 10,
|
|
||||||
"timezone": "Asia/Shanghai",
|
|
||||||
"timing": {
|
|
||||||
"refresh": 3600,
|
|
||||||
"retry": 600,
|
|
||||||
"expire": 7200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/slurm/files
|
|
||||||
|
|
||||||
列出 `RPKI_RTR_SLURM_DIR` 下的 SLURM 文件。只返回合法的 `*.slurm` 和 `*.slurm.disabled` 文件,不返回 `.backup/`。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"files": [
|
|
||||||
{
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"path": "/app/slurm/local-policy.slurm",
|
|
||||||
"enabled": true,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "old-policy.slurm.disabled",
|
|
||||||
"path": "/app/slurm/old-policy.slurm.disabled",
|
|
||||||
"enabled": false,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## GET /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
读取单个 SLURM 文件,前端编辑页面可以直接使用这个接口加载内容。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
响应示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"file": {
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"path": "/app/slurm/local-policy.slurm",
|
|
||||||
"enabled": true,
|
|
||||||
"size_bytes": 168,
|
|
||||||
"modified_unix_seconds": 1782100000,
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files
|
|
||||||
|
|
||||||
新增或覆盖一个 SLURM 文件。文件名放在请求体中。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "local-policy.slurm",
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
|
||||||
"reload": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PUT /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
新增或覆盖指定 SLURM 文件。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X PUT http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer change-me" \
|
|
||||||
-d '{
|
|
||||||
"content": "{ \"slurmVersion\": 1, \"validationOutputFilters\": { \"prefixFilters\": [], \"bgpsecFilters\": [] }, \"locallyAddedAssertions\": { \"prefixAssertions\": [], \"bgpsecAssertions\": [] } }",
|
|
||||||
"reload": true
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## DELETE /admin/rtr/slurm/files/{name}
|
|
||||||
|
|
||||||
删除指定 SLURM 文件。删除前会先备份。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X DELETE "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files/{name}/disable
|
|
||||||
|
|
||||||
将启用文件改名为失效文件:
|
|
||||||
|
|
||||||
```text
|
|
||||||
local-policy.slurm -> local-policy.slurm.disabled
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm/disable?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/files/{name}/enable
|
|
||||||
|
|
||||||
将失效文件改名为启用文件:
|
|
||||||
|
|
||||||
```text
|
|
||||||
local-policy.slurm.disabled -> local-policy.slurm
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://127.0.0.1:8323/admin/rtr/slurm/files/local-policy.slurm.disabled/enable?reload=true" \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## POST /admin/rtr/slurm/reload
|
|
||||||
|
|
||||||
不修改文件,只立即触发一次 CCR/SLURM source reload。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://127.0.0.1:8323/admin/rtr/slurm/reload \
|
|
||||||
-H "Authorization: Bearer change-me"
|
|
||||||
```
|
|
||||||
|
|
||||||
## SLURM 文件规则
|
|
||||||
|
|
||||||
- `*.slurm`:启用,会被 source refresh 加载。
|
|
||||||
- `*.slurm.disabled`:失效,不会被加载。
|
|
||||||
- `.backup/`:接口自动生成的备份目录。
|
|
||||||
- 文件名只能是 basename,不能包含 `/`、`\` 或 `..`。
|
|
||||||
- 文件名只能包含 ASCII 字母、数字、`.`、`_`、`-`。
|
|
||||||
- 写入内容必须是合法 JSON,并且能被解析为合法 SLURM 文件。
|
|
||||||
- 单个请求体默认限制为 5 MiB。
|
|
||||||
- 覆盖、删除前会写入 `.backup/` 备份。
|
|
||||||
|
|
||||||
## 错误响应
|
|
||||||
|
|
||||||
| HTTP 状态码 | 含义 |
|
|
||||||
| --- | --- |
|
|
||||||
| `400 Bad Request` | JSON 非法、配置值非法、SLURM 文件名非法、目标文件不存在、reload 失败等。 |
|
|
||||||
| `401 Unauthorized` | 配置了 token,但请求缺少或使用了错误的 `Authorization`。 |
|
|
||||||
| `404 Not Found` | 路径或方法不支持。 |
|
|
||||||
| `413 Payload Too Large` | 请求体超过限制。 |
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
IMAGE_DIR="${ROOT_DIR}/images"
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
|
|
||||||
SERVER_TAR="${RPKI_RTR_SERVER_IMAGE_TAR:-}"
|
|
||||||
CLIENT_TAR="${RPKI_RTR_CLIENT_IMAGE_TAR:-}"
|
|
||||||
CONSOLE_TAR="${RPKI_RTR_CONSOLE_IMAGE_TAR:-}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/00_0_install.sh [options]
|
|
||||||
|
|
||||||
Load offline Docker image tarballs from ./images, then run scripts/00_1_pre_check.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--server-tar FILE Server image tarball. Env: RPKI_RTR_SERVER_IMAGE_TAR
|
|
||||||
--client-tar FILE Debug client image tarball. Env: RPKI_RTR_CLIENT_IMAGE_TAR
|
|
||||||
--console-tar FILE Console image tarball. Env: RPKI_RTR_CONSOLE_IMAGE_TAR
|
|
||||||
-h, --help Show this help.
|
|
||||||
|
|
||||||
Default tarballs under ./images:
|
|
||||||
server: rpki-rtr-*.tar, excluding rpki-rtr-debug-client-*.tar
|
|
||||||
client: rpki-rtr-debug-client-*.tar
|
|
||||||
console: rpki-console-ui-*.tar
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_install] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_install] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
[ -f "${env_file}" ] || die "env file not found: ${env_file}"
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--server-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--server-tar requires a value"
|
|
||||||
SERVER_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--client-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--client-tar requires a value"
|
|
||||||
CLIENT_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--console-tar)
|
|
||||||
[ "$#" -ge 2 ] || die "--console-tar requires a value"
|
|
||||||
CONSOLE_TAR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
resolve_tar() {
|
|
||||||
local path="$1"
|
|
||||||
|
|
||||||
if [ -f "${path}" ]; then
|
|
||||||
cd "$(dirname "${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -f "${ROOT_DIR}/${path}" ]; then
|
|
||||||
cd "$(dirname "${ROOT_DIR}/${path}")" && printf '%s/%s\n' "$(pwd -P)" "$(basename "${path}")"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
load_image() {
|
|
||||||
local label="$1"
|
|
||||||
local tar_path="$2"
|
|
||||||
local output image
|
|
||||||
|
|
||||||
[ -f "${tar_path}" ] || die "${label} image tarball not found: ${tar_path}"
|
|
||||||
output="$(docker load -i "${tar_path}" 2>&1)" || {
|
|
||||||
printf '%s\n' "${output}" >&2
|
|
||||||
die "failed to load ${label} image from ${tar_path}"
|
|
||||||
}
|
|
||||||
printf '%s\n' "${output}" >&2
|
|
||||||
|
|
||||||
image="$(printf '%s\n' "${output}" | sed -n 's/^Loaded image: //p' | tail -n 1)"
|
|
||||||
[ -n "${image}" ] || die "cannot determine ${label} image name from docker load output"
|
|
||||||
printf '%s\n' "${image}"
|
|
||||||
}
|
|
||||||
|
|
||||||
find_single_tar() {
|
|
||||||
local label="$1"
|
|
||||||
local include_pattern="$2"
|
|
||||||
local exclude_pattern="${3:-}"
|
|
||||||
local matches=()
|
|
||||||
local file
|
|
||||||
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
matches+=("${file}")
|
|
||||||
done < <(
|
|
||||||
if [ -n "${exclude_pattern}" ]; then
|
|
||||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" ! -name "${exclude_pattern}" -print0 | sort -z
|
|
||||||
else
|
|
||||||
find "${IMAGE_DIR}" -maxdepth 1 -type f -name "${include_pattern}" -print0 | sort -z
|
|
||||||
fi
|
|
||||||
)
|
|
||||||
|
|
||||||
case "${#matches[@]}" in
|
|
||||||
0)
|
|
||||||
die "no ${label} image tarball found under ${IMAGE_DIR} matching ${include_pattern}"
|
|
||||||
;;
|
|
||||||
1)
|
|
||||||
printf '%s\n' "${matches[0]}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
printf '[00_install] ERROR: multiple %s image tarballs found:\n' "${label}" >&2
|
|
||||||
printf ' %s\n' "${matches[@]}" >&2
|
|
||||||
die "please specify --${label}-tar or RPKI_RTR_${label^^}_IMAGE_TAR"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
[ -d "${IMAGE_DIR}" ] || die "image directory not found: ${IMAGE_DIR}"
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
|
|
||||||
if [ -n "${SERVER_TAR}" ]; then
|
|
||||||
SERVER_TAR="$(resolve_tar "${SERVER_TAR}")" || die "server image tarball not found: ${SERVER_TAR}"
|
|
||||||
else
|
|
||||||
SERVER_TAR="$(find_single_tar server 'rpki-rtr-*.tar' 'rpki-rtr-debug-client-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${CLIENT_TAR}" ]; then
|
|
||||||
CLIENT_TAR="$(resolve_tar "${CLIENT_TAR}")" || die "client image tarball not found: ${CLIENT_TAR}"
|
|
||||||
else
|
|
||||||
CLIENT_TAR="$(find_single_tar client 'rpki-rtr-debug-client-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${CONSOLE_TAR}" ]; then
|
|
||||||
CONSOLE_TAR="$(resolve_tar "${CONSOLE_TAR}")" || die "console image tarball not found: ${CONSOLE_TAR}"
|
|
||||||
else
|
|
||||||
CONSOLE_TAR="$(find_single_tar console 'rpki-console-ui-*.tar')"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Loading Docker Images"
|
|
||||||
SERVER_IMAGE="$(load_image server "${SERVER_TAR}")"
|
|
||||||
CLIENT_IMAGE="$(load_image client "${CLIENT_TAR}")"
|
|
||||||
CONSOLE_IMAGE="$(load_image console "${CONSOLE_TAR}")"
|
|
||||||
|
|
||||||
stage "Updating Deploy Env"
|
|
||||||
set_env_var "${SERVER_ENV_FILE}" RPKI_RTR_SERVER_IMAGE "${SERVER_IMAGE}"
|
|
||||||
set_env_var "${CONSOLE_ENV_FILE}" RPKI_CONSOLE_UI_IMAGE "${CONSOLE_IMAGE}"
|
|
||||||
|
|
||||||
stage "Running Pre-Check"
|
|
||||||
RPKI_RTR_SERVER_IMAGE="${SERVER_IMAGE}" \
|
|
||||||
RPKI_RTR_CLIENT_IMAGE="${CLIENT_IMAGE}" \
|
|
||||||
bash "${SCRIPT_DIR}/00_1_pre_check.sh" || die "pre-check failed"
|
|
||||||
|
|
||||||
stage "Install Finished"
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
|
||||||
PRECHECK_WAIT_SECS="${PRECHECK_WAIT_SECS:-60}"
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_1_pre_check] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Preparing Pre-Check Data"
|
|
||||||
bash "${SCRIPT_DIR}/00_2_pre_check_prepare.sh" || die "prepare step failed"
|
|
||||||
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${PRECHECK_ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
|
||||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN}"
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
client_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${PRECHECK_ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${PRECHECK_CLIENT_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup() {
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK}" PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR}" PRECHECK_ENV_FILE="${PRECHECK_ENV_FILE}" \
|
|
||||||
bash "${SCRIPT_DIR}/00_3_pre_check_cleanup.sh"
|
|
||||||
}
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
http_get() {
|
|
||||||
local host="$1"
|
|
||||||
local port="$2"
|
|
||||||
local path="$3"
|
|
||||||
local token="$4"
|
|
||||||
|
|
||||||
exec 3<>"/dev/tcp/${host}/${port}" || return 1
|
|
||||||
printf 'GET %s HTTP/1.1\r\nHost: %s:%s\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' \
|
|
||||||
"${path}" "${host}" "${port}" "${token}" >&3
|
|
||||||
cat <&3
|
|
||||||
exec 3>&-
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_admin_health() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
local response
|
|
||||||
|
|
||||||
log "waiting for server health"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
response="$(http_get 127.0.0.1 "${ADMIN_PORT}" /admin/rtr/health "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
|
||||||
if printf '%s' "${response}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'; then
|
|
||||||
log "admin health check passed"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$(server_compose ps -q rpki-rtr | wc -l | tr -d ' ')" = "0" ]; then
|
|
||||||
server_compose logs rpki-rtr || true
|
|
||||||
die "server container is not running"
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
server_compose logs rpki-rtr || true
|
|
||||||
die "admin health check did not pass within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_client_eod() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
local log_file
|
|
||||||
|
|
||||||
log "waiting for client response"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
for log_file in "${PRECHECK_WORK_DIR}"/logs/client/*.stdout.log; do
|
|
||||||
[ -f "${log_file}" ] || continue
|
|
||||||
if grep -q 'EndOfData:' "${log_file}" && grep -q 'summary[[:space:]]*:' "${log_file}"; then
|
|
||||||
log "client received EndOfData and payload summary"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
if grep -qi 'Error Report' "${log_file}"; then
|
|
||||||
cat "${log_file}" >&2
|
|
||||||
die "client received RTR Error Report"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
find "${PRECHECK_WORK_DIR}/logs/client" -type f -maxdepth 1 -print -exec cat {} \; >&2 || true
|
|
||||||
die "client did not receive EndOfData and summary within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_no_error_logs() {
|
|
||||||
local bad
|
|
||||||
log "checking logs"
|
|
||||||
bad="$(find "${PRECHECK_WORK_DIR}/logs" -type f -name '*.log' -print0 \
|
|
||||||
| xargs -0 grep -Ein 'panic|failed|RTR admin config server exited|Error Report' 2>/dev/null || true)"
|
|
||||||
if [ -n "${bad}" ]; then
|
|
||||||
printf '%s\n' "${bad}" >&2
|
|
||||||
die "error pattern found in pre-check logs"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_report_written() {
|
|
||||||
local deadline=$((SECONDS + PRECHECK_WAIT_SECS))
|
|
||||||
|
|
||||||
log "waiting for source report"
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if find "${PRECHECK_WORK_DIR}/report" -type f -name 'rtr-source-*.json' | grep -q .; then
|
|
||||||
log "source report written"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
die "server did not write rtr-source report within ${PRECHECK_WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Starting RTR Server"
|
|
||||||
server_compose up -d --no-build rpki-rtr
|
|
||||||
wait_for_admin_health
|
|
||||||
assert_report_written
|
|
||||||
|
|
||||||
stage "Starting Debug Client"
|
|
||||||
client_compose up -d --no-build rtr-debug-client
|
|
||||||
wait_for_client_eod
|
|
||||||
|
|
||||||
assert_no_error_logs
|
|
||||||
stage "Pre-Check Passed"
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
OUT_PATH="${1:-}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/00_2_generate_ccr.sh OUT.ccr
|
|
||||||
|
|
||||||
Generate a small deterministic CCR snapshot for pre-check.
|
|
||||||
This script writes a fixed base64-encoded CCR fixture and does not require Python.
|
|
||||||
The generated snapshot contains:
|
|
||||||
- VRP: 10.0.0.0/24 maxLen 24 AS65001
|
|
||||||
- VRP: 10.0.1.0/24 maxLen 24 AS65003
|
|
||||||
- VRP: 2001:db8:1::/48 maxLen 48 AS65002
|
|
||||||
- ASPA: customer AS65010 providers AS65011, AS65012, AS65013
|
|
||||||
- ASPA: customer AS65020 provider AS65021
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_2_generate_ccr] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${OUT_PATH}" = "-h" ] || [ "${OUT_PATH}" = "--help" ]; then
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${OUT_PATH}" ] || die "missing OUT.ccr"
|
|
||||||
mkdir -p "$(dirname "${OUT_PATH}")"
|
|
||||||
|
|
||||||
decode_base64() {
|
|
||||||
if command -v base64 >/dev/null 2>&1; then
|
|
||||||
base64 -d
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v openssl >/dev/null 2>&1; then
|
|
||||||
openssl base64 -d
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "base64 or openssl command is required to write CCR fixture"
|
|
||||||
}
|
|
||||||
|
|
||||||
cat <<'EOF' | decode_base64 >"${OUT_PATH}"
|
|
||||||
MIIBAwYLKoZIhvcNAQkQATaggfMwgfACAQAwDQYJYIZIAWUDBAIBBQAYDzIwMjYwNDAzMDAwMjAx
|
|
||||||
WqJ9MHswVzAaAgMA/ekwEzARBAIAATALMAkDBAAKAAACARgwGgIDAP3rMBMwEQQCAAEwCzAJAwQA
|
|
||||||
CgABAgEYMB0CAwD96jAWMBQEAgACMA4wDAMHACABDbgAAQIBMAQgsUgCHfy5AJcFp5RymOD0HuGO
|
|
||||||
wfhoWpll90ugYECGQPSjTDBKMCYwFgIDAP3yMA8CAwD98wIDAP30AgMA/fUwDAIDAP38MAUCAwD9
|
|
||||||
/QQgRPxVPOOUsUuuV4tZ8BG5PHNtKiaEr23Ird+yNxlWJ+w=
|
|
||||||
EOF
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
|
||||||
PRECHECK_ADMIN_TOKEN="${PRECHECK_ADMIN_TOKEN:-rpki-precheck-token}"
|
|
||||||
PRECHECK_PROTOCOL_VERSION="${PRECHECK_PROTOCOL_VERSION:-2}"
|
|
||||||
PRECHECK_SERVER_IMAGE="${PRECHECK_SERVER_IMAGE:-${RPKI_RTR_SERVER_IMAGE:-rpki-rtr-arm64:v1.2}}"
|
|
||||||
PRECHECK_CLIENT_IMAGE="${PRECHECK_CLIENT_IMAGE:-${RPKI_RTR_CLIENT_IMAGE:-rpki-rtr-debug-client-arm64:v1.2}}"
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check_prepare] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[00_1_pre_check_prepare] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
abs_dir() {
|
|
||||||
local path="$1"
|
|
||||||
mkdir -p "${path}"
|
|
||||||
cd "${path}" && pwd -P
|
|
||||||
}
|
|
||||||
|
|
||||||
[ -f "${SCRIPT_DIR}/00_2_generate_ccr.sh" ] || die "missing ${SCRIPT_DIR}/00_2_generate_ccr.sh"
|
|
||||||
|
|
||||||
rm -rf "${PRECHECK_WORK_DIR}"
|
|
||||||
mkdir -p \
|
|
||||||
"${PRECHECK_WORK_DIR}/ccr" \
|
|
||||||
"${PRECHECK_WORK_DIR}/slurm" \
|
|
||||||
"${PRECHECK_WORK_DIR}/db" \
|
|
||||||
"${PRECHECK_WORK_DIR}/logs/server" \
|
|
||||||
"${PRECHECK_WORK_DIR}/logs/client" \
|
|
||||||
"${PRECHECK_WORK_DIR}/report"
|
|
||||||
|
|
||||||
bash "${SCRIPT_DIR}/00_2_generate_ccr.sh" \
|
|
||||||
"${PRECHECK_WORK_DIR}/ccr/20260403T000201Z-precheck.ccr"
|
|
||||||
|
|
||||||
cat >"${PRECHECK_WORK_DIR}/slurm/precheck.slurm" <<'EOF'
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [
|
|
||||||
{
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"maxPrefixLength": 24,
|
|
||||||
"asn": 64496,
|
|
||||||
"comment": "pre-check local assertion"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR_ABS="$(abs_dir "${PRECHECK_WORK_DIR}")"
|
|
||||||
cat >"${PRECHECK_WORK_DIR}/precheck.env" <<EOF
|
|
||||||
PRECHECK_WORK_DIR=${PRECHECK_WORK_DIR_ABS}
|
|
||||||
PRECHECK_PROJECT=${PRECHECK_PROJECT}
|
|
||||||
RPKI_RTR_SERVER_IMAGE=${PRECHECK_SERVER_IMAGE}
|
|
||||||
RPKI_RTR_CLIENT_IMAGE=${PRECHECK_CLIENT_IMAGE}
|
|
||||||
PRECHECK_SERVER_COMPOSE_FILE=${ROOT_DIR}/deploy/server/docker-compose.tcp.yml
|
|
||||||
PRECHECK_CLIENT_COMPOSE_FILE=${ROOT_DIR}/deploy/client/docker-compose.tcp.yml
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/ccr
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/slurm
|
|
||||||
RPKI_RTR_DB_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/server
|
|
||||||
RPKI_RTR_CLIENT_LOG_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/logs/client
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=${PRECHECK_WORK_DIR_ABS}/report
|
|
||||||
RPKI_RTR_CCR_DIR=/app/data
|
|
||||||
RPKI_RTR_SLURM_DIR=/app/slurm
|
|
||||||
RPKI_RTR_DB_PATH=/app/rtr-db
|
|
||||||
RPKI_RTR_REPORT_DIR=/app/report
|
|
||||||
RPKI_RTR_ADMIN_ADDR=0.0.0.0:8323
|
|
||||||
RPKI_RTR_ADMIN_TOKEN=${PRECHECK_ADMIN_TOKEN}
|
|
||||||
RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS=5
|
|
||||||
RPKI_RTR_REPORT_HISTORY_LIMIT=3
|
|
||||||
RPKI_RTR_TIMEZONE=Asia/Shanghai
|
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS=30
|
|
||||||
RPKI_RTR_MAX_DELTA=10
|
|
||||||
RPKI_RTR_MAX_CONNECTIONS=32
|
|
||||||
RPKI_RTR_MAX_CONCURRENT_HANDSHAKES=8
|
|
||||||
RPKI_RTR_TCP_SERVER_ADDR=rpki-rtr:323
|
|
||||||
RPKI_RTR_PROTOCOL_VERSION=${PRECHECK_PROTOCOL_VERSION}
|
|
||||||
RUST_LOG=info
|
|
||||||
EOF
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
PRECHECK_WORK_DIR="${PRECHECK_WORK_DIR:-${ROOT_DIR}/.precheck}"
|
|
||||||
ENV_FILE="${PRECHECK_ENV_FILE:-${PRECHECK_WORK_DIR}/precheck.env}"
|
|
||||||
KEEP_PRECHECK="${KEEP_PRECHECK:-0}"
|
|
||||||
|
|
||||||
if [ -f "${ENV_FILE}" ]; then
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
fi
|
|
||||||
|
|
||||||
PRECHECK_PROJECT="${PRECHECK_PROJECT:-rpki-precheck}"
|
|
||||||
PRECHECK_SERVER_COMPOSE_FILE="${PRECHECK_SERVER_COMPOSE_FILE:-${ROOT_DIR}/deploy/server/docker-compose.tcp.yml}"
|
|
||||||
PRECHECK_CLIENT_COMPOSE_FILE="${PRECHECK_CLIENT_COMPOSE_FILE:-${ROOT_DIR}/deploy/client/docker-compose.tcp.yml}"
|
|
||||||
COMPOSE_BIN=()
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[00_1_pre_check_cleanup] %s\n' "$*"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_run() {
|
|
||||||
local compose_file="$1"
|
|
||||||
shift
|
|
||||||
|
|
||||||
if [ "${#COMPOSE_BIN[@]}" -eq 0 ]; then
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${ENV_FILE}" -p "${PRECHECK_PROJECT}" -f "${compose_file}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
if command -v docker >/dev/null 2>&1; then
|
|
||||||
log "stopping pre-check client compose stack"
|
|
||||||
[ -f "${PRECHECK_CLIENT_COMPOSE_FILE}" ] && compose_run "${PRECHECK_CLIENT_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
|
||||||
log "stopping pre-check server compose stack"
|
|
||||||
[ -f "${PRECHECK_SERVER_COMPOSE_FILE}" ] && compose_run "${PRECHECK_SERVER_COMPOSE_FILE}" down --remove-orphans >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "${KEEP_PRECHECK}" = "1" ]; then
|
|
||||||
log "KEEP_PRECHECK=1, preserving ${PRECHECK_WORK_DIR}"
|
|
||||||
else
|
|
||||||
rm -rf "${PRECHECK_WORK_DIR}"
|
|
||||||
log "removed ${PRECHECK_WORK_DIR}"
|
|
||||||
fi
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.server"
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_COMPOSE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
WAIT_SECS="${RPKI_START_WAIT_SECS:-20}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/01_start.sh [options]
|
|
||||||
|
|
||||||
Read root ./environment.server, update deploy/server/.env, then start the RTR server and console.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--env FILE Common env file. Default: ./environment.server
|
|
||||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
|
||||||
--wait SECS Wait timeout for container running checks. Default: 20
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[01_start] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[01_start] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
|
||||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
update_env_from_common() {
|
|
||||||
local target_env_file="$1"
|
|
||||||
local updated_count=0
|
|
||||||
local line key value
|
|
||||||
|
|
||||||
while IFS= read -r line || [ -n "${line}" ]; do
|
|
||||||
case "${line}" in
|
|
||||||
''|\#*) continue ;;
|
|
||||||
*=*)
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
set_env_var "${target_env_file}" "${key}" "${value}"
|
|
||||||
updated_count=$((updated_count + 1))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <"${COMMON_ENV_FILE}"
|
|
||||||
|
|
||||||
printf '%s\n' "${updated_count}"
|
|
||||||
}
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
console_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_COMPOSE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
container_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
else
|
|
||||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
show_service_debug() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
|
|
||||||
log "${service} is not running, current compose status:"
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
server_compose ps "${service}" >&2 || true
|
|
||||||
log "${service} recent logs:"
|
|
||||||
server_compose logs --tail=80 "${service}" >&2 || true
|
|
||||||
else
|
|
||||||
console_compose ps "${service}" >&2 || true
|
|
||||||
log "${service} recent logs:"
|
|
||||||
console_compose logs --tail=80 "${service}" >&2 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local deadline=$((SECONDS + WAIT_SECS))
|
|
||||||
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if container_running "${compose_kind}" "${service}"; then
|
|
||||||
log "${service} is running"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
show_service_debug "${compose_kind}" "${service}"
|
|
||||||
die "${service} failed to reach running state after ${WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--env)
|
|
||||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
|
||||||
COMMON_ENV_FILE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--wait)
|
|
||||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
|
||||||
WAIT_SECS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
|
||||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
|
||||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
|
||||||
stage "Reading Environment"
|
|
||||||
server_updated_count="$(update_env_from_common "${SERVER_ENV_FILE}")"
|
|
||||||
log "updated ${server_updated_count} server env variable(s)"
|
|
||||||
else
|
|
||||||
stage "Reading Environment"
|
|
||||||
log "environment.server file not found, skip env update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Starting RTR Server"
|
|
||||||
log "starting docker compose service: rpki-rtr"
|
|
||||||
server_compose up -d --no-build rpki-rtr
|
|
||||||
log "waiting for rpki-rtr to be running"
|
|
||||||
wait_for_running server rpki-rtr
|
|
||||||
|
|
||||||
stage "Starting Console"
|
|
||||||
log "starting docker compose service: rpki-console-ui"
|
|
||||||
console_compose up -d --no-build rpki-console-ui
|
|
||||||
log "waiting for rpki-console-ui to be running"
|
|
||||||
wait_for_running console rpki-console-ui
|
|
||||||
|
|
||||||
stage "Start Finished"
|
|
||||||
@ -1,184 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
SERVER_COMPOSE_FILE="${ROOT_DIR}/deploy/server/docker-compose.tcp.yml"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
WAIT_SECS="${RPKI_SELF_CHECK_WAIT_SECS:-60}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/02_self_check.sh [options]
|
|
||||||
|
|
||||||
Check whether the RTR server and console started by scripts/01_start.sh are healthy.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--wait SECS Wait timeout for each check. Default: 60
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[02_self_check] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[02_self_check] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--wait)
|
|
||||||
[ "$#" -ge 2 ] || die "--wait requires a value"
|
|
||||||
WAIT_SECS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || die "server compose file not found: ${SERVER_COMPOSE_FILE}"
|
|
||||||
[ -f "${CONSOLE_ENV_FILE}" ] || die "console env file not found: ${CONSOLE_ENV_FILE}"
|
|
||||||
[ -f "${CONSOLE_COMPOSE_FILE}" ] || die "console compose file not found: ${CONSOLE_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${SERVER_ENV_FILE}"
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "${CONSOLE_ENV_FILE}"
|
|
||||||
set +a
|
|
||||||
|
|
||||||
RTR_PORT="${RPKI_RTR_TCP_HOST_PORT:-323}"
|
|
||||||
ADMIN_PORT="${RPKI_RTR_ADMIN_ADDR##*:}"
|
|
||||||
ADMIN_PORT="${ADMIN_PORT:-8323}"
|
|
||||||
ADMIN_TOKEN="${RPKI_RTR_ADMIN_TOKEN:-}"
|
|
||||||
CONSOLE_PORT="${RPKI_CONSOLE_UI_PORT:-5173}"
|
|
||||||
|
|
||||||
server_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
console_compose() {
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
container_running() {
|
|
||||||
local compose_kind="$1"
|
|
||||||
local service="$2"
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
if [ "${compose_kind}" = "server" ]; then
|
|
||||||
cid="$(server_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
else
|
|
||||||
cid="$(console_compose ps -q "${service}" 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
tcp_open() {
|
|
||||||
local host="$1"
|
|
||||||
local port="$2"
|
|
||||||
|
|
||||||
if command -v nc >/dev/null 2>&1; then
|
|
||||||
nc -z "${host}" "${port}" >/dev/null 2>&1
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
(exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
http_get() {
|
|
||||||
local url="$1"
|
|
||||||
local token="${2:-}"
|
|
||||||
|
|
||||||
if command -v curl >/dev/null 2>&1; then
|
|
||||||
if [ -n "${token}" ]; then
|
|
||||||
curl -fsS -H "Authorization: Bearer ${token}" "${url}"
|
|
||||||
else
|
|
||||||
curl -fsS "${url}"
|
|
||||||
fi
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
die "curl command is required for HTTP health checks"
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_until() {
|
|
||||||
local label="$1"
|
|
||||||
shift
|
|
||||||
local deadline=$((SECONDS + WAIT_SECS))
|
|
||||||
|
|
||||||
while [ "${SECONDS}" -lt "${deadline}" ]; do
|
|
||||||
if "$@"; then
|
|
||||||
log "${label}: ok"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
die "${label}: failed after ${WAIT_SECS}s"
|
|
||||||
}
|
|
||||||
|
|
||||||
show_command() {
|
|
||||||
printf '[02_self_check] check command: %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
check_admin_health() {
|
|
||||||
local body
|
|
||||||
body="$(http_get "http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health" "${ADMIN_TOKEN}" 2>/dev/null || true)"
|
|
||||||
printf '%s' "${body}" | grep -q '"status"[[:space:]]*:[[:space:]]*"ok"'
|
|
||||||
}
|
|
||||||
|
|
||||||
check_console_http() {
|
|
||||||
http_get "http://127.0.0.1:${CONSOLE_PORT}/" >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
stage "Checking Containers"
|
|
||||||
show_command "${COMPOSE_BIN[*]} --env-file ${SERVER_ENV_FILE} -p ${SERVER_PROJECT} -f ${SERVER_COMPOSE_FILE} ps -q rpki-rtr && docker inspect"
|
|
||||||
wait_until "server container" container_running server rpki-rtr
|
|
||||||
show_command "${COMPOSE_BIN[*]} --env-file ${CONSOLE_ENV_FILE} -p ${CONSOLE_PROJECT} -f ${CONSOLE_COMPOSE_FILE} ps -q rpki-console-ui && docker inspect"
|
|
||||||
wait_until "console container" container_running console rpki-console-ui
|
|
||||||
|
|
||||||
stage "Checking Server"
|
|
||||||
show_command "tcp connect 127.0.0.1:${RTR_PORT}"
|
|
||||||
wait_until "RTR port ${RTR_PORT}" tcp_open 127.0.0.1 "${RTR_PORT}"
|
|
||||||
show_command "curl -fsS -H 'Authorization: Bearer <token>' http://127.0.0.1:${ADMIN_PORT}/admin/rtr/health"
|
|
||||||
wait_until "Admin health" check_admin_health
|
|
||||||
|
|
||||||
stage "Checking Console"
|
|
||||||
show_command "tcp connect 127.0.0.1:${CONSOLE_PORT}"
|
|
||||||
wait_until "Console port ${CONSOLE_PORT}" tcp_open 127.0.0.1 "${CONSOLE_PORT}"
|
|
||||||
show_command "curl -fsS http://127.0.0.1:${CONSOLE_PORT}/"
|
|
||||||
wait_until "Console HTTP" check_console_http
|
|
||||||
|
|
||||||
stage "Self-Check Passed"
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
SERVER_ENV_FILE="${ROOT_DIR}/deploy/server/.env"
|
|
||||||
CONSOLE_ENV_FILE="${ROOT_DIR}/deploy/console/.env"
|
|
||||||
MODE="${RPKI_RTR_SERVER_MODE:-tcp}"
|
|
||||||
SERVER_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
CONSOLE_PROJECT="${RPKI_CONSOLE_COMPOSE_PROJECT:-rpki-console}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/03_shutdown.sh [options]
|
|
||||||
|
|
||||||
Stop services started by scripts/01_start.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Server mode: tcp|tls|ssh|base. Default: tcp
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[03_shutdown] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[03_shutdown] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.ssh.yml" ;;
|
|
||||||
base) printf '%s\n' "${ROOT_DIR}/deploy/server/docker-compose.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh|base" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
SERVER_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
CONSOLE_COMPOSE_FILE="${ROOT_DIR}/deploy/console/docker-compose.yml"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping Console"
|
|
||||||
if [ -f "${CONSOLE_ENV_FILE}" ] && [ -f "${CONSOLE_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CONSOLE_ENV_FILE}" -p "${CONSOLE_PROJECT}" -f "${CONSOLE_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "console compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping RTR Server"
|
|
||||||
if [ -f "${SERVER_ENV_FILE}" ] && [ -f "${SERVER_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${SERVER_PROJECT}" -f "${SERVER_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "server compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Shutdown Finished"
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
SERVER_DIR="${ROOT_DIR}/deploy/server"
|
|
||||||
SERVER_ENV_FILE="${SERVER_DIR}/.env"
|
|
||||||
SERVER_COMPOSE_FILE="${SERVER_DIR}/docker-compose.tcp.yml"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_COMPOSE_PROJECT:-rpki-rtr}"
|
|
||||||
FORCE=0
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/04_cleanup.sh [options]
|
|
||||||
|
|
||||||
Remove server generated directories: logs, reports, and RTR database.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--force Clean even if the server container appears to be running.
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[04_cleanup] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[04_cleanup] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
env_value() {
|
|
||||||
local key="$1"
|
|
||||||
local default_value="$2"
|
|
||||||
local line
|
|
||||||
|
|
||||||
line="$(grep -E "^[[:space:]]*${key}=" "${SERVER_ENV_FILE}" | tail -n 1 || true)"
|
|
||||||
if [ -n "${line}" ]; then
|
|
||||||
printf '%s\n' "${line#*=}"
|
|
||||||
else
|
|
||||||
printf '%s\n' "${default_value}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_host_path() {
|
|
||||||
local raw_path="$1"
|
|
||||||
|
|
||||||
case "${raw_path}" in
|
|
||||||
/*|[A-Za-z]:/*|[A-Za-z]:\\*)
|
|
||||||
printf '%s\n' "${raw_path}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
(cd "${SERVER_DIR}" && cd "${raw_path}" 2>/dev/null && pwd -P) || printf '%s/%s\n' "${SERVER_DIR}" "${raw_path}"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_available() {
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
server_running() {
|
|
||||||
local cid state
|
|
||||||
|
|
||||||
[ -f "${SERVER_COMPOSE_FILE}" ] || return 1
|
|
||||||
compose_available || return 1
|
|
||||||
|
|
||||||
cid="$("${COMPOSE_BIN[@]}" --env-file "${SERVER_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${SERVER_COMPOSE_FILE}" ps -q rpki-rtr 2>/dev/null || true)"
|
|
||||||
[ -n "${cid}" ] || return 1
|
|
||||||
|
|
||||||
state="$(docker inspect -f '{{.State.Running}}' "${cid}" 2>/dev/null || true)"
|
|
||||||
[ "${state}" = "true" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
clean_dir() {
|
|
||||||
local label="$1"
|
|
||||||
local path="$2"
|
|
||||||
local root_with_slash
|
|
||||||
|
|
||||||
if [ ! -d "${path}" ]; then
|
|
||||||
log "${label}: ${path} not found, skip"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
root_with_slash="${ROOT_DIR}/"
|
|
||||||
case "${path}/" in
|
|
||||||
"${root_with_slash}"*) ;;
|
|
||||||
*)
|
|
||||||
log "ERROR: ${label} path is outside repo root, refuse to remove: ${path}"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log "${label}: removing ${path}"
|
|
||||||
if ! rm -rf "${path}"; then
|
|
||||||
log "ERROR: failed to remove ${label}: ${path}"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--force)
|
|
||||||
FORCE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
stage "Checking Server"
|
|
||||||
[ -f "${SERVER_ENV_FILE}" ] || die "server env file not found: ${SERVER_ENV_FILE}"
|
|
||||||
if [ "${FORCE}" -ne 1 ] && command -v docker >/dev/null 2>&1 && server_running; then
|
|
||||||
die "rpki-rtr is still running, run scripts/03_shutdown.sh first or use --force"
|
|
||||||
fi
|
|
||||||
|
|
||||||
LOG_DIR="${ROOT_DIR}/logs"
|
|
||||||
REPORT_DIR="$(resolve_host_path "$(env_value RPKI_RTR_REPORT_HOST_DIR '../../report')")"
|
|
||||||
DB_DIR="$(resolve_host_path "$(env_value RPKI_RTR_DB_HOST_DIR '../../rtr-db')")"
|
|
||||||
|
|
||||||
stage "Removing Server Files"
|
|
||||||
failed_count=0
|
|
||||||
clean_dir "logs" "${LOG_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
clean_dir "report" "${REPORT_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
clean_dir "rtr-db" "${DB_DIR}" || failed_count=$((failed_count + 1))
|
|
||||||
|
|
||||||
if [ "${failed_count}" -gt 0 ]; then
|
|
||||||
die "${failed_count} cleanup item(s) failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Cleanup Finished"
|
|
||||||
@ -1,303 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
OUT_DIR="${ROOT_DIR}/certs"
|
|
||||||
TLS_DIR="${OUT_DIR}/tls"
|
|
||||||
SSH_DIR="${OUT_DIR}/ssh"
|
|
||||||
SERVER_DNS="localhost"
|
|
||||||
FORCE=0
|
|
||||||
DO_TLS=1
|
|
||||||
DO_SSH=1
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'USAGE'
|
|
||||||
Generate local development TLS certificates and SSH keys for RTR Docker deploys.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
scripts/10_generate-certs.sh [options]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--out-dir DIR Output directory, default: ./certs
|
|
||||||
--server-dns NAME TLS server dNSName SAN, default: localhost
|
|
||||||
--tls-only Generate only TLS files
|
|
||||||
--ssh-only Generate only SSH files
|
|
||||||
--force Overwrite existing files
|
|
||||||
-h, --help Show this help
|
|
||||||
|
|
||||||
Generated TLS files:
|
|
||||||
certs/tls/client-ca.crt
|
|
||||||
certs/tls/client-ca.key
|
|
||||||
certs/tls/server-dns.crt
|
|
||||||
certs/tls/server-dns.key
|
|
||||||
certs/tls/client-good.crt
|
|
||||||
certs/tls/client-good.key
|
|
||||||
|
|
||||||
Generated SSH files:
|
|
||||||
certs/ssh/ssh_host_rsa_key
|
|
||||||
certs/ssh/ssh_host_rsa_key.pub
|
|
||||||
certs/ssh/rtr-client.key
|
|
||||||
certs/ssh/rtr-client.key.pub
|
|
||||||
certs/ssh/rtr-authorized_keys
|
|
||||||
USAGE
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
echo "error: $*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
info() {
|
|
||||||
echo "[generate-certs] $*"
|
|
||||||
}
|
|
||||||
|
|
||||||
need_cmd() {
|
|
||||||
command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
run_quiet() {
|
|
||||||
local log_file
|
|
||||||
log_file="$(mktemp)"
|
|
||||||
if "$@" >"${log_file}" 2>&1; then
|
|
||||||
rm -f "${log_file}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat "${log_file}" >&2
|
|
||||||
rm -f "${log_file}"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_abs_out_dir() {
|
|
||||||
case "${OUT_DIR}" in
|
|
||||||
/*) ;;
|
|
||||||
[A-Za-z]:*) ;;
|
|
||||||
*) OUT_DIR="${ROOT_DIR}/${OUT_DIR}" ;;
|
|
||||||
esac
|
|
||||||
TLS_DIR="${OUT_DIR}/tls"
|
|
||||||
SSH_DIR="${OUT_DIR}/ssh"
|
|
||||||
}
|
|
||||||
|
|
||||||
check_overwrite() {
|
|
||||||
local path="$1"
|
|
||||||
if [[ -e "${path}" && "${FORCE}" != "1" ]]; then
|
|
||||||
die "${path} already exists; rerun with --force to overwrite"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
write_tls_config_files() {
|
|
||||||
local tmp_dir="$1"
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/ca.cnf" <<'EOF'
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = rpki-rtr-client-ca
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/server.cnf" <<EOF
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = ${SERVER_DNS}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/client.cnf" <<'EOF'
|
|
||||||
[ req ]
|
|
||||||
distinguished_name = req_distinguished_name
|
|
||||||
prompt = no
|
|
||||||
|
|
||||||
[ req_distinguished_name ]
|
|
||||||
CN = rpki-rtr-client
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/server.ext" <<EOF
|
|
||||||
basicConstraints=CA:FALSE
|
|
||||||
keyUsage=digitalSignature,keyEncipherment
|
|
||||||
extendedKeyUsage=serverAuth
|
|
||||||
subjectAltName=DNS:${SERVER_DNS}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat >"${tmp_dir}/client.ext" <<'EOF'
|
|
||||||
basicConstraints=CA:FALSE
|
|
||||||
keyUsage=digitalSignature,keyEncipherment
|
|
||||||
extendedKeyUsage=clientAuth
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_tls() {
|
|
||||||
need_cmd openssl
|
|
||||||
mkdir -p "${TLS_DIR}"
|
|
||||||
|
|
||||||
info "generating TLS certificates in ${TLS_DIR}"
|
|
||||||
info "TLS server dNSName SAN: ${SERVER_DNS}"
|
|
||||||
|
|
||||||
local files=(
|
|
||||||
"${TLS_DIR}/client-ca.crt"
|
|
||||||
"${TLS_DIR}/client-ca.key"
|
|
||||||
"${TLS_DIR}/server-dns.crt"
|
|
||||||
"${TLS_DIR}/server-dns.key"
|
|
||||||
"${TLS_DIR}/client-good.crt"
|
|
||||||
"${TLS_DIR}/client-good.key"
|
|
||||||
)
|
|
||||||
for file in "${files[@]}"; do
|
|
||||||
check_overwrite "${file}"
|
|
||||||
done
|
|
||||||
|
|
||||||
local tmp_dir
|
|
||||||
tmp_dir="$(mktemp -d)"
|
|
||||||
write_tls_config_files "${tmp_dir}"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-ca.key" 2048
|
|
||||||
info "created TLS CA key: ${TLS_DIR}/client-ca.key"
|
|
||||||
run_quiet openssl req -x509 -new -nodes \
|
|
||||||
-config "${tmp_dir}/ca.cnf" \
|
|
||||||
-key "${TLS_DIR}/client-ca.key" \
|
|
||||||
-sha256 -days 3650 \
|
|
||||||
-out "${TLS_DIR}/client-ca.crt"
|
|
||||||
info "created TLS CA cert: ${TLS_DIR}/client-ca.crt"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/server-dns.key" 2048
|
|
||||||
info "created TLS server key: ${TLS_DIR}/server-dns.key"
|
|
||||||
run_quiet openssl req -new \
|
|
||||||
-config "${tmp_dir}/server.cnf" \
|
|
||||||
-key "${TLS_DIR}/server-dns.key" \
|
|
||||||
-out "${tmp_dir}/server-dns.csr"
|
|
||||||
run_quiet openssl x509 -req \
|
|
||||||
-in "${tmp_dir}/server-dns.csr" \
|
|
||||||
-CA "${TLS_DIR}/client-ca.crt" \
|
|
||||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
|
||||||
-CAcreateserial \
|
|
||||||
-out "${TLS_DIR}/server-dns.crt" \
|
|
||||||
-days 3650 -sha256 \
|
|
||||||
-extfile "${tmp_dir}/server.ext"
|
|
||||||
info "created TLS server cert: ${TLS_DIR}/server-dns.crt"
|
|
||||||
|
|
||||||
run_quiet openssl genrsa -out "${TLS_DIR}/client-good.key" 2048
|
|
||||||
info "created TLS client key: ${TLS_DIR}/client-good.key"
|
|
||||||
run_quiet openssl req -new \
|
|
||||||
-config "${tmp_dir}/client.cnf" \
|
|
||||||
-key "${TLS_DIR}/client-good.key" \
|
|
||||||
-out "${tmp_dir}/client-good.csr"
|
|
||||||
run_quiet openssl x509 -req \
|
|
||||||
-in "${tmp_dir}/client-good.csr" \
|
|
||||||
-CA "${TLS_DIR}/client-ca.crt" \
|
|
||||||
-CAkey "${TLS_DIR}/client-ca.key" \
|
|
||||||
-CAcreateserial \
|
|
||||||
-out "${TLS_DIR}/client-good.crt" \
|
|
||||||
-days 3650 -sha256 \
|
|
||||||
-extfile "${tmp_dir}/client.ext"
|
|
||||||
info "created TLS client cert: ${TLS_DIR}/client-good.crt"
|
|
||||||
|
|
||||||
rm -f "${TLS_DIR}/client-ca.srl"
|
|
||||||
rm -rf "${tmp_dir}"
|
|
||||||
chmod 600 "${TLS_DIR}"/*.key
|
|
||||||
chmod 644 "${TLS_DIR}"/*.crt
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_ssh_key() {
|
|
||||||
local path="$1"
|
|
||||||
local comment="$2"
|
|
||||||
check_overwrite "${path}"
|
|
||||||
check_overwrite "${path}.pub"
|
|
||||||
rm -f "${path}" "${path}.pub"
|
|
||||||
run_quiet ssh-keygen -q -t rsa -b 3072 -m PEM -N "" -C "${comment}" -f "${path}"
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_ssh() {
|
|
||||||
need_cmd ssh-keygen
|
|
||||||
mkdir -p "${SSH_DIR}"
|
|
||||||
|
|
||||||
info "generating SSH keys in ${SSH_DIR}"
|
|
||||||
generate_ssh_key "${SSH_DIR}/ssh_host_rsa_key" "rpki-rtr-host"
|
|
||||||
info "created SSH host key: ${SSH_DIR}/ssh_host_rsa_key"
|
|
||||||
generate_ssh_key "${SSH_DIR}/rtr-client.key" "rpki-rtr-debug-client"
|
|
||||||
info "created SSH client key: ${SSH_DIR}/rtr-client.key"
|
|
||||||
|
|
||||||
check_overwrite "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
cp "${SSH_DIR}/rtr-client.key.pub" "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
info "created SSH authorized_keys: ${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
|
|
||||||
chmod 600 \
|
|
||||||
"${SSH_DIR}/ssh_host_rsa_key" \
|
|
||||||
"${SSH_DIR}/rtr-client.key"
|
|
||||||
chmod 644 "${SSH_DIR}"/*.pub "${SSH_DIR}/rtr-authorized_keys"
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--out-dir)
|
|
||||||
[[ $# -ge 2 ]] || die "--out-dir requires a value"
|
|
||||||
OUT_DIR="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--server-dns)
|
|
||||||
[[ $# -ge 2 ]] || die "--server-dns requires a value"
|
|
||||||
SERVER_DNS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--tls-only)
|
|
||||||
DO_TLS=1
|
|
||||||
DO_SSH=0
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--ssh-only)
|
|
||||||
DO_TLS=0
|
|
||||||
DO_SSH=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--force)
|
|
||||||
FORCE=1
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
ensure_abs_out_dir
|
|
||||||
|
|
||||||
if [[ "${DO_TLS}" == "1" ]]; then
|
|
||||||
generate_tls
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${DO_SSH}" == "1" ]]; then
|
|
||||||
generate_ssh
|
|
||||||
fi
|
|
||||||
|
|
||||||
info "done"
|
|
||||||
info "generated files under ${OUT_DIR}"
|
|
||||||
|
|
||||||
if [[ "${DO_TLS}" == "1" ]]; then
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
TLS files:
|
|
||||||
${TLS_DIR}/client-ca.crt
|
|
||||||
${TLS_DIR}/server-dns.crt
|
|
||||||
${TLS_DIR}/server-dns.key
|
|
||||||
${TLS_DIR}/client-good.crt
|
|
||||||
${TLS_DIR}/client-good.key
|
|
||||||
${TLS_DIR}/client-ca.key (local signing key; not mounted by compose)
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "${DO_SSH}" == "1" ]]; then
|
|
||||||
cat <<EOF
|
|
||||||
|
|
||||||
SSH files:
|
|
||||||
${SSH_DIR}/ssh_host_rsa_key
|
|
||||||
${SSH_DIR}/ssh_host_rsa_key.pub
|
|
||||||
${SSH_DIR}/rtr-client.key
|
|
||||||
${SSH_DIR}/rtr-client.key.pub
|
|
||||||
${SSH_DIR}/rtr-authorized_keys
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
COMMON_ENV_FILE="${ROOT_DIR}/environment.client"
|
|
||||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
|
||||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/11_start_client.sh [options]
|
|
||||||
|
|
||||||
Read root ./environment.client, update deploy/client/.env, then start the RTR debug client.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
|
||||||
--env FILE Client common env file. Default: ./environment.client
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[11_start_client] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[11_start_client] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
set_env_var() {
|
|
||||||
local env_file="$1"
|
|
||||||
local key="$2"
|
|
||||||
local value="$3"
|
|
||||||
local tmp_file
|
|
||||||
|
|
||||||
tmp_file="${env_file}.tmp.$$"
|
|
||||||
awk -v key="${key}" -v value="${value}" '
|
|
||||||
BEGIN { done = 0 }
|
|
||||||
$0 ~ "^[[:space:]]*#?[[:space:]]*" key "=" {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
done = 1
|
|
||||||
}
|
|
||||||
next
|
|
||||||
}
|
|
||||||
{ print }
|
|
||||||
END {
|
|
||||||
if (!done) {
|
|
||||||
print key "=" value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
' "${env_file}" >"${tmp_file}"
|
|
||||||
mv "${tmp_file}" "${env_file}"
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--env)
|
|
||||||
[ "$#" -ge 2 ] || die "--env requires a value"
|
|
||||||
COMMON_ENV_FILE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -f "${CLIENT_ENV_FILE}" ] || die "deploy client env file not found: ${CLIENT_ENV_FILE}"
|
|
||||||
[ -f "${CLIENT_COMPOSE_FILE}" ] || die "client compose file not found: ${CLIENT_COMPOSE_FILE}"
|
|
||||||
|
|
||||||
stage "Reading Client Environment"
|
|
||||||
if [ -f "${COMMON_ENV_FILE}" ]; then
|
|
||||||
updated_count=0
|
|
||||||
while IFS= read -r line || [ -n "${line}" ]; do
|
|
||||||
case "${line}" in
|
|
||||||
''|\#*) continue ;;
|
|
||||||
*=*)
|
|
||||||
key="${line%%=*}"
|
|
||||||
value="${line#*=}"
|
|
||||||
set_env_var "${CLIENT_ENV_FILE}" "${key}" "${value}"
|
|
||||||
updated_count=$((updated_count + 1))
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done <"${COMMON_ENV_FILE}"
|
|
||||||
log "updated ${updated_count} client env variable(s)"
|
|
||||||
else
|
|
||||||
log "environment.client file not found, skip env update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Starting RTR Debug Client"
|
|
||||||
log "mode: ${MODE}"
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" up -d --no-build rtr-debug-client
|
|
||||||
|
|
||||||
log "checking client compose status"
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" ps rtr-debug-client
|
|
||||||
|
|
||||||
stage "Client Started"
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
||||||
|
|
||||||
CLIENT_ENV_FILE="${ROOT_DIR}/deploy/client/.env"
|
|
||||||
MODE="${RPKI_RTR_CLIENT_MODE:-tcp}"
|
|
||||||
COMPOSE_PROJECT="${RPKI_RTR_CLIENT_COMPOSE_PROJECT:-rpki-rtr-client}"
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat <<'EOF'
|
|
||||||
Usage: scripts/12_shutdown_client.sh [options]
|
|
||||||
|
|
||||||
Stop the RTR debug client started by scripts/11_start_client.sh.
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--mode MODE Client mode: tcp|tls|ssh. Default: tcp
|
|
||||||
-h, --help Show this help.
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
printf '[12_shutdown_client] %s\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
stage() {
|
|
||||||
printf '\n====== %s ======\n' "$*" >&2
|
|
||||||
}
|
|
||||||
|
|
||||||
die() {
|
|
||||||
printf '[12_shutdown_client] ERROR: %s\n' "$*" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
compose_file_for_mode() {
|
|
||||||
case "$1" in
|
|
||||||
tcp) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tcp.yml" ;;
|
|
||||||
tls) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.tls.yml" ;;
|
|
||||||
ssh) printf '%s\n' "${ROOT_DIR}/deploy/client/docker-compose.ssh.yml" ;;
|
|
||||||
*) die "unsupported mode '$1', expected tcp|tls|ssh" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
while [ "$#" -gt 0 ]; do
|
|
||||||
case "$1" in
|
|
||||||
--mode)
|
|
||||||
[ "$#" -ge 2 ] || die "--mode requires a value"
|
|
||||||
MODE="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
die "unknown option: $1"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
CLIENT_COMPOSE_FILE="$(compose_file_for_mode "${MODE}")"
|
|
||||||
|
|
||||||
stage "Checking Docker"
|
|
||||||
command -v docker >/dev/null 2>&1 || die "docker command not found"
|
|
||||||
if docker compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker compose)
|
|
||||||
elif command -v docker-compose >/dev/null 2>&1 && docker-compose version >/dev/null 2>&1; then
|
|
||||||
COMPOSE_BIN=(docker-compose)
|
|
||||||
else
|
|
||||||
die "docker compose is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Stopping RTR Debug Client"
|
|
||||||
log "mode: ${MODE}"
|
|
||||||
if [ -f "${CLIENT_ENV_FILE}" ] && [ -f "${CLIENT_COMPOSE_FILE}" ]; then
|
|
||||||
"${COMPOSE_BIN[@]}" --env-file "${CLIENT_ENV_FILE}" -p "${COMPOSE_PROJECT}" -f "${CLIENT_COMPOSE_FILE}" down --remove-orphans
|
|
||||||
else
|
|
||||||
log "client compose/env not found, skip"
|
|
||||||
fi
|
|
||||||
|
|
||||||
stage "Client Shutdown Finished"
|
|
||||||
@ -1,218 +0,0 @@
|
|||||||
# 脚本说明
|
|
||||||
|
|
||||||
`scripts/` 目录用于离线安装、启动、健康检查、关闭和清理。
|
|
||||||
|
|
||||||
所有脚本建议在仓库根目录执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/00_0_install.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 目录
|
|
||||||
|
|
||||||
- [推荐流程](#推荐流程)
|
|
||||||
- [环境变量文件](#环境变量文件)
|
|
||||||
- [00 安装](#00-安装)
|
|
||||||
- [00 Pre-Check](#00-pre-check)
|
|
||||||
- [01 启动 Server 和 Console](#01-启动-server-和-console)
|
|
||||||
- [02 自检](#02-自检)
|
|
||||||
- [03 关闭 Server 和 Console](#03-关闭-server-和-console)
|
|
||||||
- [04 清理](#04-清理)
|
|
||||||
- [10 生成证书](#10-生成证书)
|
|
||||||
- [11 启动 Client](#11-启动-client)
|
|
||||||
- [12 关闭 Client](#12-关闭-client)
|
|
||||||
- [Windows 路径注意事项](#windows-路径注意事项)
|
|
||||||
|
|
||||||
## 推荐流程
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 加载镜像并执行 pre-check。
|
|
||||||
bash scripts/00_0_install.sh
|
|
||||||
|
|
||||||
# 2. 启动真实 server 和 console。
|
|
||||||
bash scripts/01_start.sh --mode tcp
|
|
||||||
|
|
||||||
# 3. 检查 server 和 console 是否正常。
|
|
||||||
bash scripts/02_self_check.sh
|
|
||||||
|
|
||||||
# 4. 关闭 server 和 console。
|
|
||||||
bash scripts/03_shutdown.sh --mode tcp
|
|
||||||
|
|
||||||
# 5. 清理 server 运行产生的文件。
|
|
||||||
bash scripts/04_cleanup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
client 单独启动和关闭:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/11_start_client.sh --mode tcp
|
|
||||||
bash scripts/12_shutdown_client.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
支持的模式:
|
|
||||||
|
|
||||||
- server:`tcp`、`tls`、`ssh`、`base`
|
|
||||||
- client:`tcp`、`tls`、`ssh`
|
|
||||||
|
|
||||||
## 环境变量文件
|
|
||||||
|
|
||||||
`01_start.sh` 会读取根目录下的 `environment.server`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/server/.env`。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```env
|
|
||||||
RPKI_RTR_CCR_HOST_DIR=/opt/rpki-rtr/data
|
|
||||||
RPKI_RTR_SLURM_HOST_DIR=/opt/rpki-rtr/data
|
|
||||||
RPKI_RTR_DB_HOST_DIR=/opt/rpki-rtr/rtr-db
|
|
||||||
RPKI_RTR_LOG_HOST_DIR=/opt/rpki-rtr/logs/server
|
|
||||||
RPKI_RTR_REPORT_HOST_DIR=/opt/rpki-rtr/report
|
|
||||||
```
|
|
||||||
|
|
||||||
`11_start_client.sh` 会读取根目录下的 `environment.client`。如果文件存在,会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`。
|
|
||||||
|
|
||||||
如果 `environment.server` 或 `environment.client` 不存在,对应脚本会跳过更新,直接使用已有的 deploy `.env` 文件。
|
|
||||||
|
|
||||||
## 00 安装
|
|
||||||
|
|
||||||
`00_0_install.sh` 用于从 `images/` 目录加载离线 Docker 镜像,更新 deploy `.env` 中的镜像名,然后执行 pre-check。
|
|
||||||
|
|
||||||
默认镜像包查找规则:
|
|
||||||
|
|
||||||
- server:`images/rpki-rtr-*.tar`,排除 `rpki-rtr-debug-client-*.tar`
|
|
||||||
- client:`images/rpki-rtr-debug-client-*.tar`
|
|
||||||
- console:`images/rpki-console-ui-*.tar`
|
|
||||||
|
|
||||||
也可以显式指定镜像包:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/00_0_install.sh \
|
|
||||||
--server-tar images/rpki-rtr-amd64-v1.2.tar \
|
|
||||||
--client-tar images/rpki-rtr-debug-client-amd64-v1.2.tar \
|
|
||||||
--console-tar images/rpki-console-ui-amd64-v1.0.tar
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本会从 `docker load` 输出中解析真实镜像名,并写入:
|
|
||||||
|
|
||||||
- `deploy/server/.env` 的 `RPKI_RTR_SERVER_IMAGE`
|
|
||||||
- `deploy/console/.env` 的 `RPKI_CONSOLE_UI_IMAGE`
|
|
||||||
|
|
||||||
## 00 Pre-Check
|
|
||||||
|
|
||||||
`00_1_pre_check.sh` 会在 `.precheck/` 下准备隔离测试数据,使用仓库里的 Docker Compose 文件启动 server 和 debug client,然后检查:
|
|
||||||
|
|
||||||
- admin health 接口返回正常。
|
|
||||||
- server 写出了 source report。
|
|
||||||
- debug client 收到了 EndOfData 和 payload summary。
|
|
||||||
- pre-check 日志中没有 fatal 错误模式。
|
|
||||||
|
|
||||||
相关辅助脚本:
|
|
||||||
|
|
||||||
- `00_2_generate_ccr.sh`:生成固定的 CCR 测试文件。
|
|
||||||
- `00_2_pre_check_prepare.sh`:创建 `.precheck/` 数据、SLURM 文件和 env 文件。
|
|
||||||
- `00_3_pre_check_cleanup.sh`:停止 pre-check 容器并清理 `.precheck/`。
|
|
||||||
|
|
||||||
## 01 启动 Server 和 Console
|
|
||||||
|
|
||||||
`01_start.sh` 用于启动真实的 RTR server 和 console。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/01_start.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
启动后脚本会检查容器是否进入 `running` 状态。如果容器启动后退出,脚本会打印 compose 状态和最近日志,然后报错退出,不会继续打印 `Start Finished`。
|
|
||||||
|
|
||||||
常用参数:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/01_start.sh --mode tls
|
|
||||||
bash scripts/01_start.sh --mode ssh
|
|
||||||
bash scripts/01_start.sh --wait 60
|
|
||||||
bash scripts/01_start.sh --env ./environment.server
|
|
||||||
```
|
|
||||||
|
|
||||||
注意:console 不读取 `environment.server`,只使用 `deploy/console/.env`。
|
|
||||||
|
|
||||||
## 02 自检
|
|
||||||
|
|
||||||
`02_self_check.sh` 用于检查 `01_start.sh` 启动的 server 和 console 是否正常。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/02_self_check.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
检查内容包括:
|
|
||||||
|
|
||||||
- server 容器是否 running。
|
|
||||||
- console 容器是否 running。
|
|
||||||
- RTR TCP 端口是否可连接。
|
|
||||||
- admin health 接口是否健康。
|
|
||||||
- console 端口和 HTTP 根路径是否可访问。
|
|
||||||
|
|
||||||
如果启动较慢,可以增加等待时间:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/02_self_check.sh --wait 120
|
|
||||||
```
|
|
||||||
|
|
||||||
## 03 关闭 Server 和 Console
|
|
||||||
|
|
||||||
`03_shutdown.sh` 用于关闭 `01_start.sh` 启动的服务。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/03_shutdown.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
脚本会先关闭 console,再关闭 RTR server。
|
|
||||||
|
|
||||||
## 04 清理
|
|
||||||
|
|
||||||
`04_cleanup.sh` 用于删除 server 运行产生的目录:
|
|
||||||
|
|
||||||
- `logs/`
|
|
||||||
- `report/`
|
|
||||||
- `rtr-db/`
|
|
||||||
|
|
||||||
推荐先关闭服务,再清理:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/03_shutdown.sh
|
|
||||||
bash scripts/04_cleanup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
清理前脚本会检查 `rpki-rtr` 是否仍在运行。如果仍在运行,会拒绝清理。只有明确需要强制清理时才使用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/04_cleanup.sh --force
|
|
||||||
```
|
|
||||||
|
|
||||||
清理脚本会拒绝删除仓库根目录之外的路径。如果某一项清理失败,会继续清理后续项,最后返回失败。
|
|
||||||
|
|
||||||
## 10 生成证书
|
|
||||||
|
|
||||||
`10_generate_certs.sh` 用于生成 TLS/SSH 模式需要的本地证书材料(测试用)。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/10_generate_certs.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## 11 启动 Client
|
|
||||||
|
|
||||||
`11_start_client.sh` 用于启动 RTR debug client。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/11_start_client.sh --mode tcp
|
|
||||||
bash scripts/11_start_client.sh --mode tls
|
|
||||||
bash scripts/11_start_client.sh --mode ssh
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 `environment.client` 存在,脚本会将其中所有 `KEY=VALUE` 同步到 `deploy/client/.env`;如果不存在,则直接使用已有的 `deploy/client/.env`。
|
|
||||||
|
|
||||||
## 12 关闭 Client
|
|
||||||
|
|
||||||
`12_shutdown_client.sh` 用于关闭 `11_start_client.sh` 启动的 client。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bash scripts/12_shutdown_client.sh --mode tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
关闭时建议使用和启动时相同的 mode。
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
# 10. SLURM(Simplified Local Internet Number Resource Management with the RPKI)
|
|
||||||
|
|
||||||
## 10.1 目标与范围
|
|
||||||
|
|
||||||
SLURM 用于让 RP(Relying Party)在本地对上游 RPKI 验证结果做“过滤”和“补充断言”,而不修改上游发布对象。
|
|
||||||
|
|
||||||
本文档基于:
|
|
||||||
- RFC 8416(SLURM v1,ROA/BGPsec)
|
|
||||||
- draft-ietf-sidrops-aspa-slurm-04(SLURM v2,新增 ASPA)
|
|
||||||
|
|
||||||
## 10.2 版本与顶层结构
|
|
||||||
|
|
||||||
### 10.2.1 SLURM v1(RFC 8416)
|
|
||||||
|
|
||||||
`slurmVersion` 必须为 `1`,且顶层 JSON 对象必须包含且仅包含以下成员:
|
|
||||||
- `slurmVersion`
|
|
||||||
- `validationOutputFilters`(必须包含 `prefixFilters`、`bgpsecFilters`)
|
|
||||||
- `locallyAddedAssertions`(必须包含 `prefixAssertions`、`bgpsecAssertions`)
|
|
||||||
|
|
||||||
空策略示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 1,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 10.2.2 SLURM v2(draft-04)
|
|
||||||
|
|
||||||
`slurmVersion` 必须为 `2`,在 v1 基础上扩展 ASPA 两类成员:
|
|
||||||
- `validationOutputFilters.aspaFilters`
|
|
||||||
- `locallyAddedAssertions.aspaAssertions`
|
|
||||||
|
|
||||||
空策略示例:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": []
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10.3 字段规范(RFC 8416)
|
|
||||||
|
|
||||||
### 10.3.1 `prefixFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须至少包含一个:`prefix` 或 `asn`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 若配置了 `prefix`:匹配“被该前缀覆盖(encompassed)”的 VRP 前缀
|
|
||||||
- 若配置了 `asn`:匹配该 ASN
|
|
||||||
- 同时配置时:两者都要匹配
|
|
||||||
|
|
||||||
### 10.3.2 `bgpsecFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须至少包含一个:`asn` 或 `SKI`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 按 `asn`/`SKI` 单独或联合匹配 Router Key(BGPsec)
|
|
||||||
|
|
||||||
### 10.3.3 `prefixAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`prefix`、`asn`
|
|
||||||
- 可选:`maxPrefixLength`、`comment`
|
|
||||||
|
|
||||||
约束:
|
|
||||||
- 若给出 `maxPrefixLength`,应满足 `prefix 长度 <= maxPrefixLength <= 地址位宽(IPv4=32, IPv6=128)`
|
|
||||||
|
|
||||||
### 10.3.4 `bgpsecAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`asn`、`SKI`、`routerPublicKey`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
## 10.4 ASPA 扩展(draft-ietf-sidrops-aspa-slurm-04)
|
|
||||||
|
|
||||||
### 10.4.1 `aspaFilters`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`customerAsn`
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
匹配规则:
|
|
||||||
- 当 VAP(Validated ASPA Payload)的 `customerAsn` 等于过滤器 `customerAsn` 时命中并移除。
|
|
||||||
|
|
||||||
### 10.4.2 `aspaAssertions`
|
|
||||||
|
|
||||||
数组元素每项:
|
|
||||||
- 必须:`customerAsn`
|
|
||||||
- 必须:`providerAsns`(ASN 数组)
|
|
||||||
- 可选:`comment`
|
|
||||||
|
|
||||||
关键约束(draft-04):
|
|
||||||
- `customerAsn` 不得出现在 `providerAsns` 中
|
|
||||||
- `providerAsns` 必须按升序排列
|
|
||||||
- `providerAsns` 里的 ASN 必须唯一(无重复)
|
|
||||||
|
|
||||||
语义补充(draft-04):
|
|
||||||
- `aspaAssertions` 仅用于“新增断言”,不构成隐式过滤(不会自动替代 `aspaFilters`)。
|
|
||||||
- 在 RTRv2 输出阶段,新增的 ASPA 断言应加入 ASPA PDU 集合,并做去重。
|
|
||||||
|
|
||||||
## 10.5 应用语义(RFC 8416 Section 4)
|
|
||||||
|
|
||||||
### 10.5.1 原子性
|
|
||||||
|
|
||||||
SLURM 应用必须是原子的:
|
|
||||||
- 要么完全不生效(等同未使用 SLURM)
|
|
||||||
- 要么完整按当前 SLURM 配置生效
|
|
||||||
|
|
||||||
### 10.5.2 处理顺序
|
|
||||||
|
|
||||||
在同一次计算中:
|
|
||||||
1. 先执行 `validationOutputFilters`(移除匹配验证结果)
|
|
||||||
2. 再追加 `locallyAddedAssertions`
|
|
||||||
|
|
||||||
### 10.5.3 多文件
|
|
||||||
|
|
||||||
实现可以支持多个 SLURM 文件并行使用(取并集),但在启用前应检查断言重叠冲突;若存在冲突,整组文件应被拒绝。
|
|
||||||
|
|
||||||
## 10.6 最小可用示例(SLURM v2)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"slurmVersion": 2,
|
|
||||||
"validationOutputFilters": {
|
|
||||||
"prefixFilters": [
|
|
||||||
{
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"comment": "Filter a broken VRP from upstream"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecFilters": [],
|
|
||||||
"aspaFilters": [
|
|
||||||
{
|
|
||||||
"customerAsn": 64496,
|
|
||||||
"comment": "Filter one customer ASPA"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locallyAddedAssertions": {
|
|
||||||
"prefixAssertions": [
|
|
||||||
{
|
|
||||||
"asn": 64496,
|
|
||||||
"prefix": "203.0.113.0/24",
|
|
||||||
"maxPrefixLength": 24,
|
|
||||||
"comment": "Local business exception"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgpsecAssertions": [],
|
|
||||||
"aspaAssertions": [
|
|
||||||
{
|
|
||||||
"customerAsn": 64496,
|
|
||||||
"providerAsns": [64497, 64498],
|
|
||||||
"comment": "Local ASPA assertion"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 10.7 参考文献
|
|
||||||
|
|
||||||
- RFC 8416: https://www.rfc-editor.org/rfc/rfc8416.html
|
|
||||||
- draft-ietf-sidrops-aspa-slurm-04: https://www.ietf.org/archive/id/draft-ietf-sidrops-aspa-slurm-04.html
|
|
||||||
179
specs/11_rtr.md
179
specs/11_rtr.md
@ -1,179 +0,0 @@
|
|||||||
# 11. RTR(RPKI to Router Protocol)
|
|
||||||
|
|
||||||
## 11.1 目标与文档范围
|
|
||||||
|
|
||||||
RTR 用于把 RP/Cache 已完成密码学验证的 RPKI 数据下发给路由器。
|
|
||||||
|
|
||||||
本文按以下规范整理:
|
|
||||||
- RFC 6810(RTR v0)
|
|
||||||
- RFC 8210(RTR v1,更新 RFC 6810)
|
|
||||||
- draft-ietf-sidrops-8210bis-26(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-26) |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 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.5.3 Cache Restart / Cache Shutdown(v2)
|
|
||||||
|
|
||||||
- `Cache Restart`:Cache 即将对已连接客户端不可用,但预计会在客户端达到 Expire Interval 前恢复;v2 session 应发送 `Error Report` code 12,随后关闭连接。
|
|
||||||
- `Cache Shutdown`:Cache 即将有意下线,并希望客户端 flush 从该 cache 学到的数据;v2 session 应发送 `Error Report` code 13,随后关闭连接。
|
|
||||||
- 这两个错误码只属于 v2。v0/v1 session 不得发送 code 12/13,只能关闭连接。
|
|
||||||
|
|
||||||
## 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.8.3 Error Code 版本范围
|
|
||||||
|
|
||||||
- v0(RFC6810):0..7。
|
|
||||||
- v1(RFC8210):继承 v0,并新增 8 `Unexpected Protocol Version`。
|
|
||||||
- v2(8210bis-26):继承 v1,并新增 9 `ASPA Provider List Error`、10 `Transport Failure`、11 `Ordering Error`、12 `Cache Restart`、13 `Cache Shutdown`。
|
|
||||||
- 实现发送 Error Report 时,只能发送当前 session 协议版本定义过的错误码。
|
|
||||||
|
|
||||||
### 11.8.4 本实现的生命周期控制
|
|
||||||
|
|
||||||
- `RPKI_RTR_STOP_REASON=cache-restart|cache-shutdown|close-only` 控制进程收到 Ctrl-C 后的服务停止语义,默认 `cache-restart`。
|
|
||||||
- Admin API:
|
|
||||||
- `POST /admin/rtr/lifecycle/restart`:向 v2 established clients 发送 `Cache Restart`,然后走进程退出路径;实际重新拉起由 systemd/Docker/Kubernetes/Windows Service 等 supervisor 完成。若没有外部机制在客户端 `Expire Interval` 到期前恢复服务,不应使用该接口。
|
|
||||||
- `POST /admin/rtr/lifecycle/shutdown`:向 v2 established clients 发送 `Cache Shutdown`,然后退出进程。
|
|
||||||
- v0/v1 或尚未完成版本协商的连接不会收到 code 12/13。
|
|
||||||
|
|
||||||
## 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-26: https://www.ietf.org/archive/id/draft-ietf-sidrops-8210bis-26.html
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@ -1,290 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
const CONTENT_TYPE_OID: &str = "1.2.840.113549.1.9.16.1.54";
|
|
||||||
const SHA256_OID: &str = "2.16.840.1.101.3.4.2.1";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Vrp {
|
|
||||||
addr: IpAddr,
|
|
||||||
prefix_len: u8,
|
|
||||||
max_len: u8,
|
|
||||||
asn: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct Vap {
|
|
||||||
customer_asn: u32,
|
|
||||||
providers: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let out_dir = parse_out_dir_arg();
|
|
||||||
fs::create_dir_all(&out_dir)
|
|
||||||
.with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000001Z-mini-a.ccr"),
|
|
||||||
"20260403000001Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65001,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65002,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65011, 65012],
|
|
||||||
}],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000101Z-mini-b.ccr"),
|
|
||||||
"20260403000101Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65001,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65003,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65002,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65011, 65012, 65013],
|
|
||||||
}],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
write_snapshot(
|
|
||||||
&out_dir.join("20260403T000201Z-mini-c.ccr"),
|
|
||||||
"20260403000201Z",
|
|
||||||
vec![
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
|
|
||||||
prefix_len: 24,
|
|
||||||
max_len: 24,
|
|
||||||
asn: 65003,
|
|
||||||
},
|
|
||||||
Vrp {
|
|
||||||
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 0)),
|
|
||||||
prefix_len: 48,
|
|
||||||
max_len: 48,
|
|
||||||
asn: 65004,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
vec![
|
|
||||||
Vap {
|
|
||||||
customer_asn: 65010,
|
|
||||||
providers: vec![65012, 65013],
|
|
||||||
},
|
|
||||||
Vap {
|
|
||||||
customer_asn: 65020,
|
|
||||||
providers: vec![65021],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
println!("generated CCR fixtures under {}", out_dir.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_out_dir_arg() -> PathBuf {
|
|
||||||
let mut args = std::env::args().skip(1);
|
|
||||||
let mut out_dir = PathBuf::from("data");
|
|
||||||
while let Some(arg) = args.next() {
|
|
||||||
if arg == "--out-dir" {
|
|
||||||
if let Some(v) = args.next() {
|
|
||||||
out_dir = PathBuf::from(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out_dir
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_snapshot(path: &Path, produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Result<()> {
|
|
||||||
let bytes = encode_ccr_snapshot(produced_at, vrps, vaps);
|
|
||||||
fs::write(path, bytes).with_context(|| format!("failed to write {}", path.display()))?;
|
|
||||||
println!("wrote {}", path.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_ccr_snapshot(produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Vec<u8> {
|
|
||||||
let vrp_sets = vrps
|
|
||||||
.into_iter()
|
|
||||||
.map(encode_roa_payload_set)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let vap_sets = vaps
|
|
||||||
.into_iter()
|
|
||||||
.map(encode_aspa_payload_set)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let vrp_set_seq = der_sequence(vrp_sets);
|
|
||||||
let vap_set_seq = der_sequence(vap_sets);
|
|
||||||
|
|
||||||
let vrp_hash = Sha256::digest(&vrp_set_seq).to_vec();
|
|
||||||
let vap_hash = Sha256::digest(&vap_set_seq).to_vec();
|
|
||||||
|
|
||||||
// draft-ietf-sidrops-rpki-ccr-02:
|
|
||||||
// ROAPayloadState/ASPAPayloadState include payload-set sequence + hash.
|
|
||||||
let vrp_state = der_sequence(vec![vrp_set_seq, der_octet_string(vrp_hash)]);
|
|
||||||
let vap_state = der_sequence(vec![vap_set_seq, der_octet_string(vap_hash)]);
|
|
||||||
|
|
||||||
// AlgorithmIdentifier for SHA-256.
|
|
||||||
let hash_alg = der_sequence(vec![der_oid(SHA256_OID), der_null()]);
|
|
||||||
|
|
||||||
let payload = der_sequence(vec![
|
|
||||||
der_integer(0),
|
|
||||||
hash_alg,
|
|
||||||
der_generalized_time(produced_at),
|
|
||||||
der_ctx(2, vrp_state),
|
|
||||||
der_ctx(3, vap_state),
|
|
||||||
]);
|
|
||||||
|
|
||||||
der_sequence(vec![der_oid(CONTENT_TYPE_OID), der_ctx(0, payload)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_roa_payload_set(v: Vrp) -> Vec<u8> {
|
|
||||||
let (afi, addr_bytes) = match v.addr {
|
|
||||||
IpAddr::V4(ip) => ([0u8, 1u8].to_vec(), ip.octets().to_vec()),
|
|
||||||
IpAddr::V6(ip) => ([0u8, 2u8].to_vec(), ip.octets().to_vec()),
|
|
||||||
};
|
|
||||||
let bit_string = prefix_to_bit_string(&addr_bytes, v.prefix_len);
|
|
||||||
let roa_ip = der_sequence(vec![
|
|
||||||
der_bit_string(0, bit_string),
|
|
||||||
der_integer(u32::from(v.max_len)),
|
|
||||||
]);
|
|
||||||
let family = der_sequence(vec![der_octet_string(afi), der_sequence(vec![roa_ip])]);
|
|
||||||
der_sequence(vec![der_integer(v.asn), der_sequence(vec![family])])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode_aspa_payload_set(v: Vap) -> Vec<u8> {
|
|
||||||
let providers = v.providers.into_iter().map(der_integer).collect::<Vec<_>>();
|
|
||||||
der_sequence(vec![der_integer(v.customer_asn), der_sequence(providers)])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prefix_to_bit_string(addr: &[u8], prefix_len: u8) -> Vec<u8> {
|
|
||||||
let byte_len = usize::from(prefix_len).div_ceil(8);
|
|
||||||
let mut out = addr[..byte_len].to_vec();
|
|
||||||
let rem = prefix_len % 8;
|
|
||||||
if rem != 0 {
|
|
||||||
let mask = 0xFFu8 << (8 - rem);
|
|
||||||
let last = out.len() - 1;
|
|
||||||
out[last] &= mask;
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_sequence(items: Vec<Vec<u8>>) -> Vec<u8> {
|
|
||||||
let content = items.concat();
|
|
||||||
der_tlv(0x30, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_integer(v: u32) -> Vec<u8> {
|
|
||||||
if v == 0 {
|
|
||||||
return der_tlv(0x02, vec![0]);
|
|
||||||
}
|
|
||||||
let mut bytes = v.to_be_bytes().to_vec();
|
|
||||||
while bytes.len() > 1 && bytes[0] == 0 {
|
|
||||||
bytes.remove(0);
|
|
||||||
}
|
|
||||||
if bytes[0] & 0x80 != 0 {
|
|
||||||
bytes.insert(0, 0);
|
|
||||||
}
|
|
||||||
der_tlv(0x02, bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_oid(oid: &str) -> Vec<u8> {
|
|
||||||
let parts = oid
|
|
||||||
.split('.')
|
|
||||||
.map(|s| s.parse::<u32>().unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
assert!(parts.len() >= 2);
|
|
||||||
let mut out = Vec::new();
|
|
||||||
out.push((parts[0] * 40 + parts[1]) as u8);
|
|
||||||
for &part in &parts[2..] {
|
|
||||||
out.extend(base128(part));
|
|
||||||
}
|
|
||||||
der_tlv(0x06, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base128(mut n: u32) -> Vec<u8> {
|
|
||||||
let mut buf = vec![(n & 0x7F) as u8];
|
|
||||||
n >>= 7;
|
|
||||||
while n > 0 {
|
|
||||||
buf.push(((n & 0x7F) as u8) | 0x80);
|
|
||||||
n >>= 7;
|
|
||||||
}
|
|
||||||
buf.reverse();
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_octet_string(bytes: Vec<u8>) -> Vec<u8> {
|
|
||||||
der_tlv(0x04, bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_null() -> Vec<u8> {
|
|
||||||
der_tlv(0x05, Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_bit_string(unused_bits: u8, bytes: Vec<u8>) -> Vec<u8> {
|
|
||||||
let mut content = Vec::with_capacity(1 + bytes.len());
|
|
||||||
content.push(unused_bits);
|
|
||||||
content.extend(bytes);
|
|
||||||
der_tlv(0x03, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_generalized_time(v: &str) -> Vec<u8> {
|
|
||||||
der_tlv(0x18, v.as_bytes().to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_ctx(tag_no: u8, encoded_inner_der: Vec<u8>) -> Vec<u8> {
|
|
||||||
der_tlv(0xA0 + tag_no, encoded_inner_der)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_tlv(tag: u8, content: Vec<u8>) -> Vec<u8> {
|
|
||||||
let mut out = Vec::with_capacity(2 + content.len());
|
|
||||||
out.push(tag);
|
|
||||||
out.extend(der_len(content.len()));
|
|
||||||
out.extend(content);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn der_len(len: usize) -> Vec<u8> {
|
|
||||||
if len < 128 {
|
|
||||||
return vec![len as u8];
|
|
||||||
}
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
let mut n = len;
|
|
||||||
while n > 0 {
|
|
||||||
bytes.push((n & 0xFF) as u8);
|
|
||||||
n >>= 8;
|
|
||||||
}
|
|
||||||
bytes.reverse();
|
|
||||||
let mut out = vec![0x80 | (bytes.len() as u8)];
|
|
||||||
out.extend(bytes);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
# rtr_debug_client
|
|
||||||
|
|
||||||
`rtr_debug_client` 是用于 RTR 协议联调的命令行调试客户端,支持 `TCP`、`TLS`、`SSH` 三种传输。
|
|
||||||
|
|
||||||
它用于:
|
|
||||||
- 手动发送 `Reset Query`、`Serial Query`
|
|
||||||
- 持续接收并打印服务端 PDU
|
|
||||||
- 观察 `session_id`、`serial`、`EndOfData` timing hint、`ErrorReport` 等状态变化
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo build --bin rtr_debug_client
|
|
||||||
```
|
|
||||||
|
|
||||||
## 基本用法
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
|
||||||
```
|
|
||||||
|
|
||||||
默认值:
|
|
||||||
- `addr`: `127.0.0.1:323`
|
|
||||||
- `version`: `1`
|
|
||||||
- `mode`: `reset`
|
|
||||||
- `--timeout`: `30`
|
|
||||||
- `--poll`: `600`
|
|
||||||
|
|
||||||
## TCP 用法
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
|
||||||
```
|
|
||||||
|
|
||||||
## TLS 用法
|
|
||||||
|
|
||||||
仅校验服务端证书:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:324 1 reset \
|
|
||||||
--tls \
|
|
||||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
|
||||||
--server-name localhost
|
|
||||||
```
|
|
||||||
|
|
||||||
双向 TLS:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:324 1 reset \
|
|
||||||
--tls \
|
|
||||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
|
||||||
--server-name localhost \
|
|
||||||
--client-cert tests/fixtures/tls/client-good.crt \
|
|
||||||
--client-key tests/fixtures/tls/client-good.key
|
|
||||||
```
|
|
||||||
|
|
||||||
## SSH 用法(按 draft-ietf-sidrops-8210bis-25)
|
|
||||||
|
|
||||||
`rtr_debug_client --ssh` 采用以下流程:
|
|
||||||
- SSHv2 连接
|
|
||||||
- `session` channel
|
|
||||||
- 请求 `subsystem`,默认 `rpki-rtr`
|
|
||||||
- 使用 `publickey` 认证
|
|
||||||
- 强制服务端 host key 校验(`known_hosts` 或 pinned server key 二选一)
|
|
||||||
|
|
||||||
### 1. 使用 known_hosts 校验服务端
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-known-hosts certs/known_hosts
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 使用固定服务端公钥校验
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-server-key certs/ssh_host_ed25519_key.pub
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 自定义 subsystem 名称
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-key certs/rtr-client.key \
|
|
||||||
--ssh-known-hosts certs/known_hosts \
|
|
||||||
--ssh-subsystem rpki-rtr
|
|
||||||
```
|
|
||||||
|
|
||||||
## 参数说明
|
|
||||||
|
|
||||||
通用参数:
|
|
||||||
- `--timeout <secs>`:读取 PDU 超时时间(秒)
|
|
||||||
- `--poll <secs>`:默认自动轮询间隔(秒)
|
|
||||||
- `--keep-after-error`:兼容旧脚本的保留参数;默认已按 RFC/draft 处理 `ErrorReport`
|
|
||||||
- `--summary-only`:仅打印摘要,抑制 payload PDU 详细内容
|
|
||||||
|
|
||||||
TLS 参数:
|
|
||||||
- `--tls`
|
|
||||||
- `--ca-cert <path>`
|
|
||||||
- `--server-name <name>`
|
|
||||||
- `--client-cert <path>`
|
|
||||||
- `--client-key <path>`
|
|
||||||
|
|
||||||
SSH 参数:
|
|
||||||
- `--ssh`
|
|
||||||
- `--ssh-user <name>`
|
|
||||||
- `--ssh-key <path>`(OpenSSH 私钥)
|
|
||||||
- `--ssh-subsystem <name>`(默认 `rpki-rtr`)
|
|
||||||
- `--ssh-known-hosts <path>` 或 `--ssh-server-key <path>`(二选一,必须提供)
|
|
||||||
|
|
||||||
## SSH 连通性测试建议
|
|
||||||
|
|
||||||
如果你已经在 Docker 中启动了支持 SSH 的 RTR server,可按以下方式验证:
|
|
||||||
|
|
||||||
1. 先用 `ssh` 命令确认认证与 host key 配置正确(可连通)。
|
|
||||||
2. 再用 `rtr_debug_client --ssh` 发起连接并发送 `reset`。
|
|
||||||
3. 观察是否收到 `Cache Response` 和 `EndOfData`。
|
|
||||||
|
|
||||||
如果 `rtr_debug_client` 报 `failed to request SSH subsystem 'rpki-rtr'`,通常表示服务端未开启对应 subsystem 名称,或名称不一致。
|
|
||||||
|
|
||||||
## 运行时交互命令
|
|
||||||
|
|
||||||
客户端启动后可在标准输入中使用:
|
|
||||||
- `help`
|
|
||||||
- `state`
|
|
||||||
- `version` / `version <n>`
|
|
||||||
- `reset`
|
|
||||||
- `serial` / `serial <sid> <serial>`
|
|
||||||
- `timeout` / `timeout <secs>`
|
|
||||||
- `poll` / `poll <secs>` / `poll pause` / `poll resume`
|
|
||||||
- `output` / `output verbose` / `output summary`
|
|
||||||
- `quit`
|
|
||||||
|
|
||||||
## SSH Password Auth (Added)
|
|
||||||
|
|
||||||
`rtr_debug_client` now supports password auth in SSH mode.
|
|
||||||
|
|
||||||
- Use exactly one auth option in SSH mode:
|
|
||||||
- `--ssh-key <path>`
|
|
||||||
- `--ssh-password <value>`
|
|
||||||
- Host key verification is still required:
|
|
||||||
- `--ssh-known-hosts <path>` or `--ssh-server-key <path>`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- \
|
|
||||||
127.0.0.1:22 1 reset \
|
|
||||||
--ssh \
|
|
||||||
--ssh-user rpki-rtr \
|
|
||||||
--ssh-password 'your-password' \
|
|
||||||
--ssh-server-key certs/ssh_host_rsa_key.pub
|
|
||||||
```
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,404 +0,0 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
|
|
||||||
use crate::protocol::{
|
|
||||||
ASPA_FIXED_BODY_LEN, END_OF_DATA_V0_BODY_LEN, END_OF_DATA_V1_BODY_LEN, IPV4_PREFIX_BODY_LEN,
|
|
||||||
IPV6_PREFIX_BODY_LEN, PduHeader, PduType, ROUTER_KEY_FIXED_BODY_LEN, flag_meaning, hex_bytes,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn print_pdu(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!("--------------------------------------------------");
|
|
||||||
println!("PDU: {}", header.pdu_type());
|
|
||||||
println!("version : {}", header.version);
|
|
||||||
println!("length : {}", header.length);
|
|
||||||
|
|
||||||
match header.pdu_type() {
|
|
||||||
PduType::CacheResponse => {
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
}
|
|
||||||
PduType::CacheReset => {
|
|
||||||
println!("cache reset");
|
|
||||||
}
|
|
||||||
PduType::Ipv4Prefix => {
|
|
||||||
print_ipv4_prefix(header, body);
|
|
||||||
}
|
|
||||||
PduType::Ipv6Prefix => {
|
|
||||||
print_ipv6_prefix(header, body);
|
|
||||||
}
|
|
||||||
PduType::RouterKey => {
|
|
||||||
print_router_key(header, body);
|
|
||||||
}
|
|
||||||
PduType::EndOfData => {
|
|
||||||
print_end_of_data(header, body);
|
|
||||||
}
|
|
||||||
PduType::ErrorReport => {
|
|
||||||
print_error_report(header, body);
|
|
||||||
}
|
|
||||||
PduType::SerialNotify => {
|
|
||||||
print_serial_notify(header, body);
|
|
||||||
}
|
|
||||||
PduType::SerialQuery => {
|
|
||||||
print_serial_query(header, body);
|
|
||||||
}
|
|
||||||
PduType::Aspa => {
|
|
||||||
print_aspa(header, body);
|
|
||||||
}
|
|
||||||
PduType::ResetQuery => {
|
|
||||||
println!("reset query");
|
|
||||||
}
|
|
||||||
PduType::Unknown(_) => {
|
|
||||||
println!("field1 : {}", header.field1);
|
|
||||||
println!("body : {}", hex_bytes(body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_ipv4_prefix(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != IPV4_PREFIX_BODY_LEN {
|
|
||||||
println!("invalid IPv4 Prefix body length: {}", body.len());
|
|
||||||
println!("raw body: {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = body[0];
|
|
||||||
let prefix_len = body[1];
|
|
||||||
let max_len = body[2];
|
|
||||||
let zero = body[3];
|
|
||||||
let prefix = Ipv4Addr::new(body[4], body[5], body[6], body[7]);
|
|
||||||
let asn = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("prefix_len : {}", prefix_len);
|
|
||||||
println!("max_len : {}", max_len);
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("prefix : {}", prefix);
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_ipv6_prefix(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != IPV6_PREFIX_BODY_LEN {
|
|
||||||
println!("invalid IPv6 Prefix body length: {}", body.len());
|
|
||||||
println!("raw body: {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = body[0];
|
|
||||||
let prefix_len = body[1];
|
|
||||||
let max_len = body[2];
|
|
||||||
let zero = body[3];
|
|
||||||
|
|
||||||
let mut addr = [0u8; 16];
|
|
||||||
addr.copy_from_slice(&body[4..20]);
|
|
||||||
let prefix = Ipv6Addr::from(addr);
|
|
||||||
|
|
||||||
let asn = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
|
||||||
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("prefix_len : {}", prefix_len);
|
|
||||||
println!("max_len : {}", max_len);
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("prefix : {}", prefix);
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_end_of_data(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
|
|
||||||
match body.len() {
|
|
||||||
END_OF_DATA_V0_BODY_LEN => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
println!("variant : v0");
|
|
||||||
}
|
|
||||||
END_OF_DATA_V1_BODY_LEN => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
|
||||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
println!("refresh : {}", refresh);
|
|
||||||
println!("retry : {}", retry);
|
|
||||||
println!("expire : {}", expire);
|
|
||||||
println!("variant : v1/v2");
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("invalid EndOfData body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_error_report(header: &PduHeader, body: &[u8]) {
|
|
||||||
println!(
|
|
||||||
"error_code : {} ({})",
|
|
||||||
header.error_code(),
|
|
||||||
error_code_name(header.error_code())
|
|
||||||
);
|
|
||||||
|
|
||||||
if body.len() < 8 {
|
|
||||||
println!("invalid ErrorReport body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encapsulated_len = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
|
|
||||||
|
|
||||||
if body.len() < 4 + encapsulated_len + 4 {
|
|
||||||
println!("invalid ErrorReport: truncated encapsulated PDU");
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let encapsulated = &body[4..4 + encapsulated_len];
|
|
||||||
|
|
||||||
let text_len_offset = 4 + encapsulated_len;
|
|
||||||
let text_len = u32::from_be_bytes([
|
|
||||||
body[text_len_offset],
|
|
||||||
body[text_len_offset + 1],
|
|
||||||
body[text_len_offset + 2],
|
|
||||||
body[text_len_offset + 3],
|
|
||||||
]) as usize;
|
|
||||||
|
|
||||||
if body.len() < text_len_offset + 4 + text_len {
|
|
||||||
println!("invalid ErrorReport: truncated text");
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text_bytes = &body[text_len_offset + 4..text_len_offset + 4 + text_len];
|
|
||||||
let text = String::from_utf8_lossy(text_bytes);
|
|
||||||
|
|
||||||
println!("encap_len : {}", encapsulated_len);
|
|
||||||
if let Some(encap_header) = parse_encapsulated_header(encapsulated) {
|
|
||||||
println!("encap_pdu_type : {}", encap_header.pdu_type());
|
|
||||||
println!("encap_version : {}", encap_header.version);
|
|
||||||
println!("encap_length : {}", encap_header.length);
|
|
||||||
match encap_header.pdu_type() {
|
|
||||||
PduType::ErrorReport => {
|
|
||||||
println!("encap_field1 : error_code={}", encap_header.error_code());
|
|
||||||
}
|
|
||||||
PduType::Unknown(_) => {
|
|
||||||
println!("encap_field1 : {}", encap_header.field1);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
println!("encap_field1 : session_id={}", encap_header.session_id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if encapsulated_len > 0 {
|
|
||||||
println!("encap_header : <truncated or unavailable>");
|
|
||||||
}
|
|
||||||
println!("encap_pdu : {}", hex_bytes(encapsulated));
|
|
||||||
println!("text_len : {}", text_len);
|
|
||||||
println!("text_utf8 : {}", std::str::from_utf8(text_bytes).is_ok());
|
|
||||||
println!("text : {}", text);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_serial_notify(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != 4 {
|
|
||||||
println!("invalid Serial Notify body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_serial_query(header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() != 4 {
|
|
||||||
println!("invalid Serial Query body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
println!("session_id : {}", header.session_id());
|
|
||||||
println!("serial : {}", serial);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_router_key(_header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() < ROUTER_KEY_FIXED_BODY_LEN {
|
|
||||||
println!("invalid Router Key body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = _header.flags();
|
|
||||||
let zero = _header.zero();
|
|
||||||
let ski = &body[0..20];
|
|
||||||
let asn = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
|
||||||
let spki = &body[24..];
|
|
||||||
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("zero : {}", zero);
|
|
||||||
println!("ski : {}", hex_bytes(ski));
|
|
||||||
println!("asn : {}", asn);
|
|
||||||
println!("spki_len : {}", spki.len());
|
|
||||||
println!("spki : {}", hex_bytes(spki));
|
|
||||||
|
|
||||||
if flags & !0x01 != 0 {
|
|
||||||
println!("warning : Router Key flags use reserved bits");
|
|
||||||
}
|
|
||||||
if zero != 0 {
|
|
||||||
println!("warning : Router Key reserved zero octet is non-zero");
|
|
||||||
}
|
|
||||||
if asn == 0 {
|
|
||||||
println!("warning : Router Key ASN is AS0");
|
|
||||||
}
|
|
||||||
if spki.is_empty() {
|
|
||||||
println!("warning : Router Key SPKI is empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print_raw_pdu(header: &PduHeader, body: &[u8]) {
|
|
||||||
let mut raw = Vec::with_capacity(8 + body.len());
|
|
||||||
raw.push(header.version);
|
|
||||||
raw.push(header.pdu_type_raw);
|
|
||||||
raw.extend_from_slice(&header.field1.to_be_bytes());
|
|
||||||
raw.extend_from_slice(&header.length.to_be_bytes());
|
|
||||||
raw.extend_from_slice(body);
|
|
||||||
|
|
||||||
println!("--------------------------------------------------");
|
|
||||||
println!("[raw] pdu_type : {}", header.pdu_type());
|
|
||||||
println!("[raw] octets : {}", hex_bytes(&raw));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn error_code_name(code: u16) -> &'static str {
|
|
||||||
match code {
|
|
||||||
0 => "Corrupt Data",
|
|
||||||
1 => "Internal Error",
|
|
||||||
2 => "No Data Available",
|
|
||||||
3 => "Invalid Request",
|
|
||||||
4 => "Unsupported Protocol Version",
|
|
||||||
5 => "Unsupported PDU Type",
|
|
||||||
6 => "Withdrawal of Unknown Record",
|
|
||||||
7 => "Duplicate Announcement Received",
|
|
||||||
8 => "Unexpected Protocol Version",
|
|
||||||
9 => "ASPA Provider List Error",
|
|
||||||
10 => "Transport Failure",
|
|
||||||
11 => "Ordering Error",
|
|
||||||
12 => "Cache Restart",
|
|
||||||
13 => "Cache Shutdown",
|
|
||||||
_ => "Unknown Error Code",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_encapsulated_header(encapsulated: &[u8]) -> Option<PduHeader> {
|
|
||||||
if encapsulated.len() < 8 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut header = [0u8; 8];
|
|
||||||
header.copy_from_slice(&encapsulated[..8]);
|
|
||||||
Some(PduHeader::from_bytes(header))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_aspa(_header: &PduHeader, body: &[u8]) {
|
|
||||||
if body.len() < ASPA_FIXED_BODY_LEN {
|
|
||||||
println!("invalid ASPA body length: {}", body.len());
|
|
||||||
println!("raw body : {}", hex_bytes(body));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let flags = _header.flags();
|
|
||||||
let zero = _header.zero();
|
|
||||||
let customer_asn = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
|
|
||||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
|
||||||
println!("reserved : {}", zero);
|
|
||||||
println!("customer_as : {}", customer_asn);
|
|
||||||
|
|
||||||
let providers_raw = &body[4..];
|
|
||||||
if providers_raw.len() % 4 != 0 {
|
|
||||||
println!("invalid ASPA providers length: {}", providers_raw.len());
|
|
||||||
println!("providers : {}", hex_bytes(providers_raw));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut providers = Vec::new();
|
|
||||||
for chunk in providers_raw.chunks_exact(4) {
|
|
||||||
providers.push(u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("providers : {:?}", providers);
|
|
||||||
|
|
||||||
if flags & !0x01 != 0 {
|
|
||||||
println!("warning : ASPA flags use reserved bits");
|
|
||||||
}
|
|
||||||
if zero != 0 {
|
|
||||||
println!("warning : ASPA reserved zero octet is non-zero");
|
|
||||||
}
|
|
||||||
if customer_asn == 0 {
|
|
||||||
println!("warning : ASPA customer ASN is AS0");
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_announcement = flags & 0x01 == 0x01;
|
|
||||||
if is_announcement {
|
|
||||||
if providers.is_empty() {
|
|
||||||
println!("warning : ASPA announcement has empty provider list");
|
|
||||||
}
|
|
||||||
} else if !providers.is_empty() {
|
|
||||||
println!("warning : ASPA withdrawal must not contain providers");
|
|
||||||
}
|
|
||||||
|
|
||||||
if providers.iter().any(|asn| *asn == 0) {
|
|
||||||
println!("warning : ASPA provider list contains AS0");
|
|
||||||
}
|
|
||||||
|
|
||||||
let strictly_increasing = providers.windows(2).all(|pair| pair[0] < pair[1]);
|
|
||||||
if !strictly_increasing && providers.len() > 1 {
|
|
||||||
println!("warning : ASPA providers are not in strictly increasing order");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_serial_notify_serial(body: &[u8]) -> Option<u32> {
|
|
||||||
if body.len() != 4 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]]))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_end_of_data_serial(body: &[u8]) -> Option<u32> {
|
|
||||||
match body.len() {
|
|
||||||
4 | 16 => Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]])),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct EndOfDataInfo {
|
|
||||||
pub serial: u32,
|
|
||||||
pub refresh: Option<u32>,
|
|
||||||
pub retry: Option<u32>,
|
|
||||||
pub expire: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_end_of_data_info(body: &[u8]) -> Option<EndOfDataInfo> {
|
|
||||||
match body.len() {
|
|
||||||
4 => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
Some(EndOfDataInfo {
|
|
||||||
serial,
|
|
||||||
refresh: None,
|
|
||||||
retry: None,
|
|
||||||
expire: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
16 => {
|
|
||||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
|
||||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
|
||||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
|
||||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
|
||||||
Some(EndOfDataInfo {
|
|
||||||
serial,
|
|
||||||
refresh: Some(refresh),
|
|
||||||
retry: Some(retry),
|
|
||||||
expire: Some(expire),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
pub const HEADER_LEN: usize = 8;
|
|
||||||
pub const SERIAL_QUERY_LEN: usize = 12;
|
|
||||||
pub const MAX_PDU_LEN: u32 = 65_535;
|
|
||||||
|
|
||||||
pub const IPV4_PREFIX_BODY_LEN: usize = 12;
|
|
||||||
pub const IPV6_PREFIX_BODY_LEN: usize = 24;
|
|
||||||
pub const END_OF_DATA_V0_BODY_LEN: usize = 4;
|
|
||||||
pub const END_OF_DATA_V1_BODY_LEN: usize = 16;
|
|
||||||
pub const ROUTER_KEY_FIXED_BODY_LEN: usize = 24;
|
|
||||||
pub const ASPA_FIXED_BODY_LEN: usize = 4;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum QueryMode {
|
|
||||||
Reset,
|
|
||||||
Serial { session_id: u16, serial: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum PduType {
|
|
||||||
SerialNotify,
|
|
||||||
SerialQuery,
|
|
||||||
ResetQuery,
|
|
||||||
CacheResponse,
|
|
||||||
Ipv4Prefix,
|
|
||||||
Ipv6Prefix,
|
|
||||||
RouterKey,
|
|
||||||
EndOfData,
|
|
||||||
CacheReset,
|
|
||||||
ErrorReport,
|
|
||||||
Aspa,
|
|
||||||
Unknown(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PduType {
|
|
||||||
pub fn code(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
Self::SerialNotify => 0,
|
|
||||||
Self::SerialQuery => 1,
|
|
||||||
Self::ResetQuery => 2,
|
|
||||||
Self::CacheResponse => 3,
|
|
||||||
Self::Ipv4Prefix => 4,
|
|
||||||
Self::Ipv6Prefix => 6,
|
|
||||||
Self::RouterKey => 9,
|
|
||||||
Self::EndOfData => 7,
|
|
||||||
Self::CacheReset => 8,
|
|
||||||
Self::ErrorReport => 10,
|
|
||||||
Self::Aspa => 11,
|
|
||||||
Self::Unknown(v) => v,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::SerialNotify => "Serial Notify",
|
|
||||||
Self::SerialQuery => "Serial Query",
|
|
||||||
Self::ResetQuery => "Reset Query",
|
|
||||||
Self::CacheResponse => "Cache Response",
|
|
||||||
Self::Ipv4Prefix => "IPv4 Prefix",
|
|
||||||
Self::Ipv6Prefix => "IPv6 Prefix",
|
|
||||||
Self::RouterKey => "Router Key",
|
|
||||||
Self::EndOfData => "End of Data",
|
|
||||||
Self::CacheReset => "Cache Reset",
|
|
||||||
Self::ErrorReport => "Error Report",
|
|
||||||
Self::Aspa => "ASPA",
|
|
||||||
Self::Unknown(_) => "Unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for PduType {
|
|
||||||
fn from(value: u8) -> Self {
|
|
||||||
match value {
|
|
||||||
0 => Self::SerialNotify,
|
|
||||||
1 => Self::SerialQuery,
|
|
||||||
2 => Self::ResetQuery,
|
|
||||||
3 => Self::CacheResponse,
|
|
||||||
4 => Self::Ipv4Prefix,
|
|
||||||
6 => Self::Ipv6Prefix,
|
|
||||||
9 => Self::RouterKey,
|
|
||||||
7 => Self::EndOfData,
|
|
||||||
8 => Self::CacheReset,
|
|
||||||
10 => Self::ErrorReport,
|
|
||||||
11 => Self::Aspa,
|
|
||||||
x => Self::Unknown(x),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PduType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Unknown(v) => write!(f, "{} ({})", self.name(), v),
|
|
||||||
_ => write!(f, "{} ({})", self.name(), self.code()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PduHeader {
|
|
||||||
pub version: u8,
|
|
||||||
pub pdu_type_raw: u8,
|
|
||||||
pub field1: u16,
|
|
||||||
pub length: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PduHeader {
|
|
||||||
pub fn from_bytes(buf: [u8; HEADER_LEN]) -> Self {
|
|
||||||
Self {
|
|
||||||
version: buf[0],
|
|
||||||
pdu_type_raw: buf[1],
|
|
||||||
field1: u16::from_be_bytes([buf[2], buf[3]]),
|
|
||||||
length: u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pdu_type(&self) -> PduType {
|
|
||||||
self.pdu_type_raw.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_id(&self) -> u16 {
|
|
||||||
self.field1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error_code(&self) -> u16 {
|
|
||||||
self.field1
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flags(&self) -> u8 {
|
|
||||||
self.field1.to_be_bytes()[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn zero(&self) -> u8 {
|
|
||||||
self.field1.to_be_bytes()[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RawPdu {
|
|
||||||
pub header: PduHeader,
|
|
||||||
pub body: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn flag_meaning(flags: u8) -> &'static str {
|
|
||||||
if flags & 0x01 == 0x01 {
|
|
||||||
"announcement"
|
|
||||||
} else {
|
|
||||||
"withdrawal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hex_bytes(data: &[u8]) -> String {
|
|
||||||
if data.is_empty() {
|
|
||||||
return "<empty>".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut out = String::with_capacity(data.len() * 3 - 1);
|
|
||||||
for (idx, b) in data.iter().enumerate() {
|
|
||||||
if idx > 0 {
|
|
||||||
out.push(' ');
|
|
||||||
}
|
|
||||||
use std::fmt::Write as _;
|
|
||||||
let _ = write!(out, "{:02x}", b);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
|
||||||
|
|
||||||
use crate::protocol::{HEADER_LEN, MAX_PDU_LEN, PduHeader, PduType, RawPdu, SERIAL_QUERY_LEN};
|
|
||||||
|
|
||||||
pub async fn send_reset_query<S>(stream: &mut S, version: u8) -> io::Result<()>
|
|
||||||
where
|
|
||||||
S: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; HEADER_LEN];
|
|
||||||
buf[0] = version;
|
|
||||||
buf[1] = PduType::ResetQuery.code();
|
|
||||||
buf[2..4].copy_from_slice(&0u16.to_be_bytes());
|
|
||||||
buf[4..8].copy_from_slice(&(HEADER_LEN as u32).to_be_bytes());
|
|
||||||
stream.write_all(&buf).await?;
|
|
||||||
stream.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_serial_query<S>(
|
|
||||||
stream: &mut S,
|
|
||||||
version: u8,
|
|
||||||
session_id: u16,
|
|
||||||
serial: u32,
|
|
||||||
) -> io::Result<()>
|
|
||||||
where
|
|
||||||
S: AsyncWrite + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; SERIAL_QUERY_LEN];
|
|
||||||
buf[0] = version;
|
|
||||||
buf[1] = PduType::SerialQuery.code();
|
|
||||||
buf[2..4].copy_from_slice(&session_id.to_be_bytes());
|
|
||||||
buf[4..8].copy_from_slice(&(SERIAL_QUERY_LEN as u32).to_be_bytes());
|
|
||||||
buf[8..12].copy_from_slice(&serial.to_be_bytes());
|
|
||||||
stream.write_all(&buf).await?;
|
|
||||||
stream.flush().await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_header<S>(stream: &mut S) -> io::Result<PduHeader>
|
|
||||||
where
|
|
||||||
S: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
let mut buf = [0u8; HEADER_LEN];
|
|
||||||
stream.read_exact(&mut buf).await?;
|
|
||||||
Ok(PduHeader::from_bytes(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_pdu<S>(stream: &mut S) -> io::Result<RawPdu>
|
|
||||||
where
|
|
||||||
S: AsyncRead + Unpin,
|
|
||||||
{
|
|
||||||
let header = read_header(stream).await?;
|
|
||||||
|
|
||||||
if header.length < HEADER_LEN as u32 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!("invalid PDU length {} < {}", header.length, HEADER_LEN),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if header.length > MAX_PDU_LEN {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"PDU length {} exceeds max allowed {}",
|
|
||||||
header.length, MAX_PDU_LEN
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_len = header.length as usize - HEADER_LEN;
|
|
||||||
let mut body = vec![0u8; body_len];
|
|
||||||
stream.read_exact(&mut body).await?;
|
|
||||||
|
|
||||||
Ok(RawPdu { header, body })
|
|
||||||
}
|
|
||||||
@ -1,163 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use rpki::rtr::payload::Payload;
|
|
||||||
use rpki::slurm::file::SlurmFile;
|
|
||||||
use rpki::source::ccr::{
|
|
||||||
find_latest_ccr_file, load_ccr_snapshot_from_file, snapshot_to_payloads_with_options,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Cli {
|
|
||||||
ccr_path: PathBuf,
|
|
||||||
slurm_path: PathBuf,
|
|
||||||
dump_payloads: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct Output {
|
|
||||||
ccr_path: String,
|
|
||||||
slurm_path: String,
|
|
||||||
produced_at: Option<String>,
|
|
||||||
slurm_version: u32,
|
|
||||||
input_payload_count: usize,
|
|
||||||
input_vrp_count: usize,
|
|
||||||
input_vap_count: usize,
|
|
||||||
output_payload_count: usize,
|
|
||||||
output_vrp_count: usize,
|
|
||||||
output_vap_count: usize,
|
|
||||||
invalid_vrps: Vec<String>,
|
|
||||||
invalid_vaps: Vec<String>,
|
|
||||||
sample_output_aspa_customers: Vec<u32>,
|
|
||||||
payloads: Option<Vec<Payload>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let cli = parse_args(env::args().skip(1))?;
|
|
||||||
|
|
||||||
let snapshot = load_ccr_snapshot_from_file(&cli.ccr_path)
|
|
||||||
.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, 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);
|
|
||||||
|
|
||||||
let output = Output {
|
|
||||||
ccr_path: cli.ccr_path.display().to_string(),
|
|
||||||
slurm_path: cli.slurm_path.display().to_string(),
|
|
||||||
produced_at: snapshot.produced_at.clone(),
|
|
||||||
slurm_version: slurm.version().as_u32(),
|
|
||||||
input_payload_count: conversion.payloads.len(),
|
|
||||||
input_vrp_count,
|
|
||||||
input_vap_count,
|
|
||||||
output_payload_count: payloads.len(),
|
|
||||||
output_vrp_count,
|
|
||||||
output_vap_count,
|
|
||||||
invalid_vrps: conversion.invalid_vrps,
|
|
||||||
invalid_vaps: conversion.invalid_vaps,
|
|
||||||
sample_output_aspa_customers: sample_aspa_customers(&payloads, 8),
|
|
||||||
payloads: cli.dump_payloads.then_some(payloads),
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("{}", serde_json::to_string_pretty(&output)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_slurm(path: &PathBuf) -> Result<SlurmFile> {
|
|
||||||
let file = File::open(path)
|
|
||||||
.with_context(|| format!("failed to open SLURM file: {}", path.display()))?;
|
|
||||||
SlurmFile::from_reader(file)
|
|
||||||
.with_context(|| format!("failed to parse SLURM file: {}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
|
|
||||||
let mut dump_payloads = false;
|
|
||||||
let mut positionals = Vec::new();
|
|
||||||
|
|
||||||
for arg in args {
|
|
||||||
match arg.as_str() {
|
|
||||||
"--dump-payloads" => dump_payloads = true,
|
|
||||||
"-h" | "--help" => {
|
|
||||||
print_help();
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
_ if arg.starts_with('-') => {
|
|
||||||
return Err(anyhow!("unknown option: {}", arg));
|
|
||||||
}
|
|
||||||
_ => positionals.push(arg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if positionals.is_empty() {
|
|
||||||
return Ok(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"),
|
|
||||||
dump_payloads,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if positionals.len() != 2 {
|
|
||||||
print_help();
|
|
||||||
return Err(anyhow!(
|
|
||||||
"expected: slurm_apply_client <snapshot.ccr> <policy.slurm>"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Cli {
|
|
||||||
ccr_path: PathBuf::from(&positionals[0]),
|
|
||||||
slurm_path: PathBuf::from(&positionals[1]),
|
|
||||||
dump_payloads,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_help() {
|
|
||||||
eprintln!(
|
|
||||||
"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.");
|
|
||||||
eprintln!(
|
|
||||||
"When no arguments are provided, it defaults to the latest .ccr under ./data and ./data/example.slurm."
|
|
||||||
);
|
|
||||||
eprintln!("Use --dump-payloads to include the full payload list in the JSON output.");
|
|
||||||
}
|
|
||||||
|
|
||||||
fn count_vrps_and_vaps(payloads: &[Payload]) -> (usize, usize) {
|
|
||||||
let mut vrps = 0;
|
|
||||||
let mut vaps = 0;
|
|
||||||
|
|
||||||
for payload in payloads {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(_) => vrps += 1,
|
|
||||||
Payload::Aspa(_) => vaps += 1,
|
|
||||||
Payload::RouterKey(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(vrps, vaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sample_aspa_customers(payloads: &[Payload], limit: usize) -> Vec<u32> {
|
|
||||||
let mut customers = Vec::new();
|
|
||||||
|
|
||||||
for payload in payloads {
|
|
||||||
if let Payload::Aspa(aspa) = payload {
|
|
||||||
let customer = aspa.customer_asn().into_u32();
|
|
||||||
if !customers.contains(&customer) {
|
|
||||||
customers.push(customer);
|
|
||||||
if customers.len() == limit {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customers
|
|
||||||
}
|
|
||||||
@ -2,5 +2,5 @@ pub mod crl;
|
|||||||
mod rc;
|
mod rc;
|
||||||
mod tal;
|
mod tal;
|
||||||
mod ta;
|
mod ta;
|
||||||
pub mod resources;
|
mod resources;
|
||||||
mod oids;
|
mod oids;
|
||||||
|
|||||||
1278
src/data_model/rc.rs
1278
src/data_model/rc.rs
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ASIdentifiers {
|
pub struct ASIdentifiers {
|
||||||
@ -55,7 +54,7 @@ impl ASRange {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Default, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
pub struct Asn(u32);
|
pub struct Asn(u32);
|
||||||
|
|
||||||
impl Asn {
|
impl Asn {
|
||||||
|
|||||||
@ -1,157 +1,47 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct IPAddrBlocks {
|
pub struct IPAddrBlocks {
|
||||||
pub ips: Vec<IPAddressFamily>,
|
pub ips: Vec<IPAddressFamily>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// IP Address Family
|
// IP Address Family
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct IPAddressFamily {
|
pub struct IPAddressFamily {
|
||||||
pub address_family: Afi,
|
pub address_family: Afi,
|
||||||
pub ip_address_choice: IPAddressChoice,
|
pub ip_address_choice: IPAddressChoice,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Afi {
|
pub enum Afi {
|
||||||
Ipv4,
|
Ipv4,
|
||||||
Ipv6,
|
Ipv6,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum IPAddressChoice {
|
pub enum IPAddressChoice {
|
||||||
Inherit,
|
Inherit,
|
||||||
AddressOrRange(Vec<IPAddressOrRange>),
|
AddressOrRange(Vec<IPAddressOrRange>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum IPAddressOrRange {
|
pub enum IPAddressOrRange {
|
||||||
AddressPrefix(IPAddressPrefix),
|
AddressPrefix(IPAddressPrefix),
|
||||||
AddressRange(IPAddressRange),
|
AddressRange(IPAddressRange),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct IPAddressPrefix {
|
pub struct IPAddressPrefix {
|
||||||
pub address: IPAddress,
|
pub address: IPAddress,
|
||||||
pub prefix_length: u8,
|
pub prefix_length: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IPAddressPrefix {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub fn new(address: IPAddress, prefix_length: u8) -> Self {
|
|
||||||
Self {
|
|
||||||
address,
|
|
||||||
prefix_length,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv4(&self) -> bool {
|
|
||||||
self.address.is_ipv4()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv6(&self) -> bool {
|
|
||||||
self.address.is_ipv6()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn afi(&self) -> Afi {
|
|
||||||
self.address.afi()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> IPAddress {
|
|
||||||
self.address
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prefix_length(&self) -> u8 {
|
|
||||||
self.prefix_length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct IPAddressRange {
|
pub struct IPAddressRange {
|
||||||
pub min: IPAddress,
|
pub min: IPAddress,
|
||||||
pub max: IPAddress,
|
pub max: IPAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IPAddressRange {
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
pub fn new(min: IPAddress, max: IPAddress) -> Self {
|
pub struct IPAddress(u128);
|
||||||
Self { min, max }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn min(&self) -> IPAddress {
|
|
||||||
self.min
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max(&self) -> IPAddress {
|
|
||||||
self.max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub enum IPAddress {
|
|
||||||
V4(Ipv4Addr),
|
|
||||||
V6(Ipv6Addr),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IPAddress {
|
|
||||||
pub fn from_ipv4(addr: Ipv4Addr) -> Self {
|
|
||||||
Self::V4(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_ipv6(addr: Ipv6Addr) -> Self {
|
|
||||||
Self::V6(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ipv4(self) -> Option<Ipv4Addr> {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => Some(addr),
|
|
||||||
Self::V6(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ipv6(self) -> Option<Ipv6Addr> {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => None,
|
|
||||||
Self::V6(addr) => Some(addr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv4(self) -> bool {
|
|
||||||
matches!(self, Self::V4(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_ipv6(self) -> bool {
|
|
||||||
matches!(self, Self::V6(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn afi(self) -> Afi {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => Afi::Ipv4,
|
|
||||||
Self::V6(_) => Afi::Ipv6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the numeric address value.
|
|
||||||
///
|
|
||||||
/// For IPv4, this is the 32-bit address widened to u128.
|
|
||||||
/// For IPv6, this is the full 128-bit address.
|
|
||||||
pub fn as_u128(self) -> u128 {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => u32::from(addr) as u128,
|
|
||||||
Self::V6(addr) => u128::from(addr),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_v4_u32(self) -> Option<u32> {
|
|
||||||
match self {
|
|
||||||
Self::V4(addr) => Some(u32::from(addr)),
|
|
||||||
Self::V6(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_v6_u128(self) -> Option<u128> {
|
|
||||||
match self {
|
|
||||||
Self::V4(_) => None,
|
|
||||||
Self::V6(addr) => Some(u128::from(addr)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
pub mod ip_resources;
|
pub(crate) mod ip_resources;
|
||||||
pub mod as_resources;
|
pub(crate) mod as_resources;
|
||||||
pub mod resource;
|
pub mod resource;
|
||||||
|
|||||||
@ -1,4 +1 @@
|
|||||||
pub mod data_model;
|
pub mod data_model;
|
||||||
pub mod slurm;
|
|
||||||
pub mod rtr;
|
|
||||||
pub mod source;
|
|
||||||
|
|||||||
133
src/main_rtr.rs
133
src/main_rtr.rs
@ -1,133 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use chrono::Utc;
|
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
use rpki::rtr::admin::{
|
|
||||||
AdminState, LogTailConfig, RuntimeConfigHandle, SourceReloadHandle, spawn_admin_config_server,
|
|
||||||
};
|
|
||||||
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, RtrShutdownReason};
|
|
||||||
use rpki::slurm::admin::SlurmAdmin;
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
let config = AppConfig::from_env()?;
|
|
||||||
init_tracing(config.timezone);
|
|
||||||
log_startup_config(&config);
|
|
||||||
|
|
||||||
let report_context = ReportContext::new(ReportConfiguration::new(
|
|
||||||
config.source_refresh_interval.as_secs(),
|
|
||||||
config.runtime_report_interval.as_secs(),
|
|
||||||
config.report_history_limit,
|
|
||||||
config.max_delta,
|
|
||||||
config.prune_delta_by_snapshot_size,
|
|
||||||
config.timezone,
|
|
||||||
(
|
|
||||||
config.timing.refresh,
|
|
||||||
config.timing.retry,
|
|
||||||
config.timing.expire,
|
|
||||||
),
|
|
||||||
));
|
|
||||||
let store = open_store(&config)?;
|
|
||||||
let shared_cache = init_shared_cache(&config, &store, &report_context)?;
|
|
||||||
let runtime_config = RuntimeConfigHandle::new(config.runtime_config());
|
|
||||||
|
|
||||||
let service = RtrService::with_config(shared_cache.clone(), config.service_config.clone());
|
|
||||||
let notifier = service.notifier();
|
|
||||||
let service_stats = service.stats();
|
|
||||||
let shutdown_handle = service.shutdown_handle();
|
|
||||||
let (source_reload_tx, source_reload_rx) = mpsc::channel(8);
|
|
||||||
let source_reload = SourceReloadHandle::new(source_reload_tx);
|
|
||||||
let (process_shutdown_tx, process_shutdown_rx) = mpsc::channel(1);
|
|
||||||
|
|
||||||
let admin_task = config.admin_addr.map(|addr| {
|
|
||||||
let slurm_admin = config.slurm_dir.as_ref().map(SlurmAdmin::new);
|
|
||||||
let admin_state = AdminState::new(
|
|
||||||
runtime_config.clone(),
|
|
||||||
Some(source_reload.clone()),
|
|
||||||
Some(shutdown_handle.clone()),
|
|
||||||
Some(process_shutdown_tx.clone()),
|
|
||||||
slurm_admin,
|
|
||||||
LogTailConfig::from_env(),
|
|
||||||
);
|
|
||||||
spawn_admin_config_server(addr, config.admin_token.clone(), admin_state)
|
|
||||||
});
|
|
||||||
let running = start_servers(&config, &service);
|
|
||||||
let refresh_task = spawn_refresh_task(
|
|
||||||
&config,
|
|
||||||
runtime_config,
|
|
||||||
source_reload_rx,
|
|
||||||
shared_cache.clone(),
|
|
||||||
store.clone(),
|
|
||||||
notifier,
|
|
||||||
service_stats,
|
|
||||||
report_context,
|
|
||||||
);
|
|
||||||
|
|
||||||
let stop_reason = wait_for_shutdown(process_shutdown_rx).await?;
|
|
||||||
info!("stopping RTR service: reason={}", stop_reason.as_str());
|
|
||||||
running.shutdown(stop_reason);
|
|
||||||
running.wait().await;
|
|
||||||
|
|
||||||
refresh_task.abort();
|
|
||||||
let _ = refresh_task.await;
|
|
||||||
if let Some(admin_task) = admin_task {
|
|
||||||
admin_task.abort();
|
|
||||||
let _ = admin_task.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("RTR service stopped");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn wait_for_shutdown(
|
|
||||||
mut process_shutdown_rx: mpsc::Receiver<RtrShutdownReason>,
|
|
||||||
) -> Result<RtrShutdownReason> {
|
|
||||||
tokio::select! {
|
|
||||||
signal = tokio::signal::ctrl_c() => {
|
|
||||||
signal?;
|
|
||||||
let reason = RtrShutdownReason::from_env();
|
|
||||||
info!("shutdown signal received: reason={}", reason.as_str());
|
|
||||||
Ok(reason)
|
|
||||||
}
|
|
||||||
reason = process_shutdown_rx.recv() => {
|
|
||||||
let reason = reason.unwrap_or_else(RtrShutdownReason::from_env);
|
|
||||||
info!("admin process lifecycle request received: reason={}", reason.as_str());
|
|
||||||
Ok(reason)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_tracing(timezone: chrono_tz::Tz) {
|
|
||||||
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
|
||||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
|
|
||||||
|
|
||||||
struct LocalTimer {
|
|
||||||
timezone: chrono_tz::Tz,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl tracing_subscriber::fmt::time::FormatTime for LocalTimer {
|
|
||||||
fn format_time(
|
|
||||||
&self,
|
|
||||||
w: &mut tracing_subscriber::fmt::format::Writer<'_>,
|
|
||||||
) -> std::fmt::Result {
|
|
||||||
let now = Utc::now().with_timezone(&self.timezone);
|
|
||||||
write!(w, "{}", now.format("%Y-%m-%d %H:%M:%S%.3f %:z"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = tracing_subscriber::fmt()
|
|
||||||
.with_timer(LocalTimer { timezone })
|
|
||||||
.with_env_filter(filter)
|
|
||||||
.with_target(true)
|
|
||||||
.with_thread_ids(true)
|
|
||||||
.with_level(true)
|
|
||||||
.try_init()
|
|
||||||
{
|
|
||||||
eprintln!("failed to initialize tracing subscriber: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
920
src/rtr/admin.rs
920
src/rtr/admin.rs
@ -1,920 +0,0 @@
|
|||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio::sync::{mpsc, oneshot, watch};
|
|
||||||
use tokio::time::sleep;
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
use crate::rtr::config::{RuntimeConfig, RuntimeConfigPatch};
|
|
||||||
use crate::rtr::server::{RtrShutdownHandle, RtrShutdownReason};
|
|
||||||
use crate::slurm::admin::{
|
|
||||||
SlurmAdmin, SlurmFileActionRequest, SlurmFileOperationResult, SlurmFileWriteRequest,
|
|
||||||
parse_reload_query,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RuntimeConfigHandle {
|
|
||||||
current: Arc<RwLock<RuntimeConfig>>,
|
|
||||||
tx: watch::Sender<RuntimeConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SourceReloadHandle {
|
|
||||||
tx: mpsc::Sender<SourceReloadCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceReloadHandle {
|
|
||||||
pub fn new(tx: mpsc::Sender<SourceReloadCommand>) -> Self {
|
|
||||||
Self { tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reload(&self, phase: &'static str, force: bool) -> Result<SourceReloadResult> {
|
|
||||||
let (respond_to, response) = oneshot::channel();
|
|
||||||
self.tx
|
|
||||||
.send(SourceReloadCommand {
|
|
||||||
phase,
|
|
||||||
force,
|
|
||||||
respond_to,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("source reload task is not available"))?;
|
|
||||||
response
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow!("source reload task dropped the response"))?
|
|
||||||
.map_err(anyhow::Error::msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SourceReloadCommand {
|
|
||||||
pub phase: &'static str,
|
|
||||||
pub force: bool,
|
|
||||||
pub respond_to: oneshot::Sender<Result<SourceReloadResult, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct SourceReloadResult {
|
|
||||||
pub phase: &'static str,
|
|
||||||
pub changed: bool,
|
|
||||||
pub skipped_unchanged: bool,
|
|
||||||
pub payload_count: Option<usize>,
|
|
||||||
pub serials: [u32; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct AdminState {
|
|
||||||
runtime_config: RuntimeConfigHandle,
|
|
||||||
source_reload: Option<SourceReloadHandle>,
|
|
||||||
shutdown: Option<RtrShutdownHandle>,
|
|
||||||
process_shutdown: Option<mpsc::Sender<RtrShutdownReason>>,
|
|
||||||
slurm_admin: Option<SlurmAdmin>,
|
|
||||||
log_tail: LogTailConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AdminState {
|
|
||||||
pub fn new(
|
|
||||||
runtime_config: RuntimeConfigHandle,
|
|
||||||
source_reload: Option<SourceReloadHandle>,
|
|
||||||
shutdown: Option<RtrShutdownHandle>,
|
|
||||||
process_shutdown: Option<mpsc::Sender<RtrShutdownReason>>,
|
|
||||||
slurm_admin: Option<SlurmAdmin>,
|
|
||||||
log_tail: LogTailConfig,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
runtime_config,
|
|
||||||
source_reload,
|
|
||||||
shutdown,
|
|
||||||
process_shutdown,
|
|
||||||
slurm_admin,
|
|
||||||
log_tail,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct LogTailConfig {
|
|
||||||
dir: PathBuf,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogTailConfig {
|
|
||||||
pub fn from_env() -> Self {
|
|
||||||
let dir = std::env::var_os("RPKI_RTR_LOG_DIR")
|
|
||||||
.map(PathBuf::from)
|
|
||||||
.unwrap_or_else(|| PathBuf::from("/app/logs"));
|
|
||||||
let name = std::env::var("RPKI_RTR_LOG_NAME")
|
|
||||||
.or_else(|_| std::env::var("HOSTNAME"))
|
|
||||||
.unwrap_or_else(|_| "rpki-rtr".to_string());
|
|
||||||
Self { dir, name }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path_for(&self, stream: LogStream) -> PathBuf {
|
|
||||||
self.dir
|
|
||||||
.join(format!("{}.{}.log", self.name, stream.as_str()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuntimeConfigHandle {
|
|
||||||
pub fn new(config: RuntimeConfig) -> Self {
|
|
||||||
let (tx, _) = watch::channel(config.clone());
|
|
||||||
Self {
|
|
||||||
current: Arc::new(RwLock::new(config)),
|
|
||||||
tx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current(&self) -> RuntimeConfig {
|
|
||||||
self.current
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe(&self) -> watch::Receiver<RuntimeConfig> {
|
|
||||||
self.tx.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_patch(&self, patch: RuntimeConfigPatch) -> Result<RuntimeConfig> {
|
|
||||||
let next = self.current().apply_patch(patch)?;
|
|
||||||
{
|
|
||||||
let mut current = self
|
|
||||||
.current
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
*current = next.clone();
|
|
||||||
}
|
|
||||||
let _ = self.tx.send_replace(next.clone());
|
|
||||||
Ok(next)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_admin_config_server(
|
|
||||||
addr: SocketAddr,
|
|
||||||
token: Option<String>,
|
|
||||||
state: AdminState,
|
|
||||||
) -> tokio::task::JoinHandle<()> {
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = run_admin_config_server(addr, token, state).await {
|
|
||||||
warn!("RTR admin config server exited: {:?}", err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_admin_config_server(
|
|
||||||
addr: SocketAddr,
|
|
||||||
token: Option<String>,
|
|
||||||
state: AdminState,
|
|
||||||
) -> Result<()> {
|
|
||||||
if token.is_none() && !addr.ip().is_loopback() {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"RPKI_RTR_ADMIN_TOKEN is required when admin addr is not loopback: {}",
|
|
||||||
addr
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let listener = TcpListener::bind(addr)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("bind RTR admin config server on {addr}"))?;
|
|
||||||
info!("RTR admin config server listening on {}", addr);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (stream, peer_addr) = listener.accept().await?;
|
|
||||||
let token = token.clone();
|
|
||||||
let state = state.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = handle_admin_connection(stream, peer_addr, token, state).await {
|
|
||||||
warn!(
|
|
||||||
"RTR admin config request failed from {}: {:?}",
|
|
||||||
peer_addr, err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_admin_connection(
|
|
||||||
mut stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
token: Option<String>,
|
|
||||||
state: AdminState,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut buffer = vec![0u8; 64 * 1024];
|
|
||||||
let mut read = 0usize;
|
|
||||||
let header_end = loop {
|
|
||||||
if read == buffer.len() {
|
|
||||||
write_response(&mut stream, 413, "payload too large", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let n = stream.read(&mut buffer[read..]).await?;
|
|
||||||
if n == 0 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
read += n;
|
|
||||||
if let Some(pos) = find_header_end(&buffer[..read]) {
|
|
||||||
break pos;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = std::str::from_utf8(&buffer[..header_end])
|
|
||||||
.map_err(|err| anyhow!("invalid HTTP header from {}: {}", peer_addr, err))?;
|
|
||||||
let request = parse_request_header(header)?;
|
|
||||||
|
|
||||||
if let Some(token) = token.as_deref() {
|
|
||||||
let expected = format!("Bearer {token}");
|
|
||||||
if request.authorization.as_deref() != Some(expected.as_str()) {
|
|
||||||
write_response(&mut stream, 401, "unauthorized", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let content_length = request.content_length.unwrap_or(0);
|
|
||||||
let max_body_bytes = if request.path.starts_with("/admin/rtr/slurm/") {
|
|
||||||
state
|
|
||||||
.slurm_admin
|
|
||||||
.as_ref()
|
|
||||||
.map(SlurmAdmin::max_body_bytes)
|
|
||||||
.unwrap_or(32 * 1024)
|
|
||||||
} else {
|
|
||||||
32 * 1024
|
|
||||||
};
|
|
||||||
if content_length > max_body_bytes {
|
|
||||||
write_response(&mut stream, 413, "payload too large", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let body_start = header_end + 4;
|
|
||||||
let available_body = read.saturating_sub(body_start);
|
|
||||||
let mut body = Vec::with_capacity(content_length);
|
|
||||||
body.extend_from_slice(&buffer[body_start..read]);
|
|
||||||
if available_body < content_length {
|
|
||||||
let remaining = content_length - available_body;
|
|
||||||
let mut tail = vec![0u8; remaining];
|
|
||||||
stream.read_exact(&mut tail).await?;
|
|
||||||
body.extend_from_slice(&tail);
|
|
||||||
}
|
|
||||||
body.truncate(content_length);
|
|
||||||
|
|
||||||
route_admin_request(&mut stream, request, body, state, peer_addr).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn route_admin_request(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
request: RequestHeader,
|
|
||||||
body: Vec<u8>,
|
|
||||||
state: AdminState,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
) -> Result<()> {
|
|
||||||
if request.method == "POST" && request.path == "/admin/rtr/config" {
|
|
||||||
let patch = match serde_json::from_slice::<RuntimeConfigPatch>(&body) {
|
|
||||||
Ok(patch) => patch,
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid json: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match state.runtime_config.apply_patch(patch) {
|
|
||||||
Ok(config) => {
|
|
||||||
info!("RTR admin config updated from {}: {:?}", peer_addr, config);
|
|
||||||
let json = serde_json::to_string_pretty(&AdminConfigResponse {
|
|
||||||
status: "ok",
|
|
||||||
config,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid config: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "GET" && request.path == "/admin/rtr/health" {
|
|
||||||
let json = serde_json::to_string_pretty(&AdminHealthResponse {
|
|
||||||
status: "ok",
|
|
||||||
config_api: true,
|
|
||||||
source_reload_api: state.source_reload.is_some(),
|
|
||||||
lifecycle_api: state.shutdown.is_some(),
|
|
||||||
process_lifecycle_api: state.process_shutdown.is_some(),
|
|
||||||
slurm_api: state.slurm_admin.is_some(),
|
|
||||||
logs_api: true,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "POST" && request.path == "/admin/rtr/lifecycle/restart" {
|
|
||||||
return trigger_process_lifecycle(stream, &state, RtrShutdownReason::CacheRestart).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "POST" && request.path == "/admin/rtr/lifecycle/shutdown" {
|
|
||||||
return trigger_process_lifecycle(stream, &state, RtrShutdownReason::CacheShutdown).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "GET" && request.path == "/admin/rtr/logs/tail" {
|
|
||||||
return tail_log_stream(stream, request.query.as_deref(), &state.log_tail).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "GET" && request.path == "/admin/rtr/config" {
|
|
||||||
let json = serde_json::to_string_pretty(&AdminConfigResponse {
|
|
||||||
status: "ok",
|
|
||||||
config: state.runtime_config.current(),
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "POST" && request.path == "/admin/rtr/slurm/reload" {
|
|
||||||
let reload = reload_source(&state, "admin_slurm_reload", true).await;
|
|
||||||
return write_json_or_error(stream, reload).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.path == "/admin/rtr/slurm/files" && request.method == "GET" {
|
|
||||||
let Some(slurm_admin) = state.slurm_admin.as_ref() else {
|
|
||||||
write_response(stream, 400, "SLURM admin is disabled", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string_pretty(&SlurmFileListResponse {
|
|
||||||
status: "ok",
|
|
||||||
files: slurm_admin.list_files()?,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.path == "/admin/rtr/slurm/files" && request.method == "POST" {
|
|
||||||
let Some(slurm_admin) = state.slurm_admin.as_ref() else {
|
|
||||||
write_response(stream, 400, "SLURM admin is disabled", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let request_body = parse_json::<SlurmFileWriteRequest>(stream, &body).await?;
|
|
||||||
let Some(name) = request_body.name.as_deref() else {
|
|
||||||
write_response(stream, 400, "missing SLURM file name", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let reload = request_body.reload.unwrap_or(false);
|
|
||||||
let operation = slurm_admin.put_file(name, &request_body.content, "create_or_update");
|
|
||||||
return apply_slurm_operation(stream, &state, operation, reload).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let slurm_file_prefix = "/admin/rtr/slurm/files/";
|
|
||||||
if request.path.starts_with(slurm_file_prefix) {
|
|
||||||
let rest = request.path[slurm_file_prefix.len()..].to_string();
|
|
||||||
return route_slurm_file_request(stream, request, body, state, rest).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
write_response(stream, 404, "not found", "text/plain").await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn route_slurm_file_request(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
request: RequestHeader,
|
|
||||||
body: Vec<u8>,
|
|
||||||
state: AdminState,
|
|
||||||
rest: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let Some(slurm_admin) = state.slurm_admin.as_ref() else {
|
|
||||||
write_response(stream, 400, "SLURM admin is disabled", "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if request.method == "GET" {
|
|
||||||
let file = match slurm_admin.read_file(&rest) {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid SLURM file request: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string_pretty(&SlurmFileContentResponse { status: "ok", file })?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "PUT" {
|
|
||||||
let request_body = parse_json::<SlurmFileWriteRequest>(stream, &body).await?;
|
|
||||||
let reload = parse_reload_query(request.query.as_deref(), request_body.reload);
|
|
||||||
let operation = slurm_admin.put_file(&rest, &request_body.content, "create_or_update");
|
|
||||||
return apply_slurm_operation(stream, &state, operation, reload).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "DELETE" {
|
|
||||||
let reload = if body.is_empty() {
|
|
||||||
parse_reload_query(request.query.as_deref(), None)
|
|
||||||
} else {
|
|
||||||
let request_body = parse_json::<SlurmFileActionRequest>(stream, &body).await?;
|
|
||||||
parse_reload_query(request.query.as_deref(), request_body.reload)
|
|
||||||
};
|
|
||||||
let operation = slurm_admin.delete_file(&rest);
|
|
||||||
return apply_slurm_operation(stream, &state, operation, reload).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if request.method == "POST" {
|
|
||||||
if let Some(name) = rest.strip_suffix("/enable") {
|
|
||||||
let request_body: SlurmFileActionRequest = parse_optional_json(stream, &body).await?;
|
|
||||||
let reload = parse_reload_query(request.query.as_deref(), request_body.reload);
|
|
||||||
let operation = slurm_admin.enable_file(name);
|
|
||||||
return apply_slurm_operation(stream, &state, operation, reload).await;
|
|
||||||
}
|
|
||||||
if let Some(name) = rest.strip_suffix("/disable") {
|
|
||||||
let request_body: SlurmFileActionRequest = parse_optional_json(stream, &body).await?;
|
|
||||||
let reload = parse_reload_query(request.query.as_deref(), request_body.reload);
|
|
||||||
let operation = slurm_admin.disable_file(name);
|
|
||||||
return apply_slurm_operation(stream, &state, operation, reload).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
write_response(stream, 404, "not found", "text/plain").await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn apply_slurm_operation(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
state: &AdminState,
|
|
||||||
operation: Result<crate::slurm::admin::AppliedSlurmFileOperation>,
|
|
||||||
reload: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let operation = match operation {
|
|
||||||
Ok(operation) => operation,
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid SLURM operation: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !reload {
|
|
||||||
let json = serde_json::to_string_pretty(&SlurmOperationResponse {
|
|
||||||
status: "ok",
|
|
||||||
operation: operation.result,
|
|
||||||
reload: None,
|
|
||||||
rollback: None,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
match reload_source(state, "admin_slurm_changed", true).await {
|
|
||||||
Ok(result) => {
|
|
||||||
let json = serde_json::to_string_pretty(&SlurmOperationResponse {
|
|
||||||
status: "ok",
|
|
||||||
operation: operation.result,
|
|
||||||
reload: Some(result),
|
|
||||||
rollback: None,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let rollback = match operation.rollback() {
|
|
||||||
Ok(()) => match reload_source(state, "admin_slurm_rollback", true).await {
|
|
||||||
Ok(result) => Some(RollbackReport {
|
|
||||||
status: "ok".to_string(),
|
|
||||||
reload: Some(result),
|
|
||||||
error: None,
|
|
||||||
}),
|
|
||||||
Err(reload_err) => Some(RollbackReport {
|
|
||||||
status: "reload_failed".to_string(),
|
|
||||||
reload: None,
|
|
||||||
error: Some(reload_err.to_string()),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Err(rollback_err) => Some(RollbackReport {
|
|
||||||
status: "failed".to_string(),
|
|
||||||
reload: None,
|
|
||||||
error: Some(rollback_err.to_string()),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string_pretty(&SlurmOperationErrorResponse {
|
|
||||||
status: "reload_failed",
|
|
||||||
error: err.to_string(),
|
|
||||||
rollback,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 400, &json, "application/json").await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn reload_source(
|
|
||||||
state: &AdminState,
|
|
||||||
phase: &'static str,
|
|
||||||
force: bool,
|
|
||||||
) -> Result<SourceReloadResult> {
|
|
||||||
let Some(source_reload) = state.source_reload.as_ref() else {
|
|
||||||
return Err(anyhow!("source reload is not available"));
|
|
||||||
};
|
|
||||||
source_reload.reload(phase, force).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn trigger_process_lifecycle(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
state: &AdminState,
|
|
||||||
reason: RtrShutdownReason,
|
|
||||||
) -> Result<()> {
|
|
||||||
let Some(process_shutdown) = state.process_shutdown.as_ref() else {
|
|
||||||
write_response(
|
|
||||||
stream,
|
|
||||||
400,
|
|
||||||
"process lifecycle admin is disabled",
|
|
||||||
"text/plain",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let action = match reason {
|
|
||||||
RtrShutdownReason::CacheRestart => "restart",
|
|
||||||
RtrShutdownReason::CacheShutdown => "shutdown",
|
|
||||||
RtrShutdownReason::CloseOnly => {
|
|
||||||
unreachable!("close-only is not a process lifecycle action")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let json = serde_json::to_string_pretty(&LifecycleResponse {
|
|
||||||
status: "ok",
|
|
||||||
action,
|
|
||||||
reason: reason.as_str(),
|
|
||||||
process_exit: true,
|
|
||||||
})?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
if process_shutdown.send(reason).await.is_err() {
|
|
||||||
warn!("process lifecycle request accepted but shutdown receiver is gone");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn parse_json<T: for<'de> Deserialize<'de>>(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
body: &[u8],
|
|
||||||
) -> Result<T> {
|
|
||||||
match serde_json::from_slice::<T>(body) {
|
|
||||||
Ok(value) => Ok(value),
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid json: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
Err(anyhow!(message))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn parse_optional_json<T: for<'de> Deserialize<'de> + Default>(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
body: &[u8],
|
|
||||||
) -> Result<T> {
|
|
||||||
if body.is_empty() {
|
|
||||||
return Ok(T::default());
|
|
||||||
}
|
|
||||||
parse_json(stream, body).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_json_or_error<T: Serialize>(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
result: Result<T>,
|
|
||||||
) -> Result<()> {
|
|
||||||
match result {
|
|
||||||
Ok(value) => {
|
|
||||||
let json = serde_json::to_string_pretty(&value)?;
|
|
||||||
write_response(stream, 200, &json, "application/json").await?;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("reload failed: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
enum LogStream {
|
|
||||||
Stdout,
|
|
||||||
Stderr,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogStream {
|
|
||||||
fn parse(value: Option<&str>) -> Result<Self> {
|
|
||||||
match value.unwrap_or("stdout") {
|
|
||||||
"stdout" => Ok(Self::Stdout),
|
|
||||||
"stderr" => Ok(Self::Stderr),
|
|
||||||
other => Err(anyhow!(
|
|
||||||
"invalid stream '{}': expected 'stdout' or 'stderr'",
|
|
||||||
other
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Stdout => "stdout",
|
|
||||||
Self::Stderr => "stderr",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LogTailRequest {
|
|
||||||
stream: LogStream,
|
|
||||||
lines: usize,
|
|
||||||
follow: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogTailRequest {
|
|
||||||
fn parse(query: Option<&str>) -> Result<Self> {
|
|
||||||
let stream = LogStream::parse(query_param(query, "stream"))?;
|
|
||||||
let lines = query_param(query, "lines")
|
|
||||||
.map(|value| {
|
|
||||||
value
|
|
||||||
.parse::<usize>()
|
|
||||||
.map_err(|err| anyhow!("invalid lines '{}': {}", value, err))
|
|
||||||
})
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(200)
|
|
||||||
.clamp(1, 5000);
|
|
||||||
let follow = query_param(query, "follow")
|
|
||||||
.map(|value| parse_bool_query(value, "follow"))
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(true);
|
|
||||||
Ok(Self {
|
|
||||||
stream,
|
|
||||||
lines,
|
|
||||||
follow,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn tail_log_stream(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
query: Option<&str>,
|
|
||||||
config: &LogTailConfig,
|
|
||||||
) -> Result<()> {
|
|
||||||
let request = match LogTailRequest::parse(query) {
|
|
||||||
Ok(request) => request,
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("invalid log tail request: {err}");
|
|
||||||
write_response(stream, 400, &message, "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let path = config.path_for(request.stream);
|
|
||||||
if !Path::new(&path).is_file() {
|
|
||||||
let message = format!("log file not found: {}", path.display());
|
|
||||||
write_response(stream, 404, &message, "text/plain").await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
write_chunked_headers(stream, "text/plain; charset=utf-8").await?;
|
|
||||||
let (tail, mut offset) = read_tail(&path, request.lines).await?;
|
|
||||||
if !tail.is_empty() {
|
|
||||||
write_chunk(stream, &tail).await?;
|
|
||||||
}
|
|
||||||
if !request.follow {
|
|
||||||
write_final_chunk(stream).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
sleep(Duration::from_secs(1)).await;
|
|
||||||
let metadata = match tokio::fs::metadata(&path).await {
|
|
||||||
Ok(metadata) => metadata,
|
|
||||||
Err(err) => {
|
|
||||||
let message = format!("\nlog file unavailable: {err}\n");
|
|
||||||
let _ = write_chunk(stream, message.as_bytes()).await;
|
|
||||||
let _ = write_final_chunk(stream).await;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let len = metadata.len();
|
|
||||||
if len < offset {
|
|
||||||
offset = 0;
|
|
||||||
write_chunk(stream, b"\nlog file truncated; restarting from beginning\n").await?;
|
|
||||||
}
|
|
||||||
if len == offset {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut file = tokio::fs::File::open(&path).await?;
|
|
||||||
file.seek(std::io::SeekFrom::Start(offset)).await?;
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
file.read_to_end(&mut buf).await?;
|
|
||||||
offset = len;
|
|
||||||
if !buf.is_empty() {
|
|
||||||
write_chunk(stream, &buf).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn read_tail(path: &Path, lines: usize) -> Result<(Vec<u8>, u64)> {
|
|
||||||
const MAX_INITIAL_TAIL_BYTES: u64 = 1024 * 1024;
|
|
||||||
|
|
||||||
let metadata = tokio::fs::metadata(path).await?;
|
|
||||||
let len = metadata.len();
|
|
||||||
let start = len.saturating_sub(MAX_INITIAL_TAIL_BYTES);
|
|
||||||
let mut file = tokio::fs::File::open(path).await?;
|
|
||||||
file.seek(std::io::SeekFrom::Start(start)).await?;
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
file.read_to_end(&mut buf).await?;
|
|
||||||
Ok((tail_log_lines(buf, lines), len))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tail_log_lines(buf: Vec<u8>, lines: usize) -> Vec<u8> {
|
|
||||||
if lines == 0 || buf.is_empty() {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut seen = 0usize;
|
|
||||||
for (idx, byte) in buf.iter().enumerate().rev() {
|
|
||||||
if *byte == b'\n' {
|
|
||||||
seen += 1;
|
|
||||||
if seen > lines {
|
|
||||||
return buf[idx + 1..].to_vec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_param<'a>(query: Option<&'a str>, key: &str) -> Option<&'a str> {
|
|
||||||
query?.split('&').find_map(|part| {
|
|
||||||
let (name, value) = part.split_once('=').unwrap_or((part, ""));
|
|
||||||
(name == key).then_some(value)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_bool_query(value: &str, name: &str) -> Result<bool> {
|
|
||||||
match value {
|
|
||||||
"true" | "1" | "yes" | "on" => Ok(true),
|
|
||||||
"false" | "0" | "no" | "off" => Ok(false),
|
|
||||||
_ => Err(anyhow!("invalid {} '{}': expected true/false", name, value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_chunked_headers(stream: &mut TcpStream, content_type: &str) -> Result<()> {
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 200 OK\r\ncontent-type: {content_type}\r\ntransfer-encoding: chunked\r\ncache-control: no-store\r\nconnection: close\r\n\r\n"
|
|
||||||
);
|
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_chunk(stream: &mut TcpStream, chunk: &[u8]) -> Result<()> {
|
|
||||||
if chunk.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let header = format!("{:x}\r\n", chunk.len());
|
|
||||||
stream.write_all(header.as_bytes()).await?;
|
|
||||||
stream.write_all(chunk).await?;
|
|
||||||
stream.write_all(b"\r\n").await?;
|
|
||||||
stream.flush().await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_final_chunk(stream: &mut TcpStream) -> Result<()> {
|
|
||||||
stream.write_all(b"0\r\n\r\n").await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct AdminHealthResponse {
|
|
||||||
status: &'static str,
|
|
||||||
config_api: bool,
|
|
||||||
source_reload_api: bool,
|
|
||||||
lifecycle_api: bool,
|
|
||||||
process_lifecycle_api: bool,
|
|
||||||
slurm_api: bool,
|
|
||||||
logs_api: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct LifecycleResponse {
|
|
||||||
status: &'static str,
|
|
||||||
action: &'static str,
|
|
||||||
reason: &'static str,
|
|
||||||
process_exit: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct AdminConfigResponse {
|
|
||||||
status: &'static str,
|
|
||||||
config: RuntimeConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SlurmFileListResponse {
|
|
||||||
status: &'static str,
|
|
||||||
files: Vec<crate::slurm::admin::SlurmFileListEntry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SlurmFileContentResponse {
|
|
||||||
status: &'static str,
|
|
||||||
file: crate::slurm::admin::SlurmFileContent,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SlurmOperationResponse {
|
|
||||||
status: &'static str,
|
|
||||||
operation: SlurmFileOperationResult,
|
|
||||||
reload: Option<SourceReloadResult>,
|
|
||||||
rollback: Option<RollbackReport>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SlurmOperationErrorResponse {
|
|
||||||
status: &'static str,
|
|
||||||
error: String,
|
|
||||||
rollback: Option<RollbackReport>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RollbackReport {
|
|
||||||
status: String,
|
|
||||||
reload: Option<SourceReloadResult>,
|
|
||||||
error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RequestHeader {
|
|
||||||
method: String,
|
|
||||||
path: String,
|
|
||||||
query: Option<String>,
|
|
||||||
content_length: Option<usize>,
|
|
||||||
authorization: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_request_header(header: &str) -> Result<RequestHeader> {
|
|
||||||
let mut lines = header.lines();
|
|
||||||
let request_line = lines
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("missing HTTP request line"))?;
|
|
||||||
let mut parts = request_line.split_whitespace();
|
|
||||||
let method = parts
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("missing HTTP method"))?
|
|
||||||
.to_string();
|
|
||||||
let target = parts
|
|
||||||
.next()
|
|
||||||
.ok_or_else(|| anyhow!("missing HTTP path"))?
|
|
||||||
.to_string();
|
|
||||||
let (path, query) = match target.split_once('?') {
|
|
||||||
Some((path, query)) => (path.to_string(), Some(query.to_string())),
|
|
||||||
None => (target, None),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut content_length = None;
|
|
||||||
let mut authorization = None;
|
|
||||||
for line in lines {
|
|
||||||
let Some((name, value)) = line.split_once(':') else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let name = name.trim().to_ascii_lowercase();
|
|
||||||
let value = value.trim();
|
|
||||||
match name.as_str() {
|
|
||||||
"content-length" => {
|
|
||||||
content_length = Some(
|
|
||||||
value
|
|
||||||
.parse::<usize>()
|
|
||||||
.map_err(|err| anyhow!("invalid content-length '{}': {}", value, err))?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
"authorization" => authorization = Some(value.to_string()),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RequestHeader {
|
|
||||||
method,
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
content_length,
|
|
||||||
authorization,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_header_end(buffer: &[u8]) -> Option<usize> {
|
|
||||||
buffer.windows(4).position(|window| window == b"\r\n\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn write_response(
|
|
||||||
stream: &mut TcpStream,
|
|
||||||
status: u16,
|
|
||||||
body: &str,
|
|
||||||
content_type: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
let reason = match status {
|
|
||||||
200 => "OK",
|
|
||||||
400 => "Bad Request",
|
|
||||||
401 => "Unauthorized",
|
|
||||||
404 => "Not Found",
|
|
||||||
413 => "Payload Too Large",
|
|
||||||
_ => "Internal Server Error",
|
|
||||||
};
|
|
||||||
let response = format!(
|
|
||||||
"HTTP/1.1 {status} {reason}\r\ncontent-type: {content_type}\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
|
|
||||||
body.len()
|
|
||||||
);
|
|
||||||
stream.write_all(response.as_bytes()).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,115 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
900
src/rtr/cache/core.rs
vendored
900
src/rtr/cache/core.rs
vendored
@ -1,900 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
use crate::rtr::payload::{Payload, Timing};
|
|
||||||
|
|
||||||
use super::model::{Delta, DualTime, Snapshot};
|
|
||||||
use super::ordering::{ChangeKey, change_key};
|
|
||||||
|
|
||||||
const SERIAL_HALF_RANGE: u32 = 1 << 31;
|
|
||||||
const VERSION_COUNT: usize = 3;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
|
|
||||||
pub enum CacheAvailability {
|
|
||||||
Ready,
|
|
||||||
NoDataAvailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
|
||||||
pub struct SessionIds {
|
|
||||||
ids: [u16; VERSION_COUNT],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionIds {
|
|
||||||
pub fn from_array(ids: [u16; VERSION_COUNT]) -> Self {
|
|
||||||
Self { ids }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn random_distinct() -> Self {
|
|
||||||
let mut ids = [0u16; VERSION_COUNT];
|
|
||||||
for idx in 0..ids.len() {
|
|
||||||
loop {
|
|
||||||
let candidate: u16 = rand::random();
|
|
||||||
if ids[..idx].iter().all(|existing| *existing != candidate) {
|
|
||||||
ids[idx] = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self { ids }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, version: u8) -> u16 {
|
|
||||||
self.ids[version_index(version)]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_array(&self) -> [u16; VERSION_COUNT] {
|
|
||||||
self.ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct VersionState {
|
|
||||||
session_id: u16,
|
|
||||||
serial: u32,
|
|
||||||
snapshot: Arc<Snapshot>,
|
|
||||||
#[serde(skip)]
|
|
||||||
rtr_payloads: Arc<Vec<Payload>>,
|
|
||||||
#[serde(skip)]
|
|
||||||
deltas: VecDeque<Arc<Delta>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VersionState {
|
|
||||||
fn new(session_id: u16, serial: u32, snapshot: Snapshot, max_delta: u8) -> Self {
|
|
||||||
let rtr_payloads = snapshot.rtr_payloads_for_rtr_arc();
|
|
||||||
Self {
|
|
||||||
session_id,
|
|
||||||
serial,
|
|
||||||
snapshot: Arc::new(snapshot),
|
|
||||||
rtr_payloads,
|
|
||||||
deltas: VecDeque::with_capacity(max_delta as usize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RtrCache {
|
|
||||||
availability: CacheAvailability,
|
|
||||||
versions: [VersionState; VERSION_COUNT],
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timing: Timing,
|
|
||||||
last_update_begin: DualTime,
|
|
||||||
last_update_end: DualTime,
|
|
||||||
created_at: DualTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RtrCache {
|
|
||||||
fn default() -> Self {
|
|
||||||
let now = DualTime::now();
|
|
||||||
let session_ids = SessionIds::random_distinct();
|
|
||||||
let versions = std::array::from_fn(|idx| {
|
|
||||||
VersionState::new(session_ids.as_array()[idx], 0, Snapshot::empty(), 100)
|
|
||||||
});
|
|
||||||
Self {
|
|
||||||
availability: CacheAvailability::Ready,
|
|
||||||
versions,
|
|
||||||
max_delta: 100,
|
|
||||||
prune_delta_by_snapshot_size: false,
|
|
||||||
timing: Timing::default(),
|
|
||||||
last_update_begin: now.clone(),
|
|
||||||
last_update_end: now.clone(),
|
|
||||||
created_at: now,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RtrCacheBuilder {
|
|
||||||
availability: Option<CacheAvailability>,
|
|
||||||
session_ids: Option<SessionIds>,
|
|
||||||
max_delta: Option<u8>,
|
|
||||||
prune_delta_by_snapshot_size: Option<bool>,
|
|
||||||
timing: Option<Timing>,
|
|
||||||
serials: Option<[u32; VERSION_COUNT]>,
|
|
||||||
snapshots: Option<[Snapshot; VERSION_COUNT]>,
|
|
||||||
deltas: Option<[VecDeque<Arc<Delta>>; VERSION_COUNT]>,
|
|
||||||
created_at: Option<DualTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrCacheBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
availability: None,
|
|
||||||
session_ids: None,
|
|
||||||
max_delta: None,
|
|
||||||
prune_delta_by_snapshot_size: None,
|
|
||||||
timing: None,
|
|
||||||
serials: None,
|
|
||||||
snapshots: None,
|
|
||||||
deltas: None,
|
|
||||||
created_at: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_ids(mut self, v: SessionIds) -> Self {
|
|
||||||
self.session_ids = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn availability(mut self, v: CacheAvailability) -> Self {
|
|
||||||
self.availability = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_delta(mut self, v: u8) -> Self {
|
|
||||||
self.max_delta = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prune_delta_by_snapshot_size(mut self, v: bool) -> Self {
|
|
||||||
self.prune_delta_by_snapshot_size = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timing(mut self, v: Timing) -> Self {
|
|
||||||
self.timing = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serials(mut self, v: [u32; VERSION_COUNT]) -> Self {
|
|
||||||
self.serials = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshots(mut self, v: [Snapshot; VERSION_COUNT]) -> Self {
|
|
||||||
self.snapshots = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deltas_by_version(mut self, v: [VecDeque<Arc<Delta>>; VERSION_COUNT]) -> Self {
|
|
||||||
self.deltas = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created_at(mut self, v: DualTime) -> Self {
|
|
||||||
self.created_at = Some(v);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> RtrCache {
|
|
||||||
let now = DualTime::now();
|
|
||||||
let max_delta = self.max_delta.unwrap_or(100);
|
|
||||||
let prune_delta_by_snapshot_size = self.prune_delta_by_snapshot_size.unwrap_or(false);
|
|
||||||
let timing = self.timing.unwrap_or_default();
|
|
||||||
let session_ids = self.session_ids.unwrap_or_else(SessionIds::random_distinct);
|
|
||||||
let serials = self.serials.unwrap_or([0; VERSION_COUNT]);
|
|
||||||
let snapshots = self
|
|
||||||
.snapshots
|
|
||||||
.unwrap_or_else(|| std::array::from_fn(|_| Snapshot::empty()));
|
|
||||||
let deltas = self.deltas.unwrap_or_else(|| {
|
|
||||||
std::array::from_fn(|_| VecDeque::with_capacity(max_delta as usize))
|
|
||||||
});
|
|
||||||
|
|
||||||
let versions = std::array::from_fn(|idx| {
|
|
||||||
let snapshot = snapshots[idx].clone();
|
|
||||||
VersionState {
|
|
||||||
session_id: session_ids.as_array()[idx],
|
|
||||||
serial: serials[idx],
|
|
||||||
rtr_payloads: snapshot.rtr_payloads_for_rtr_arc(),
|
|
||||||
snapshot: Arc::new(snapshot),
|
|
||||||
deltas: deltas[idx].clone(),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let created_at = self.created_at.unwrap_or_else(|| now.clone());
|
|
||||||
let availability = self.availability.unwrap_or(CacheAvailability::Ready);
|
|
||||||
|
|
||||||
RtrCache {
|
|
||||||
availability,
|
|
||||||
versions,
|
|
||||||
max_delta,
|
|
||||||
prune_delta_by_snapshot_size,
|
|
||||||
timing,
|
|
||||||
last_update_begin: now.clone(),
|
|
||||||
last_update_end: now,
|
|
||||||
created_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrCache {
|
|
||||||
fn set_unavailable(&mut self) {
|
|
||||||
warn!(
|
|
||||||
"RTR cache entering NoDataAvailable: serials={:?}",
|
|
||||||
self.serials()
|
|
||||||
);
|
|
||||||
self.availability = CacheAvailability::NoDataAvailable;
|
|
||||||
for version_state in &mut self.versions {
|
|
||||||
version_state.snapshot = Arc::new(Snapshot::empty());
|
|
||||||
version_state.rtr_payloads = version_state.snapshot.rtr_payloads_for_rtr_arc();
|
|
||||||
version_state.deltas.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reinitialize_from_snapshot(&mut self, source_snapshot: &Snapshot) -> AppliedUpdate {
|
|
||||||
let old_serials = self.serials();
|
|
||||||
let old_session_ids = self.session_ids();
|
|
||||||
let new_session_ids = SessionIds::random_distinct();
|
|
||||||
self.availability = CacheAvailability::Ready;
|
|
||||||
|
|
||||||
for version in 0..VERSION_COUNT {
|
|
||||||
let v = version as u8;
|
|
||||||
let state = &mut self.versions[version];
|
|
||||||
state.session_id = new_session_ids.get(v);
|
|
||||||
state.serial = 1;
|
|
||||||
state.snapshot = Arc::new(project_snapshot_for_version(source_snapshot, v));
|
|
||||||
state.rtr_payloads = state.snapshot.rtr_payloads_for_rtr_arc();
|
|
||||||
state.deltas.clear();
|
|
||||||
}
|
|
||||||
self.last_update_end = DualTime::now();
|
|
||||||
info!(
|
|
||||||
"RTR cache reinitialized from usable snapshot: old_serials={:?}, new_serials={:?}, old_session_ids={:?}, new_session_ids={:?}",
|
|
||||||
old_serials,
|
|
||||||
self.serials(),
|
|
||||||
old_session_ids,
|
|
||||||
new_session_ids
|
|
||||||
);
|
|
||||||
|
|
||||||
self.applied_update_with_clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_serial(state: &mut VersionState) -> u32 {
|
|
||||||
let old = state.serial;
|
|
||||||
state.serial = state.serial.wrapping_add(1);
|
|
||||||
debug!(
|
|
||||||
"RTR cache advanced serial for version state: old_serial={}, new_serial={}",
|
|
||||||
old, state.serial
|
|
||||||
);
|
|
||||||
state.serial
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_delta(
|
|
||||||
state: &mut VersionState,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
delta: Arc<Delta>,
|
|
||||||
) {
|
|
||||||
state.deltas.push_back(delta);
|
|
||||||
Self::prune_delta_window(state, max_delta, prune_delta_by_snapshot_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prune_delta_window(
|
|
||||||
state: &mut VersionState,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
) {
|
|
||||||
let max_keep = usize::from(max_delta.max(1));
|
|
||||||
while state.deltas.len() > max_keep {
|
|
||||||
state.deltas.pop_front();
|
|
||||||
}
|
|
||||||
let mut dropped_serials = Vec::new();
|
|
||||||
if prune_delta_by_snapshot_size {
|
|
||||||
let snapshot_wire_size = estimate_snapshot_payload_wire_size(state.snapshot.as_ref());
|
|
||||||
let mut cumulative_delta_wire_size =
|
|
||||||
estimate_delta_window_payload_wire_size(&state.deltas);
|
|
||||||
while !state.deltas.is_empty() && cumulative_delta_wire_size >= snapshot_wire_size {
|
|
||||||
if let Some(oldest) = state.deltas.pop_front() {
|
|
||||||
dropped_serials.push(oldest.serial());
|
|
||||||
cumulative_delta_wire_size =
|
|
||||||
estimate_delta_window_payload_wire_size(&state.deltas);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug!(
|
|
||||||
"RTR cache delta-size pruning evaluated: snapshot_wire_size={}, cumulative_delta_wire_size={}, dropped_serials={:?}",
|
|
||||||
snapshot_wire_size, cumulative_delta_wire_size, dropped_serials
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn delta_window(state: &VersionState) -> Option<(u32, u32)> {
|
|
||||||
let min = state.deltas.front().map(|d| d.serial());
|
|
||||||
let max = state.deltas.back().map(|d| d.serial());
|
|
||||||
match (min, max) {
|
|
||||||
(Some(min), Some(max)) => Some((min, max)),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn apply_update(
|
|
||||||
&mut self,
|
|
||||||
new_payloads: Vec<Payload>,
|
|
||||||
) -> Result<Option<AppliedUpdate>> {
|
|
||||||
let source_snapshot = Snapshot::from_payloads(new_payloads);
|
|
||||||
self.apply_update_from_snapshot(source_snapshot)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn apply_update_from_snapshot(
|
|
||||||
&mut self,
|
|
||||||
source_snapshot: Snapshot,
|
|
||||||
) -> Result<Option<AppliedUpdate>> {
|
|
||||||
self.last_update_begin = DualTime::now();
|
|
||||||
info!(
|
|
||||||
"RTR cache applying update: availability={:?}, current_serials={:?}, incoming_snapshot_sizes=(origins={}, router_keys={}, aspas={})",
|
|
||||||
self.availability,
|
|
||||||
self.serials(),
|
|
||||||
source_snapshot.origins().len(),
|
|
||||||
source_snapshot.router_keys().len(),
|
|
||||||
source_snapshot.aspas().len()
|
|
||||||
);
|
|
||||||
if source_snapshot.is_empty() {
|
|
||||||
let changed = self.availability != CacheAvailability::NoDataAvailable
|
|
||||||
|| self.versions.iter().any(|state| !state.snapshot.is_empty())
|
|
||||||
|| self.versions.iter().any(|state| !state.deltas.is_empty());
|
|
||||||
|
|
||||||
self.set_unavailable();
|
|
||||||
self.last_update_end = DualTime::now();
|
|
||||||
if !changed {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
return Ok(Some(self.applied_update_with_clear()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.availability == CacheAvailability::NoDataAvailable {
|
|
||||||
return Ok(Some(self.reinitialize_from_snapshot(&source_snapshot)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut changed_any = false;
|
|
||||||
for version in 0..VERSION_COUNT {
|
|
||||||
let v = version as u8;
|
|
||||||
let projected = project_snapshot_for_version(&source_snapshot, v);
|
|
||||||
let state = &mut self.versions[version];
|
|
||||||
if state.snapshot.same_content(&projected) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (announced, withdrawn) = state.snapshot.diff(&projected);
|
|
||||||
if announced.is_empty() && withdrawn.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_serial = Self::next_serial(state);
|
|
||||||
let delta = Arc::new(Delta::new(new_serial, announced, withdrawn));
|
|
||||||
if delta.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
state.snapshot = Arc::new(projected);
|
|
||||||
state.rtr_payloads = state.snapshot.rtr_payloads_for_rtr_arc();
|
|
||||||
Self::push_delta(
|
|
||||||
state,
|
|
||||||
self.max_delta,
|
|
||||||
self.prune_delta_by_snapshot_size,
|
|
||||||
delta,
|
|
||||||
);
|
|
||||||
changed_any = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.last_update_end = DualTime::now();
|
|
||||||
if !changed_any {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"RTR cache applied update: serials={:?}, session_ids={:?}, delta_lengths={:?}",
|
|
||||||
self.serials(),
|
|
||||||
self.session_ids(),
|
|
||||||
self.delta_lengths()
|
|
||||||
);
|
|
||||||
Ok(Some(self.applied_update_with_windows()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn applied_update_with_clear(&self) -> AppliedUpdate {
|
|
||||||
let snapshots = std::array::from_fn(|idx| self.versions[idx].snapshot.clone());
|
|
||||||
let serials = std::array::from_fn(|idx| self.versions[idx].serial);
|
|
||||||
let session_ids = std::array::from_fn(|idx| self.versions[idx].session_id);
|
|
||||||
AppliedUpdate {
|
|
||||||
availability: self.availability,
|
|
||||||
snapshots,
|
|
||||||
serials,
|
|
||||||
session_ids,
|
|
||||||
deltas: [None, None, None],
|
|
||||||
delta_windows: [None, None, None],
|
|
||||||
clear_delta_windows: [true, true, true],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn applied_update_with_windows(&self) -> AppliedUpdate {
|
|
||||||
let snapshots = std::array::from_fn(|idx| self.versions[idx].snapshot.clone());
|
|
||||||
let serials = std::array::from_fn(|idx| self.versions[idx].serial);
|
|
||||||
let session_ids = std::array::from_fn(|idx| self.versions[idx].session_id);
|
|
||||||
let deltas = std::array::from_fn(|idx| self.versions[idx].deltas.back().cloned());
|
|
||||||
let delta_windows = std::array::from_fn(|idx| Self::delta_window(&self.versions[idx]));
|
|
||||||
AppliedUpdate {
|
|
||||||
availability: self.availability,
|
|
||||||
snapshots,
|
|
||||||
serials,
|
|
||||||
session_ids,
|
|
||||||
deltas,
|
|
||||||
delta_windows,
|
|
||||||
clear_delta_windows: [false, false, false],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn applied_update_with_existing_windows(&self) -> AppliedUpdate {
|
|
||||||
let snapshots = std::array::from_fn(|idx| self.versions[idx].snapshot.clone());
|
|
||||||
let serials = std::array::from_fn(|idx| self.versions[idx].serial);
|
|
||||||
let session_ids = std::array::from_fn(|idx| self.versions[idx].session_id);
|
|
||||||
let delta_windows = std::array::from_fn(|idx| Self::delta_window(&self.versions[idx]));
|
|
||||||
let clear_delta_windows = std::array::from_fn(|idx| self.versions[idx].deltas.is_empty());
|
|
||||||
AppliedUpdate {
|
|
||||||
availability: self.availability,
|
|
||||||
snapshots,
|
|
||||||
serials,
|
|
||||||
session_ids,
|
|
||||||
deltas: [None, None, None],
|
|
||||||
delta_windows,
|
|
||||||
clear_delta_windows,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_data_available(&self) -> bool {
|
|
||||||
self.availability == CacheAvailability::Ready
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn availability(&self) -> CacheAvailability {
|
|
||||||
self.availability
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_id_for_version(&self, version: u8) -> u16 {
|
|
||||||
self.versions[version_index(version)].session_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session_ids(&self) -> SessionIds {
|
|
||||||
SessionIds::from_array(std::array::from_fn(|idx| self.versions[idx].session_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot_for_version(&self, version: u8) -> Snapshot {
|
|
||||||
self.versions[version_index(version)]
|
|
||||||
.snapshot
|
|
||||||
.as_ref()
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rtr_payloads_for_version(&self, version: u8) -> Arc<Vec<Payload>> {
|
|
||||||
self.versions[version_index(version)].rtr_payloads.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serial_for_version(&self, version: u8) -> u32 {
|
|
||||||
self.versions[version_index(version)].serial
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serials(&self) -> [u32; VERSION_COUNT] {
|
|
||||||
std::array::from_fn(|idx| self.versions[idx].serial)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delta_lengths(&self) -> [usize; VERSION_COUNT] {
|
|
||||||
std::array::from_fn(|idx| self.versions[idx].deltas.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timing(&self) -> Timing {
|
|
||||||
self.timing
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_delta(&self) -> u8 {
|
|
||||||
self.max_delta
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prune_delta_by_snapshot_size(&self) -> bool {
|
|
||||||
self.prune_delta_by_snapshot_size
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn apply_runtime_config(
|
|
||||||
&mut self,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timing: Timing,
|
|
||||||
) -> Option<AppliedUpdate> {
|
|
||||||
let old_delta_lengths = self.delta_lengths();
|
|
||||||
let old_max_delta = self.max_delta;
|
|
||||||
let old_prune_delta_by_snapshot_size = self.prune_delta_by_snapshot_size;
|
|
||||||
let old_timing = self.timing;
|
|
||||||
|
|
||||||
self.max_delta = max_delta.max(1);
|
|
||||||
self.prune_delta_by_snapshot_size = prune_delta_by_snapshot_size;
|
|
||||||
self.timing = timing;
|
|
||||||
|
|
||||||
for state in &mut self.versions {
|
|
||||||
Self::prune_delta_window(state, self.max_delta, self.prune_delta_by_snapshot_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
let config_changed = old_max_delta != self.max_delta
|
|
||||||
|| old_prune_delta_by_snapshot_size != self.prune_delta_by_snapshot_size
|
|
||||||
|| old_timing.refresh != self.timing.refresh
|
|
||||||
|| old_timing.retry != self.timing.retry
|
|
||||||
|| old_timing.expire != self.timing.expire;
|
|
||||||
let delta_changed = old_delta_lengths != self.delta_lengths();
|
|
||||||
|
|
||||||
if config_changed {
|
|
||||||
info!(
|
|
||||||
"RTR cache runtime config updated: old_max_delta={}, new_max_delta={}, old_prune_delta_by_snapshot_size={}, new_prune_delta_by_snapshot_size={}, old_delta_lengths={:?}, new_delta_lengths={:?}, timing=({}, {}, {})",
|
|
||||||
old_max_delta,
|
|
||||||
self.max_delta,
|
|
||||||
old_prune_delta_by_snapshot_size,
|
|
||||||
self.prune_delta_by_snapshot_size,
|
|
||||||
old_delta_lengths,
|
|
||||||
self.delta_lengths(),
|
|
||||||
self.timing.refresh,
|
|
||||||
self.timing.retry,
|
|
||||||
self.timing.expire
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if delta_changed {
|
|
||||||
Some(self.applied_update_with_existing_windows())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_update_begin(&self) -> DualTime {
|
|
||||||
self.last_update_begin.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn last_update_end(&self) -> DualTime {
|
|
||||||
self.last_update_end.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created_at(&self) -> DualTime {
|
|
||||||
self.created_at.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_deltas_since_for_version(&self, version: u8, client_serial: u32) -> SerialResult {
|
|
||||||
let state = &self.versions[version_index(version)];
|
|
||||||
if client_serial == state.serial {
|
|
||||||
return SerialResult::UpToDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
serial_cmp(client_serial, state.serial),
|
|
||||||
Some(Ordering::Greater) | None
|
|
||||||
) {
|
|
||||||
return SerialResult::ResetRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
let deltas = match collect_deltas_since(state, client_serial) {
|
|
||||||
Some(deltas) => deltas,
|
|
||||||
None => return SerialResult::ResetRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
if deltas.is_empty() {
|
|
||||||
return SerialResult::UpToDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
let merged = merge_deltas_minimally(state.serial, &deltas);
|
|
||||||
if merged.is_empty() {
|
|
||||||
SerialResult::UpToDate
|
|
||||||
} else {
|
|
||||||
SerialResult::Delta(merged)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect_deltas_since(state: &VersionState, client_serial: u32) -> Option<Vec<Arc<Delta>>> {
|
|
||||||
if state.deltas.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let oldest_serial = state.deltas.front().unwrap().serial();
|
|
||||||
let min_supported = oldest_serial.wrapping_sub(1);
|
|
||||||
|
|
||||||
if matches!(
|
|
||||||
serial_cmp(client_serial, min_supported),
|
|
||||||
Some(Ordering::Less) | None
|
|
||||||
) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = Vec::new();
|
|
||||||
for delta in &state.deltas {
|
|
||||||
if serial_gt(delta.serial(), client_serial) {
|
|
||||||
result.push(delta.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(first) = result.first() {
|
|
||||||
if first.serial() != client_serial.wrapping_add(1) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_deltas_minimally(current_serial: u32, deltas: &[Arc<Delta>]) -> Delta {
|
|
||||||
let mut states = BTreeMap::<ChangeKey, LogicalStateRef<'_>>::new();
|
|
||||||
|
|
||||||
for delta in deltas {
|
|
||||||
for payload in delta.withdrawn() {
|
|
||||||
let key = change_key(payload);
|
|
||||||
let state = states.entry(key).or_insert_with(LogicalStateRef::new);
|
|
||||||
if state.before.is_none() && state.after.is_none() {
|
|
||||||
state.before = Some(payload);
|
|
||||||
}
|
|
||||||
state.after = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
for payload in delta.announced() {
|
|
||||||
let key = change_key(payload);
|
|
||||||
let state = states.entry(key).or_insert_with(LogicalStateRef::new);
|
|
||||||
state.after = Some(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut announced = Vec::new();
|
|
||||||
let mut withdrawn = Vec::new();
|
|
||||||
for (_key, state) in states {
|
|
||||||
match (state.before, state.after) {
|
|
||||||
(None, None) => {}
|
|
||||||
(None, Some(new_payload)) => announced.push(new_payload.clone()),
|
|
||||||
(Some(old_payload), None) => withdrawn.push(old_payload.clone()),
|
|
||||||
(Some(old_payload), Some(new_payload)) => {
|
|
||||||
if old_payload != new_payload {
|
|
||||||
if matches!(old_payload, Payload::Aspa(_))
|
|
||||||
&& matches!(new_payload, Payload::Aspa(_))
|
|
||||||
{
|
|
||||||
announced.push(new_payload.clone());
|
|
||||||
} else {
|
|
||||||
withdrawn.push(old_payload.clone());
|
|
||||||
announced.push(new_payload.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Delta::new(current_serial, announced, withdrawn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot {
|
|
||||||
snapshot.project_for_version(version)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_snapshot_payload_wire_size(snapshot: &Snapshot) -> usize {
|
|
||||||
snapshot
|
|
||||||
.rtr_payloads_for_rtr_arc()
|
|
||||||
.iter()
|
|
||||||
.map(|payload| estimate_payload_wire_size(payload, true))
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_delta_window_payload_wire_size(deltas: &VecDeque<Arc<Delta>>) -> usize {
|
|
||||||
deltas
|
|
||||||
.iter()
|
|
||||||
.map(|delta| estimate_delta_wire_size(delta))
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_delta_wire_size(delta: &Delta) -> usize {
|
|
||||||
delta
|
|
||||||
.payload_updates_for_rtr()
|
|
||||||
.iter()
|
|
||||||
.map(|(announce, payload)| estimate_payload_wire_size(payload, *announce))
|
|
||||||
.sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn estimate_payload_wire_size(payload: &Payload, announce: bool) -> usize {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(origin) => match origin.prefix().address {
|
|
||||||
crate::data_model::resources::ip_resources::IPAddress::V4(_) => 20,
|
|
||||||
crate::data_model::resources::ip_resources::IPAddress::V6(_) => 32,
|
|
||||||
},
|
|
||||||
Payload::RouterKey(key) => 8 + 20 + 4 + key.spki().len(),
|
|
||||||
Payload::Aspa(aspa) => {
|
|
||||||
let providers = if announce {
|
|
||||||
aspa.provider_asns().len()
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
8 + 4 + providers * 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
|
||||||
struct LogicalStateRef<'a> {
|
|
||||||
before: Option<&'a Payload>,
|
|
||||||
after: Option<&'a Payload>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> LogicalStateRef<'a> {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
before: None,
|
|
||||||
after: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum SerialResult {
|
|
||||||
UpToDate,
|
|
||||||
Delta(Delta),
|
|
||||||
ResetRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
|
||||||
pub struct CacheMemoryStats {
|
|
||||||
pub serials: [u32; VERSION_COUNT],
|
|
||||||
pub snapshot_payload_counts: [usize; VERSION_COUNT],
|
|
||||||
pub delta_lengths: [usize; VERSION_COUNT],
|
|
||||||
pub delta_payload_counts: [usize; VERSION_COUNT],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Serialize)]
|
|
||||||
pub struct PayloadCounts {
|
|
||||||
pub total: usize,
|
|
||||||
pub vrp: usize,
|
|
||||||
pub router_key: usize,
|
|
||||||
pub aspa: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PayloadCounts {
|
|
||||||
fn add_payload(&mut self, payload: &Payload) {
|
|
||||||
self.total += 1;
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(_) => self.vrp += 1,
|
|
||||||
Payload::RouterKey(_) => self.router_key += 1,
|
|
||||||
Payload::Aspa(_) => self.aspa += 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_counts(&mut self, other: Self) {
|
|
||||||
self.total += other.total;
|
|
||||||
self.vrp += other.vrp;
|
|
||||||
self.router_key += other.router_key;
|
|
||||||
self.aspa += other.aspa;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_payloads(payloads: &[Payload]) -> Self {
|
|
||||||
let mut counts = Self::default();
|
|
||||||
for payload in payloads {
|
|
||||||
counts.add_payload(payload);
|
|
||||||
}
|
|
||||||
counts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
|
||||||
pub struct DeltaReportStats {
|
|
||||||
pub serial: u32,
|
|
||||||
pub announced: PayloadCounts,
|
|
||||||
pub withdrawn: PayloadCounts,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
|
||||||
pub struct DeltaWindowReportStats {
|
|
||||||
pub length: usize,
|
|
||||||
pub oldest_serial: Option<u32>,
|
|
||||||
pub newest_serial: Option<u32>,
|
|
||||||
pub announced: PayloadCounts,
|
|
||||||
pub withdrawn: PayloadCounts,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
|
||||||
pub struct VersionReportStats {
|
|
||||||
pub version: u8,
|
|
||||||
pub session_id: u16,
|
|
||||||
pub serial: u32,
|
|
||||||
pub snapshot: PayloadCounts,
|
|
||||||
pub latest_delta: Option<DeltaReportStats>,
|
|
||||||
pub delta_window: DeltaWindowReportStats,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrCache {
|
|
||||||
pub fn memory_stats(&self) -> CacheMemoryStats {
|
|
||||||
let snapshot_payload_counts =
|
|
||||||
std::array::from_fn(|idx| self.versions[idx].rtr_payloads.len());
|
|
||||||
let delta_lengths = std::array::from_fn(|idx| self.versions[idx].deltas.len());
|
|
||||||
let delta_payload_counts = std::array::from_fn(|idx| {
|
|
||||||
self.versions[idx]
|
|
||||||
.deltas
|
|
||||||
.iter()
|
|
||||||
.map(|delta| delta.announced().len() + delta.withdrawn().len())
|
|
||||||
.sum()
|
|
||||||
});
|
|
||||||
CacheMemoryStats {
|
|
||||||
serials: self.serials(),
|
|
||||||
snapshot_payload_counts,
|
|
||||||
delta_lengths,
|
|
||||||
delta_payload_counts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn version_report_stats(&self) -> [VersionReportStats; VERSION_COUNT] {
|
|
||||||
std::array::from_fn(|idx| {
|
|
||||||
let state = &self.versions[idx];
|
|
||||||
let snapshot = PayloadCounts {
|
|
||||||
total: state.rtr_payloads.len(),
|
|
||||||
vrp: state.snapshot.origins().len(),
|
|
||||||
router_key: state.snapshot.router_keys().len(),
|
|
||||||
aspa: state.snapshot.aspas().len(),
|
|
||||||
};
|
|
||||||
let latest_delta = state.deltas.back().map(|delta| DeltaReportStats {
|
|
||||||
serial: delta.serial(),
|
|
||||||
announced: PayloadCounts::from_payloads(delta.announced()),
|
|
||||||
withdrawn: PayloadCounts::from_payloads(delta.withdrawn()),
|
|
||||||
});
|
|
||||||
let mut window_announced = PayloadCounts::default();
|
|
||||||
let mut window_withdrawn = PayloadCounts::default();
|
|
||||||
for delta in &state.deltas {
|
|
||||||
window_announced.add_counts(PayloadCounts::from_payloads(delta.announced()));
|
|
||||||
window_withdrawn.add_counts(PayloadCounts::from_payloads(delta.withdrawn()));
|
|
||||||
}
|
|
||||||
|
|
||||||
VersionReportStats {
|
|
||||||
version: idx as u8,
|
|
||||||
session_id: state.session_id,
|
|
||||||
serial: state.serial,
|
|
||||||
snapshot,
|
|
||||||
latest_delta,
|
|
||||||
delta_window: DeltaWindowReportStats {
|
|
||||||
length: state.deltas.len(),
|
|
||||||
oldest_serial: state.deltas.front().map(|delta| delta.serial()),
|
|
||||||
newest_serial: state.deltas.back().map(|delta| delta.serial()),
|
|
||||||
announced: window_announced,
|
|
||||||
withdrawn: window_withdrawn,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(super) struct AppliedUpdate {
|
|
||||||
pub(super) availability: CacheAvailability,
|
|
||||||
pub(super) snapshots: [Arc<Snapshot>; VERSION_COUNT],
|
|
||||||
pub(super) serials: [u32; VERSION_COUNT],
|
|
||||||
pub(super) session_ids: [u16; VERSION_COUNT],
|
|
||||||
pub(super) deltas: [Option<Arc<Delta>>; VERSION_COUNT],
|
|
||||||
pub(super) delta_windows: [Option<(u32, u32)>; VERSION_COUNT],
|
|
||||||
pub(super) clear_delta_windows: [bool; VERSION_COUNT],
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serial_cmp(a: u32, b: u32) -> Option<Ordering> {
|
|
||||||
if a == b {
|
|
||||||
return Some(Ordering::Equal);
|
|
||||||
}
|
|
||||||
|
|
||||||
let diff = a.wrapping_sub(b);
|
|
||||||
if diff == SERIAL_HALF_RANGE {
|
|
||||||
None
|
|
||||||
} else if diff < SERIAL_HALF_RANGE {
|
|
||||||
Some(Ordering::Greater)
|
|
||||||
} else {
|
|
||||||
Some(Ordering::Less)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serial_gt(a: u32, b: u32) -> bool {
|
|
||||||
matches!(serial_cmp(a, b), Some(Ordering::Greater))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn version_index(version: u8) -> usize {
|
|
||||||
match version {
|
|
||||||
0..=2 => version as usize,
|
|
||||||
_ => panic!("unsupported RTR protocol version: {}", version),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
20
src/rtr/cache/mod.rs
vendored
20
src/rtr/cache/mod.rs
vendored
@ -1,20 +0,0 @@
|
|||||||
mod core;
|
|
||||||
mod model;
|
|
||||||
mod ordering;
|
|
||||||
mod store;
|
|
||||||
|
|
||||||
pub use core::{
|
|
||||||
CacheAvailability, CacheMemoryStats, DeltaReportStats, DeltaWindowReportStats, PayloadCounts,
|
|
||||||
RtrCache, RtrCacheBuilder, SerialResult, SessionIds, VersionReportStats,
|
|
||||||
};
|
|
||||||
pub use model::{Delta, DualTime, Snapshot};
|
|
||||||
pub use ordering::{
|
|
||||||
OrderingViolation, validate_payload_update_refs_for_rtr, validate_payload_updates_for_rtr,
|
|
||||||
validate_payloads_for_rtr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
|
||||||
|
|
||||||
pub type SharedRtrCache = Arc<ArcSwap<RtrCache>>;
|
|
||||||
523
src/rtr/cache/model.rs
vendored
523
src/rtr/cache/model.rs
vendored
@ -1,523 +0,0 @@
|
|||||||
use std::collections::BTreeMap;
|
|
||||||
use std::sync::{Arc, OnceLock};
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
use crate::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey};
|
|
||||||
|
|
||||||
use super::ordering::{compare_payload_update_for_rtr, sort_payloads_for_rtr};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DualTime {
|
|
||||||
instant: Instant,
|
|
||||||
utc: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DualTime {
|
|
||||||
pub fn now() -> Self {
|
|
||||||
Self {
|
|
||||||
instant: Instant::now(),
|
|
||||||
utc: Utc::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn utc(&self) -> DateTime<Utc> {
|
|
||||||
self.utc
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn elapsed(&self) -> Duration {
|
|
||||||
self.instant.elapsed()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_expired(&self, duration: Duration) -> bool {
|
|
||||||
self.elapsed() >= duration
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.instant = Instant::now();
|
|
||||||
self.utc = Utc::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for DualTime {
|
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::Serializer,
|
|
||||||
{
|
|
||||||
self.utc.timestamp_millis().serialize(serializer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for DualTime {
|
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
||||||
where
|
|
||||||
D: serde::Deserializer<'de>,
|
|
||||||
{
|
|
||||||
let millis = i64::deserialize(deserializer)?;
|
|
||||||
let utc = DateTime::from_timestamp_millis(millis)
|
|
||||||
.ok_or_else(|| serde::de::Error::custom("invalid timestamp"))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
instant: Instant::now(),
|
|
||||||
utc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Snapshot {
|
|
||||||
origins: Arc<Vec<RouteOrigin>>,
|
|
||||||
router_keys: Arc<Vec<RouterKey>>,
|
|
||||||
aspas: Arc<Vec<Aspa>>,
|
|
||||||
created_at: DualTime,
|
|
||||||
origins_hash: [u8; 32],
|
|
||||||
router_keys_hash: [u8; 32],
|
|
||||||
aspas_hash: [u8; 32],
|
|
||||||
snapshot_hash: [u8; 32],
|
|
||||||
#[serde(skip)]
|
|
||||||
rtr_payloads_cache: OnceLock<Arc<Vec<Payload>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Snapshot {
|
|
||||||
pub fn new(origins: Vec<RouteOrigin>, router_keys: Vec<RouterKey>, aspas: Vec<Aspa>) -> Self {
|
|
||||||
Self::from_shared_parts(
|
|
||||||
Arc::new(sorted_dedup(origins)),
|
|
||||||
Arc::new(sorted_dedup(router_keys)),
|
|
||||||
Arc::new(normalize_aspas(aspas)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_shared_parts(
|
|
||||||
origins: Arc<Vec<RouteOrigin>>,
|
|
||||||
router_keys: Arc<Vec<RouterKey>>,
|
|
||||||
aspas: Arc<Vec<Aspa>>,
|
|
||||||
) -> Self {
|
|
||||||
let mut snapshot = Snapshot {
|
|
||||||
origins,
|
|
||||||
router_keys,
|
|
||||||
aspas,
|
|
||||||
created_at: DualTime::now(),
|
|
||||||
origins_hash: [0u8; 32],
|
|
||||||
router_keys_hash: [0u8; 32],
|
|
||||||
aspas_hash: [0u8; 32],
|
|
||||||
snapshot_hash: [0u8; 32],
|
|
||||||
rtr_payloads_cache: OnceLock::new(),
|
|
||||||
};
|
|
||||||
snapshot.recompute_hashes();
|
|
||||||
let cached = build_snapshot_payloads_for_rtr(&snapshot);
|
|
||||||
let _ = snapshot.rtr_payloads_cache.set(Arc::new(cached));
|
|
||||||
snapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty() -> Self {
|
|
||||||
Self::new(Vec::new(), Vec::new(), Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_payloads(payloads: Vec<Payload>) -> Self {
|
|
||||||
let mut builder = SnapshotBuilder::new();
|
|
||||||
builder.extend(payloads);
|
|
||||||
builder.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builder() -> SnapshotBuilder {
|
|
||||||
SnapshotBuilder::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn project_for_version(&self, version: u8) -> Self {
|
|
||||||
let router_keys = if version >= 1 {
|
|
||||||
self.router_keys.clone()
|
|
||||||
} else {
|
|
||||||
Arc::new(Vec::new())
|
|
||||||
};
|
|
||||||
let aspas = if version >= 2 {
|
|
||||||
self.aspas.clone()
|
|
||||||
} else {
|
|
||||||
Arc::new(Vec::new())
|
|
||||||
};
|
|
||||||
|
|
||||||
Self::from_shared_parts(self.origins.clone(), router_keys, aspas)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recompute_hashes(&mut self) {
|
|
||||||
self.origins_hash = self.compute_origins_hash();
|
|
||||||
self.router_keys_hash = self.compute_router_keys_hash();
|
|
||||||
self.aspas_hash = self.compute_aspas_hash();
|
|
||||||
self.snapshot_hash = self.compute_snapshot_hash();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_origins_hash(&self) -> [u8; 32] {
|
|
||||||
Self::hash_ordered_iter(self.origins.iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_router_keys_hash(&self) -> [u8; 32] {
|
|
||||||
Self::hash_ordered_iter(self.router_keys.iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_aspas_hash(&self) -> [u8; 32] {
|
|
||||||
Self::hash_ordered_iter(self.aspas.iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_snapshot_hash(&self) -> [u8; 32] {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(b"snapshot:v1");
|
|
||||||
hasher.update(self.origins_hash);
|
|
||||||
hasher.update(self.router_keys_hash);
|
|
||||||
hasher.update(self.aspas_hash);
|
|
||||||
hasher.finalize().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_ordered_iter<'a, T, I>(iter: I) -> [u8; 32]
|
|
||||||
where
|
|
||||||
T: Serialize + 'a,
|
|
||||||
I: IntoIterator<Item = &'a T>,
|
|
||||||
{
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(b"set:v1");
|
|
||||||
|
|
||||||
for item in iter {
|
|
||||||
let encoded =
|
|
||||||
serde_json::to_vec(item).expect("serialize snapshot item for hashing failed");
|
|
||||||
let len = (encoded.len() as u32).to_be_bytes();
|
|
||||||
hasher.update(len);
|
|
||||||
hasher.update(encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
hasher.finalize().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn diff(&self, new_snapshot: &Snapshot) -> (Vec<Payload>, Vec<Payload>) {
|
|
||||||
let mut announced = Vec::new();
|
|
||||||
let mut withdrawn = Vec::new();
|
|
||||||
|
|
||||||
if !self.same_origins(new_snapshot) {
|
|
||||||
diff_sorted(
|
|
||||||
self.origins.as_slice(),
|
|
||||||
new_snapshot.origins.as_slice(),
|
|
||||||
&mut announced,
|
|
||||||
&mut withdrawn,
|
|
||||||
Payload::RouteOrigin,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.same_router_keys(new_snapshot) {
|
|
||||||
diff_sorted(
|
|
||||||
self.router_keys.as_slice(),
|
|
||||||
new_snapshot.router_keys.as_slice(),
|
|
||||||
&mut announced,
|
|
||||||
&mut withdrawn,
|
|
||||||
Payload::RouterKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.same_aspas(new_snapshot) {
|
|
||||||
diff_aspas(
|
|
||||||
&self.aspas,
|
|
||||||
&new_snapshot.aspas,
|
|
||||||
&mut announced,
|
|
||||||
&mut withdrawn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
(announced, withdrawn)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created_at(&self) -> DualTime {
|
|
||||||
self.created_at.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn payloads(&self) -> Vec<Payload> {
|
|
||||||
let mut v =
|
|
||||||
Vec::with_capacity(self.origins.len() + self.router_keys.len() + self.aspas.len());
|
|
||||||
|
|
||||||
v.extend(self.origins.iter().cloned().map(Payload::RouteOrigin));
|
|
||||||
v.extend(self.router_keys.iter().cloned().map(Payload::RouterKey));
|
|
||||||
v.extend(self.aspas.iter().cloned().map(Payload::Aspa));
|
|
||||||
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn payloads_for_rtr(&self) -> Vec<Payload> {
|
|
||||||
self.rtr_payloads_for_rtr_arc().as_ref().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rtr_payloads_for_rtr_arc(&self) -> Arc<Vec<Payload>> {
|
|
||||||
self.rtr_payloads_cache
|
|
||||||
.get_or_init(|| Arc::new(build_snapshot_payloads_for_rtr(self)))
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn origins_hash(&self) -> [u8; 32] {
|
|
||||||
self.origins_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn router_keys_hash(&self) -> [u8; 32] {
|
|
||||||
self.router_keys_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aspas_hash(&self) -> [u8; 32] {
|
|
||||||
self.aspas_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot_hash(&self) -> [u8; 32] {
|
|
||||||
self.snapshot_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn same_origins(&self, other: &Self) -> bool {
|
|
||||||
self.origins_hash == other.origins_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn same_router_keys(&self, other: &Self) -> bool {
|
|
||||||
self.router_keys_hash == other.router_keys_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn same_aspas(&self, other: &Self) -> bool {
|
|
||||||
self.aspas_hash == other.aspas_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn same_content(&self, other: &Self) -> bool {
|
|
||||||
self.snapshot_hash == other.snapshot_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn origins(&self) -> &[RouteOrigin] {
|
|
||||||
self.origins.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn router_keys(&self) -> &[RouterKey] {
|
|
||||||
self.router_keys.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn aspas(&self) -> &[Aspa] {
|
|
||||||
self.aspas.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.origins.is_empty() && self.router_keys.is_empty() && self.aspas.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct SnapshotBuilder {
|
|
||||||
origins: Vec<RouteOrigin>,
|
|
||||||
router_keys: Vec<RouterKey>,
|
|
||||||
aspas: Vec<Aspa>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SnapshotBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(&mut self, payload: Payload) {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(o) => self.origins.push(o),
|
|
||||||
Payload::RouterKey(k) => self.router_keys.push(k),
|
|
||||||
Payload::Aspa(a) => self.aspas.push(a),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extend<I>(&mut self, payloads: I)
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = Payload>,
|
|
||||||
{
|
|
||||||
for payload in payloads {
|
|
||||||
self.push(payload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn finish(self) -> Snapshot {
|
|
||||||
Snapshot::new(self.origins, self.router_keys, normalize_aspas(self.aspas))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub struct Delta {
|
|
||||||
serial: u32,
|
|
||||||
announced: Vec<Payload>,
|
|
||||||
withdrawn: Vec<Payload>,
|
|
||||||
created_at: DualTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Delta {
|
|
||||||
pub fn new(serial: u32, mut announced: Vec<Payload>, mut withdrawn: Vec<Payload>) -> Self {
|
|
||||||
dedup_payloads(&mut announced);
|
|
||||||
dedup_payloads(&mut withdrawn);
|
|
||||||
|
|
||||||
sort_payloads_for_rtr(&mut announced, true);
|
|
||||||
sort_payloads_for_rtr(&mut withdrawn, false);
|
|
||||||
|
|
||||||
Delta {
|
|
||||||
serial,
|
|
||||||
announced,
|
|
||||||
withdrawn,
|
|
||||||
created_at: DualTime::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serial(&self) -> u32 {
|
|
||||||
self.serial
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn announced(&self) -> &[Payload] {
|
|
||||||
&self.announced
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn withdrawn(&self) -> &[Payload] {
|
|
||||||
&self.withdrawn
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created_at(&self) -> DualTime {
|
|
||||||
self.created_at.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.announced.is_empty() && self.withdrawn.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn payload_updates_for_rtr(&self) -> Vec<(bool, &Payload)> {
|
|
||||||
build_payload_updates_for_rtr_refs(&self.announced, &self.withdrawn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_payload_updates_for_rtr_refs<'a>(
|
|
||||||
announced: &'a [Payload],
|
|
||||||
withdrawn: &'a [Payload],
|
|
||||||
) -> Vec<(bool, &'a Payload)> {
|
|
||||||
let mut updates = Vec::with_capacity(announced.len() + withdrawn.len());
|
|
||||||
|
|
||||||
updates.extend(announced.iter().map(|p| (true, p)));
|
|
||||||
updates.extend(withdrawn.iter().map(|p| (false, p)));
|
|
||||||
|
|
||||||
updates.sort_by(|(a_upd, a_payload), (b_upd, b_payload)| {
|
|
||||||
compare_payload_update_for_rtr(a_payload, *a_upd, b_payload, *b_upd)
|
|
||||||
});
|
|
||||||
|
|
||||||
updates
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_payload_updates_for_rtr(
|
|
||||||
announced: &[Payload],
|
|
||||||
withdrawn: &[Payload],
|
|
||||||
) -> Vec<(bool, Payload)> {
|
|
||||||
let mut updates = Vec::with_capacity(announced.len() + withdrawn.len());
|
|
||||||
|
|
||||||
updates.extend(announced.iter().cloned().map(|p| (true, p)));
|
|
||||||
updates.extend(withdrawn.iter().cloned().map(|p| (false, p)));
|
|
||||||
|
|
||||||
updates.sort_by(|(a_upd, a_payload), (b_upd, b_payload)| {
|
|
||||||
compare_payload_update_for_rtr(a_payload, *a_upd, b_payload, *b_upd)
|
|
||||||
});
|
|
||||||
|
|
||||||
updates
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_snapshot_payloads_for_rtr(snapshot: &Snapshot) -> Vec<Payload> {
|
|
||||||
let mut payloads = snapshot.payloads();
|
|
||||||
sort_payloads_for_rtr(&mut payloads, true);
|
|
||||||
payloads
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dedup_payloads(payloads: &mut Vec<Payload>) {
|
|
||||||
payloads.sort();
|
|
||||||
payloads.dedup();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_aspas<I>(aspas: I) -> Vec<Aspa>
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = Aspa>,
|
|
||||||
{
|
|
||||||
let mut by_customer = BTreeMap::<u32, Vec<_>>::new();
|
|
||||||
|
|
||||||
for aspa in aspas {
|
|
||||||
let providers = by_customer
|
|
||||||
.entry(aspa.customer_asn().into_u32())
|
|
||||||
.or_default();
|
|
||||||
providers.extend(aspa.provider_asns().iter().copied());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut normalized = by_customer
|
|
||||||
.into_iter()
|
|
||||||
.map(|(customer_asn, providers)| Aspa::new(customer_asn.into(), providers))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
normalized.sort();
|
|
||||||
normalized
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff_aspas(
|
|
||||||
current: &[Aspa],
|
|
||||||
next: &[Aspa],
|
|
||||||
announced: &mut Vec<Payload>,
|
|
||||||
withdrawn: &mut Vec<Payload>,
|
|
||||||
) {
|
|
||||||
let current = current
|
|
||||||
.iter()
|
|
||||||
.map(|aspa| (aspa.customer_asn().into_u32(), aspa))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
let next = next
|
|
||||||
.iter()
|
|
||||||
.map(|aspa| (aspa.customer_asn().into_u32(), aspa))
|
|
||||||
.collect::<BTreeMap<_, _>>();
|
|
||||||
|
|
||||||
let customers = current
|
|
||||||
.keys()
|
|
||||||
.chain(next.keys())
|
|
||||||
.copied()
|
|
||||||
.collect::<std::collections::BTreeSet<_>>();
|
|
||||||
|
|
||||||
for customer in customers {
|
|
||||||
match (current.get(&customer), next.get(&customer)) {
|
|
||||||
(None, Some(new_aspa)) => announced.push(Payload::Aspa((*new_aspa).clone())),
|
|
||||||
(Some(old_aspa), None) => withdrawn.push(Payload::Aspa((*old_aspa).clone())),
|
|
||||||
(Some(old_aspa), Some(new_aspa)) if old_aspa != new_aspa => {
|
|
||||||
announced.push(Payload::Aspa((*new_aspa).clone()));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sorted_dedup<T: Ord>(mut items: Vec<T>) -> Vec<T> {
|
|
||||||
items.sort();
|
|
||||||
items.dedup();
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff_sorted<T, F>(
|
|
||||||
current: &[T],
|
|
||||||
next: &[T],
|
|
||||||
announced: &mut Vec<Payload>,
|
|
||||||
withdrawn: &mut Vec<Payload>,
|
|
||||||
wrap: F,
|
|
||||||
) where
|
|
||||||
T: Ord + Clone,
|
|
||||||
F: Fn(T) -> Payload,
|
|
||||||
{
|
|
||||||
let mut i = 0usize;
|
|
||||||
let mut j = 0usize;
|
|
||||||
while i < current.len() && j < next.len() {
|
|
||||||
match current[i].cmp(&next[j]) {
|
|
||||||
std::cmp::Ordering::Less => {
|
|
||||||
withdrawn.push(wrap(current[i].clone()));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Greater => {
|
|
||||||
announced.push(wrap(next[j].clone()));
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Equal => {
|
|
||||||
i += 1;
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while i < current.len() {
|
|
||||||
withdrawn.push(wrap(current[i].clone()));
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
while j < next.len() {
|
|
||||||
announced.push(wrap(next[j].clone()));
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
343
src/rtr/cache/ordering.rs
vendored
343
src/rtr/cache/ordering.rs
vendored
@ -1,343 +0,0 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::data_model::resources::ip_resources::IPAddress;
|
|
||||||
use crate::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub struct OrderingViolation {
|
|
||||||
index: usize,
|
|
||||||
left: String,
|
|
||||||
right: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OrderingViolation {
|
|
||||||
fn new(index: usize, left: &Payload, right: &Payload) -> Self {
|
|
||||||
Self {
|
|
||||||
index,
|
|
||||||
left: payload_brief(left),
|
|
||||||
right: payload_brief(right),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_update(index: usize, left: (bool, &Payload), right: (bool, &Payload)) -> Self {
|
|
||||||
Self {
|
|
||||||
index,
|
|
||||||
left: payload_update_brief(left.0, left.1),
|
|
||||||
right: payload_update_brief(right.0, right.1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for OrderingViolation {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"RTR payload ordering violation at positions {} and {}: {} should not appear before {}",
|
|
||||||
self.index,
|
|
||||||
self.index + 1,
|
|
||||||
self.left,
|
|
||||||
self.right
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub(crate) enum RouterKeyKey {
|
|
||||||
Key { ski: Ski, spki: Vec<u8>, asn: u32 },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub(crate) enum ChangeKey {
|
|
||||||
RouteOrigin(RouteOriginKey),
|
|
||||||
RouterKey(RouterKeyKey),
|
|
||||||
AspaCustomer(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
enum PayloadPduType {
|
|
||||||
Ipv4Prefix = 4,
|
|
||||||
Ipv6Prefix = 6,
|
|
||||||
RouterKey = 9,
|
|
||||||
Aspa = 11,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
|
||||||
pub(crate) enum RouteOriginKey {
|
|
||||||
V4 {
|
|
||||||
addr: u32,
|
|
||||||
plen: u8,
|
|
||||||
mlen: u8,
|
|
||||||
asn: u32,
|
|
||||||
},
|
|
||||||
V6 {
|
|
||||||
addr: u128,
|
|
||||||
plen: u8,
|
|
||||||
mlen: u8,
|
|
||||||
asn: u32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn change_key(payload: &Payload) -> ChangeKey {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(ro) => ChangeKey::RouteOrigin(route_origin_key(ro)),
|
|
||||||
Payload::RouterKey(rk) => ChangeKey::RouterKey(router_key_key(rk)),
|
|
||||||
Payload::Aspa(aspa) => ChangeKey::AspaCustomer(aspa.customer_asn().into_u32()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn compare_payload_update_for_rtr(
|
|
||||||
a_payload: &Payload,
|
|
||||||
a_announce: bool,
|
|
||||||
b_payload: &Payload,
|
|
||||||
b_announce: bool,
|
|
||||||
) -> Ordering {
|
|
||||||
let type_a = payload_pdu_type(a_payload);
|
|
||||||
let type_b = payload_pdu_type(b_payload);
|
|
||||||
|
|
||||||
match type_a.cmp(&type_b) {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
other => return other,
|
|
||||||
}
|
|
||||||
|
|
||||||
match b_announce.cmp(&a_announce) {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
other => return other,
|
|
||||||
}
|
|
||||||
|
|
||||||
match (a_payload, b_payload) {
|
|
||||||
(Payload::RouteOrigin(a), Payload::RouteOrigin(b)) => {
|
|
||||||
compare_route_origin_for_rtr(a, b, a_announce)
|
|
||||||
}
|
|
||||||
(Payload::RouterKey(a), Payload::RouterKey(b)) => compare_router_key_for_rtr(a, b),
|
|
||||||
(Payload::Aspa(a), Payload::Aspa(b)) => compare_aspa_for_rtr(a, b),
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sort_payloads_for_rtr(payloads: &mut [Payload], announce: bool) {
|
|
||||||
payloads.sort_by(|a, b| compare_payload_for_rtr(a, b, announce));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_payloads_for_rtr(
|
|
||||||
payloads: &[Payload],
|
|
||||||
announce: bool,
|
|
||||||
) -> Result<(), OrderingViolation> {
|
|
||||||
for (index, pair) in payloads.windows(2).enumerate() {
|
|
||||||
if compare_payload_for_rtr(&pair[0], &pair[1], announce) == Ordering::Greater {
|
|
||||||
return Err(OrderingViolation::new(index, &pair[0], &pair[1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_payload_updates_for_rtr(
|
|
||||||
updates: &[(bool, Payload)],
|
|
||||||
) -> Result<(), OrderingViolation> {
|
|
||||||
for (index, pair) in updates.windows(2).enumerate() {
|
|
||||||
if compare_payload_update_for_rtr(&pair[0].1, pair[0].0, &pair[1].1, pair[1].0)
|
|
||||||
== Ordering::Greater
|
|
||||||
{
|
|
||||||
return Err(OrderingViolation::new_update(
|
|
||||||
index,
|
|
||||||
(pair[0].0, &pair[0].1),
|
|
||||||
(pair[1].0, &pair[1].1),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_payload_update_refs_for_rtr(
|
|
||||||
updates: &[(bool, &Payload)],
|
|
||||||
) -> Result<(), OrderingViolation> {
|
|
||||||
for (index, pair) in updates.windows(2).enumerate() {
|
|
||||||
if compare_payload_update_for_rtr(pair[0].1, pair[0].0, pair[1].1, pair[1].0)
|
|
||||||
== Ordering::Greater
|
|
||||||
{
|
|
||||||
return Err(OrderingViolation::new_update(
|
|
||||||
index,
|
|
||||||
(pair[0].0, pair[0].1),
|
|
||||||
(pair[1].0, pair[1].1),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn router_key_key(rk: &RouterKey) -> RouterKeyKey {
|
|
||||||
RouterKeyKey::Key {
|
|
||||||
ski: rk.ski(),
|
|
||||||
spki: rk.spki().as_ref().to_vec(),
|
|
||||||
asn: rk.asn().into_u32(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_payload_for_rtr(a: &Payload, b: &Payload, announce: bool) -> Ordering {
|
|
||||||
let type_a = payload_pdu_type(a);
|
|
||||||
let type_b = payload_pdu_type(b);
|
|
||||||
|
|
||||||
match type_a.cmp(&type_b) {
|
|
||||||
Ordering::Equal => {}
|
|
||||||
other => return other,
|
|
||||||
}
|
|
||||||
|
|
||||||
match (a, b) {
|
|
||||||
(Payload::RouteOrigin(a), Payload::RouteOrigin(b)) => {
|
|
||||||
compare_route_origin_for_rtr(a, b, announce)
|
|
||||||
}
|
|
||||||
(Payload::RouterKey(a), Payload::RouterKey(b)) => compare_router_key_for_rtr(a, b),
|
|
||||||
(Payload::Aspa(a), Payload::Aspa(b)) => compare_aspa_for_rtr(a, b),
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn payload_pdu_type(payload: &Payload) -> PayloadPduType {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(ro) => {
|
|
||||||
if route_origin_is_ipv4(ro) {
|
|
||||||
PayloadPduType::Ipv4Prefix
|
|
||||||
} else {
|
|
||||||
PayloadPduType::Ipv6Prefix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Payload::RouterKey(_) => PayloadPduType::RouterKey,
|
|
||||||
Payload::Aspa(_) => PayloadPduType::Aspa,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route_origin_is_ipv4(ro: &RouteOrigin) -> bool {
|
|
||||||
ro.prefix().address.is_ipv4()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn route_origin_key(ro: &RouteOrigin) -> RouteOriginKey {
|
|
||||||
let prefix = ro.prefix();
|
|
||||||
let plen = prefix.prefix_length;
|
|
||||||
let mlen = ro.max_length();
|
|
||||||
let asn = ro.asn().into_u32();
|
|
||||||
|
|
||||||
match prefix.address {
|
|
||||||
IPAddress::V4(addr) => RouteOriginKey::V4 {
|
|
||||||
addr: u32::from(addr),
|
|
||||||
plen,
|
|
||||||
mlen,
|
|
||||||
asn,
|
|
||||||
},
|
|
||||||
IPAddress::V6(addr) => RouteOriginKey::V6 {
|
|
||||||
addr: u128::from(addr),
|
|
||||||
plen,
|
|
||||||
mlen,
|
|
||||||
asn,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_route_origin_for_rtr(a: &RouteOrigin, b: &RouteOrigin, announce: bool) -> Ordering {
|
|
||||||
match (route_origin_key(a), route_origin_key(b)) {
|
|
||||||
(
|
|
||||||
RouteOriginKey::V4 {
|
|
||||||
addr: addr_a,
|
|
||||||
plen: plen_a,
|
|
||||||
mlen: mlen_a,
|
|
||||||
asn: asn_a,
|
|
||||||
},
|
|
||||||
RouteOriginKey::V4 {
|
|
||||||
addr: addr_b,
|
|
||||||
plen: plen_b,
|
|
||||||
mlen: mlen_b,
|
|
||||||
asn: asn_b,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if announce {
|
|
||||||
addr_b
|
|
||||||
.cmp(&addr_a)
|
|
||||||
.then_with(|| mlen_b.cmp(&mlen_a))
|
|
||||||
.then_with(|| plen_b.cmp(&plen_a))
|
|
||||||
.then_with(|| asn_b.cmp(&asn_a))
|
|
||||||
} else {
|
|
||||||
addr_a
|
|
||||||
.cmp(&addr_b)
|
|
||||||
.then_with(|| mlen_a.cmp(&mlen_b))
|
|
||||||
.then_with(|| plen_a.cmp(&plen_b))
|
|
||||||
.then_with(|| asn_a.cmp(&asn_b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(
|
|
||||||
RouteOriginKey::V6 {
|
|
||||||
addr: addr_a,
|
|
||||||
plen: plen_a,
|
|
||||||
mlen: mlen_a,
|
|
||||||
asn: asn_a,
|
|
||||||
},
|
|
||||||
RouteOriginKey::V6 {
|
|
||||||
addr: addr_b,
|
|
||||||
plen: plen_b,
|
|
||||||
mlen: mlen_b,
|
|
||||||
asn: asn_b,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
if announce {
|
|
||||||
addr_b
|
|
||||||
.cmp(&addr_a)
|
|
||||||
.then_with(|| mlen_b.cmp(&mlen_a))
|
|
||||||
.then_with(|| plen_b.cmp(&plen_a))
|
|
||||||
.then_with(|| asn_b.cmp(&asn_a))
|
|
||||||
} else {
|
|
||||||
addr_a
|
|
||||||
.cmp(&addr_b)
|
|
||||||
.then_with(|| mlen_a.cmp(&mlen_b))
|
|
||||||
.then_with(|| plen_a.cmp(&plen_b))
|
|
||||||
.then_with(|| asn_a.cmp(&asn_b))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Ordering::Equal,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_router_key_for_rtr(a: &RouterKey, b: &RouterKey) -> Ordering {
|
|
||||||
a.ski()
|
|
||||||
.cmp(&b.ski())
|
|
||||||
.then_with(|| a.spki().len().cmp(&b.spki().len()))
|
|
||||||
.then_with(|| a.spki().cmp(b.spki()))
|
|
||||||
.then_with(|| a.asn().into_u32().cmp(&b.asn().into_u32()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_aspa_for_rtr(a: &Aspa, b: &Aspa) -> Ordering {
|
|
||||||
a.customer_asn()
|
|
||||||
.into_u32()
|
|
||||||
.cmp(&b.customer_asn().into_u32())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn payload_brief(payload: &Payload) -> String {
|
|
||||||
match payload {
|
|
||||||
Payload::RouteOrigin(origin) => format!(
|
|
||||||
"{} prefix {:?}/{} max={} asn={}",
|
|
||||||
if route_origin_is_ipv4(origin) {
|
|
||||||
"IPv4"
|
|
||||||
} else {
|
|
||||||
"IPv6"
|
|
||||||
},
|
|
||||||
origin.prefix().address,
|
|
||||||
origin.prefix().prefix_length,
|
|
||||||
origin.max_length(),
|
|
||||||
origin.asn().into_u32()
|
|
||||||
),
|
|
||||||
Payload::RouterKey(key) => format!(
|
|
||||||
"RouterKey ski={:02x?} asn={}",
|
|
||||||
key.ski().as_ref(),
|
|
||||||
key.asn().into_u32()
|
|
||||||
),
|
|
||||||
Payload::Aspa(aspa) => format!("ASPA customer_asn={}", aspa.customer_asn().into_u32()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn payload_update_brief(announce: bool, payload: &Payload) -> String {
|
|
||||||
format!(
|
|
||||||
"{} {}",
|
|
||||||
if announce { "announce" } else { "withdraw" },
|
|
||||||
payload_brief(payload)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
233
src/rtr/cache/store.rs
vendored
233
src/rtr/cache/store.rs
vendored
@ -1,233 +0,0 @@
|
|||||||
use std::collections::VecDeque;
|
|
||||||
use std::sync::{Arc, OnceLock};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use tokio::sync::watch;
|
|
||||||
|
|
||||||
use crate::rtr::payload::{Payload, Timing};
|
|
||||||
use crate::rtr::store::RtrStore;
|
|
||||||
|
|
||||||
use super::core::{AppliedUpdate, CacheAvailability, RtrCache, RtrCacheBuilder, SessionIds};
|
|
||||||
use super::model::{Delta, Snapshot};
|
|
||||||
|
|
||||||
const VERSION_COUNT: usize = 3;
|
|
||||||
static STORE_SYNC_WORKER: OnceLock<watch::Sender<Option<StoreSyncJob>>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct StoreSyncJob {
|
|
||||||
store: RtrStore,
|
|
||||||
update: AppliedUpdate,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrCache {
|
|
||||||
pub fn init(
|
|
||||||
self,
|
|
||||||
store: &RtrStore,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timing: Timing,
|
|
||||||
file_loader: impl Fn() -> Result<Vec<Payload>>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
if let Some(cache) =
|
|
||||||
try_restore_from_store(store, max_delta, prune_delta_by_snapshot_size, timing)?
|
|
||||||
{
|
|
||||||
tracing::info!(
|
|
||||||
"RTR cache restored from store: availability={:?}, session_ids={:?}, serials={:?}",
|
|
||||||
cache.availability(),
|
|
||||||
cache.session_ids(),
|
|
||||||
cache.serials()
|
|
||||||
);
|
|
||||||
return Ok(cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::warn!("RTR cache store unavailable or invalid, fallback to file loader");
|
|
||||||
|
|
||||||
let payloads = file_loader()?;
|
|
||||||
let source_snapshot = Snapshot::from_payloads(payloads);
|
|
||||||
let availability = if source_snapshot.is_empty() {
|
|
||||||
CacheAvailability::NoDataAvailable
|
|
||||||
} else {
|
|
||||||
CacheAvailability::Ready
|
|
||||||
};
|
|
||||||
let session_ids = SessionIds::random_distinct();
|
|
||||||
let serial = if source_snapshot.is_empty() { 0 } else { 1 };
|
|
||||||
|
|
||||||
let snapshots = std::array::from_fn(|version| {
|
|
||||||
project_snapshot_for_version(&source_snapshot, version as u8)
|
|
||||||
});
|
|
||||||
let serials = [serial; VERSION_COUNT];
|
|
||||||
let deltas =
|
|
||||||
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
|
||||||
|
|
||||||
tokio::spawn({
|
|
||||||
let store = store.clone();
|
|
||||||
let snapshots_for_store = snapshots.clone();
|
|
||||||
let session_ids_for_store = session_ids.as_array();
|
|
||||||
async move {
|
|
||||||
let deltas_none: [Option<&Delta>; 3] = [None, None, None];
|
|
||||||
let windows_none: [Option<(u32, u32)>; 3] = [None, None, None];
|
|
||||||
let clear = [true, true, true];
|
|
||||||
if let Err(e) = store.save_cache_state_versioned(
|
|
||||||
availability,
|
|
||||||
&snapshots_for_store,
|
|
||||||
&session_ids_for_store,
|
|
||||||
&serials,
|
|
||||||
&deltas_none,
|
|
||||||
&windows_none,
|
|
||||||
&clear,
|
|
||||||
) {
|
|
||||||
tracing::error!("persist cache state failed: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(RtrCacheBuilder::new()
|
|
||||||
.availability(availability)
|
|
||||||
.session_ids(session_ids)
|
|
||||||
.max_delta(max_delta)
|
|
||||||
.prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
|
|
||||||
.timing(timing)
|
|
||||||
.serials(serials)
|
|
||||||
.snapshots(snapshots)
|
|
||||||
.deltas_by_version(deltas)
|
|
||||||
.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self, new_payloads: Vec<Payload>, store: &RtrStore) -> Result<()> {
|
|
||||||
if let Some(update) = self.apply_update(new_payloads)? {
|
|
||||||
spawn_store_sync(store, update);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_with_snapshot(&mut self, snapshot: Snapshot, store: &RtrStore) -> Result<()> {
|
|
||||||
if let Some(update) = self.apply_update_from_snapshot(snapshot)? {
|
|
||||||
spawn_store_sync(store, update);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_runtime_config(
|
|
||||||
&mut self,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timing: Timing,
|
|
||||||
store: &RtrStore,
|
|
||||||
) {
|
|
||||||
if let Some(update) =
|
|
||||||
self.apply_runtime_config(max_delta, prune_delta_by_snapshot_size, timing)
|
|
||||||
{
|
|
||||||
spawn_store_sync(store, update);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn try_restore_from_store(
|
|
||||||
store: &RtrStore,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timing: Timing,
|
|
||||||
) -> Result<Option<RtrCache>> {
|
|
||||||
let availability = store.get_availability()?;
|
|
||||||
|
|
||||||
let mut snapshots = std::array::from_fn(|_| Snapshot::empty());
|
|
||||||
let mut session_ids = [0u16; VERSION_COUNT];
|
|
||||||
let mut serials = [0u32; VERSION_COUNT];
|
|
||||||
let mut deltas =
|
|
||||||
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
|
||||||
|
|
||||||
for version in 0u8..=2 {
|
|
||||||
let idx = version as usize;
|
|
||||||
let snapshot = store.get_snapshot_for_version(version)?;
|
|
||||||
let session_id = store.get_session_id_for_version(version)?;
|
|
||||||
let serial = store.get_serial_for_version(version)?;
|
|
||||||
let (snapshot, session_id, serial) = match (snapshot, session_id, serial) {
|
|
||||||
(Some(snapshot), Some(session_id), Some(serial)) => (snapshot, session_id, serial),
|
|
||||||
_ => return Ok(None),
|
|
||||||
};
|
|
||||||
snapshots[idx] = snapshot;
|
|
||||||
session_ids[idx] = session_id;
|
|
||||||
serials[idx] = serial;
|
|
||||||
|
|
||||||
if availability == Some(CacheAvailability::NoDataAvailable) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((min_serial, max_serial)) = store.get_delta_window_for_version(version)? {
|
|
||||||
let mut loaded =
|
|
||||||
store.load_delta_window_for_version(version, min_serial, max_serial)?;
|
|
||||||
let max_keep = usize::from(max_delta.max(1));
|
|
||||||
if loaded.len() > max_keep {
|
|
||||||
let drop_count = loaded.len() - max_keep;
|
|
||||||
let dropped_serials = loaded
|
|
||||||
.iter()
|
|
||||||
.take(drop_count)
|
|
||||||
.map(Delta::serial)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
loaded.drain(..drop_count);
|
|
||||||
tracing::warn!(
|
|
||||||
"RTR cache restore truncated persisted deltas to max_delta: version={}, max_delta={}, dropped_count={}, dropped_serials={:?}",
|
|
||||||
version,
|
|
||||||
max_delta,
|
|
||||||
drop_count,
|
|
||||||
dropped_serials
|
|
||||||
);
|
|
||||||
}
|
|
||||||
deltas[idx] = loaded.into_iter().map(Arc::new).collect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let availability = availability.unwrap_or(CacheAvailability::Ready);
|
|
||||||
Ok(Some(
|
|
||||||
RtrCacheBuilder::new()
|
|
||||||
.availability(availability)
|
|
||||||
.session_ids(SessionIds::from_array(session_ids))
|
|
||||||
.max_delta(max_delta)
|
|
||||||
.prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
|
|
||||||
.timing(timing)
|
|
||||||
.serials(serials)
|
|
||||||
.snapshots(snapshots)
|
|
||||||
.deltas_by_version(deltas)
|
|
||||||
.build(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_store_sync(store: &RtrStore, update: AppliedUpdate) {
|
|
||||||
let tx = STORE_SYNC_WORKER.get_or_init(|| {
|
|
||||||
let (tx, mut rx) = watch::channel::<Option<StoreSyncJob>>(None);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while rx.changed().await.is_ok() {
|
|
||||||
let Some(job) = rx.borrow().clone() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
persist_update_job(job);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
tx
|
|
||||||
});
|
|
||||||
|
|
||||||
let _ = tx.send_replace(Some(StoreSyncJob {
|
|
||||||
store: store.clone(),
|
|
||||||
update,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn persist_update_job(job: StoreSyncJob) {
|
|
||||||
let delta_refs: [Option<&Delta>; 3] =
|
|
||||||
std::array::from_fn(|idx| job.update.deltas[idx].as_deref());
|
|
||||||
if let Err(e) = job.store.save_cache_state_versioned(
|
|
||||||
job.update.availability,
|
|
||||||
&job.update.snapshots,
|
|
||||||
&job.update.session_ids,
|
|
||||||
&job.update.serials,
|
|
||||||
&delta_refs,
|
|
||||||
&job.update.delta_windows,
|
|
||||||
&job.update.clear_delta_windows,
|
|
||||||
) {
|
|
||||||
tracing::error!("persist cache state failed: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn project_snapshot_for_version(snapshot: &Snapshot, version: u8) -> Snapshot {
|
|
||||||
snapshot.project_for_version(version)
|
|
||||||
}
|
|
||||||
@ -1,615 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
|
|
||||||
use crate::rtr::payload::Timing;
|
|
||||||
use crate::rtr::server::RtrServiceConfig;
|
|
||||||
use crate::rtr::server::ssh::SshAuthMode;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AppConfig {
|
|
||||||
pub enable_tls: bool,
|
|
||||||
pub enable_ssh: bool,
|
|
||||||
pub tcp_addr: SocketAddr,
|
|
||||||
pub tls_addr: SocketAddr,
|
|
||||||
pub ssh_addr: SocketAddr,
|
|
||||||
|
|
||||||
pub db_path: String,
|
|
||||||
pub ccr_dir: String,
|
|
||||||
pub slurm_dir: Option<String>,
|
|
||||||
pub tls_cert_path: String,
|
|
||||||
pub tls_key_path: String,
|
|
||||||
pub tls_client_ca_path: String,
|
|
||||||
pub ssh_host_key_path: String,
|
|
||||||
pub ssh_authorized_keys_path: String,
|
|
||||||
pub ssh_username: String,
|
|
||||||
pub ssh_subsystem_name: String,
|
|
||||||
pub ssh_auth_mode: SshAuthMode,
|
|
||||||
pub ssh_password: Option<String>,
|
|
||||||
|
|
||||||
pub max_delta: u8,
|
|
||||||
pub prune_delta_by_snapshot_size: bool,
|
|
||||||
pub source_refresh_interval: Duration,
|
|
||||||
pub report_dir: String,
|
|
||||||
pub runtime_report_interval: Duration,
|
|
||||||
pub report_history_limit: usize,
|
|
||||||
pub timezone: Tz,
|
|
||||||
pub timing: Timing,
|
|
||||||
pub admin_addr: Option<SocketAddr>,
|
|
||||||
pub admin_token: Option<String>,
|
|
||||||
|
|
||||||
pub service_config: RtrServiceConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct RuntimeConfig {
|
|
||||||
pub max_delta: u8,
|
|
||||||
pub prune_delta_by_snapshot_size: bool,
|
|
||||||
pub source_refresh_interval_seconds: u64,
|
|
||||||
pub runtime_report_interval_seconds: u64,
|
|
||||||
pub report_history_limit: usize,
|
|
||||||
pub timezone: String,
|
|
||||||
pub timing: RuntimeTimingConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct RuntimeTimingConfig {
|
|
||||||
pub refresh: u32,
|
|
||||||
pub retry: u32,
|
|
||||||
pub expire: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
|
||||||
pub struct RuntimeConfigPatch {
|
|
||||||
pub max_delta: Option<u8>,
|
|
||||||
pub prune_delta_by_snapshot_size: Option<bool>,
|
|
||||||
pub source_refresh_interval_seconds: Option<u64>,
|
|
||||||
pub runtime_report_interval_seconds: Option<u64>,
|
|
||||||
pub report_history_limit: Option<usize>,
|
|
||||||
pub timezone: Option<String>,
|
|
||||||
pub timing: Option<RuntimeTimingConfigPatch>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default, Deserialize)]
|
|
||||||
pub struct RuntimeTimingConfigPatch {
|
|
||||||
pub refresh: Option<u32>,
|
|
||||||
pub retry: Option<u32>,
|
|
||||||
pub expire: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AppConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
enable_tls: false,
|
|
||||||
enable_ssh: false,
|
|
||||||
tcp_addr: "0.0.0.0:323".parse().expect("invalid default tcp_addr"),
|
|
||||||
tls_addr: "0.0.0.0:324".parse().expect("invalid default tls_addr"),
|
|
||||||
ssh_addr: "0.0.0.0:22".parse().expect("invalid default ssh_addr"),
|
|
||||||
|
|
||||||
db_path: "./rtr-db".to_string(),
|
|
||||||
ccr_dir: "./data".to_string(),
|
|
||||||
slurm_dir: None,
|
|
||||||
tls_cert_path: "./certs/tls/server-dns.crt".to_string(),
|
|
||||||
tls_key_path: "./certs/tls/server-dns.key".to_string(),
|
|
||||||
tls_client_ca_path: "./certs/tls/client-ca.crt".to_string(),
|
|
||||||
ssh_host_key_path: "./certs/ssh/ssh_host_rsa_key".to_string(),
|
|
||||||
ssh_authorized_keys_path: "./certs/ssh/rtr-authorized_keys".to_string(),
|
|
||||||
ssh_username: "rpki-rtr".to_string(),
|
|
||||||
ssh_subsystem_name: "rpki-rtr".to_string(),
|
|
||||||
ssh_auth_mode: SshAuthMode::Key,
|
|
||||||
ssh_password: None,
|
|
||||||
|
|
||||||
max_delta: 100,
|
|
||||||
prune_delta_by_snapshot_size: false,
|
|
||||||
source_refresh_interval: Duration::from_secs(300),
|
|
||||||
report_dir: "./report".to_string(),
|
|
||||||
runtime_report_interval: Duration::from_secs(300),
|
|
||||||
report_history_limit: 10,
|
|
||||||
timezone: default_timezone(),
|
|
||||||
timing: Timing::default(),
|
|
||||||
admin_addr: None,
|
|
||||||
admin_token: None,
|
|
||||||
|
|
||||||
service_config: RtrServiceConfig {
|
|
||||||
max_connections: 512,
|
|
||||||
max_concurrent_handshakes: 128,
|
|
||||||
notify_queue_size: 1024,
|
|
||||||
tcp_keepalive: Some(Duration::from_secs(60)),
|
|
||||||
warn_insecure_tcp: true,
|
|
||||||
require_tls_server_dns_name_san: false,
|
|
||||||
enforce_tls_client_san_ip_match: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppConfig {
|
|
||||||
pub fn from_env() -> Result<Self> {
|
|
||||||
let mut config = Self::default();
|
|
||||||
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_ENABLE_TLS")? {
|
|
||||||
config.enable_tls = parse_bool(&value, "RPKI_RTR_ENABLE_TLS")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_ENABLE_SSH")? {
|
|
||||||
config.enable_ssh = parse_bool(&value, "RPKI_RTR_ENABLE_SSH")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TCP_ADDR")? {
|
|
||||||
config.tcp_addr = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_TCP_ADDR '{}': {}", value, err))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TLS_ADDR")? {
|
|
||||||
config.tls_addr = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_TLS_ADDR '{}': {}", value, err))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_ADDR")? {
|
|
||||||
config.ssh_addr = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_SSH_ADDR '{}': {}", value, err))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_PORT")? {
|
|
||||||
let port: u16 = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_SSH_PORT '{}': {}", value, err))?;
|
|
||||||
config.ssh_addr.set_port(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_DB_PATH")? {
|
|
||||||
config.db_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_CCR_DIR")? {
|
|
||||||
config.ccr_dir = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SLURM_DIR")? {
|
|
||||||
let value = value.trim();
|
|
||||||
config.slurm_dir = if value.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(value.to_string())
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TLS_CERT_PATH")? {
|
|
||||||
config.tls_cert_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TLS_KEY_PATH")? {
|
|
||||||
config.tls_key_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TLS_CLIENT_CA_PATH")? {
|
|
||||||
config.tls_client_ca_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_HOST_KEY_PATH")? {
|
|
||||||
config.ssh_host_key_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH")? {
|
|
||||||
config.ssh_authorized_keys_path = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_USERNAME")? {
|
|
||||||
config.ssh_username = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_SUBSYSTEM_NAME")? {
|
|
||||||
config.ssh_subsystem_name = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_AUTH_MODE")? {
|
|
||||||
config.ssh_auth_mode = SshAuthMode::parse(&value).ok_or_else(|| {
|
|
||||||
anyhow!(
|
|
||||||
"invalid RPKI_RTR_SSH_AUTH_MODE '{}': expected key|password|both",
|
|
||||||
value
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_SSH_PASSWORD")? {
|
|
||||||
let value = value.trim().to_string();
|
|
||||||
config.ssh_password = if value.is_empty() { None } else { Some(value) };
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_MAX_DELTA")? {
|
|
||||||
let parsed: u8 = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_DELTA '{}': {}", value, err))?;
|
|
||||||
if parsed == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid RPKI_RTR_MAX_DELTA '{}': must be >= 1",
|
|
||||||
value
|
|
||||||
));
|
|
||||||
}
|
|
||||||
config.max_delta = parsed;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")? {
|
|
||||||
config.prune_delta_by_snapshot_size =
|
|
||||||
parse_bool(&value, "RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_REPORT_DIR")? {
|
|
||||||
config.report_dir = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS")? {
|
|
||||||
let secs = parse_positive_u64(&value, "RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS")?;
|
|
||||||
config.runtime_report_interval = Duration::from_secs(secs);
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_REPORT_HISTORY_LIMIT")? {
|
|
||||||
config.report_history_limit =
|
|
||||||
parse_positive_usize(&value, "RPKI_RTR_REPORT_HISTORY_LIMIT")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TIMEZONE")? {
|
|
||||||
config.timezone = parse_timezone(&value, "RPKI_RTR_TIMEZONE")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_ADMIN_ADDR")? {
|
|
||||||
let value = value.trim();
|
|
||||||
if !value.is_empty() {
|
|
||||||
config.admin_addr =
|
|
||||||
Some(value.parse().map_err(|err| {
|
|
||||||
anyhow!("invalid RPKI_RTR_ADMIN_ADDR '{}': {}", value, err)
|
|
||||||
})?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_ADMIN_TOKEN")? {
|
|
||||||
let value = value.trim().to_string();
|
|
||||||
config.admin_token = if value.is_empty() { None } else { Some(value) };
|
|
||||||
}
|
|
||||||
|
|
||||||
let source_refresh_interval_new = env_var("RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
|
|
||||||
let source_refresh_interval_legacy = env_var("RPKI_RTR_REFRESH_INTERVAL_SECS")?;
|
|
||||||
match (
|
|
||||||
source_refresh_interval_new.as_deref(),
|
|
||||||
source_refresh_interval_legacy.as_deref(),
|
|
||||||
) {
|
|
||||||
(Some(new_value), Some(_)) => {
|
|
||||||
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
|
|
||||||
config.source_refresh_interval = Duration::from_secs(secs);
|
|
||||||
warn!(
|
|
||||||
"both RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS and legacy RPKI_RTR_REFRESH_INTERVAL_SECS are set; using RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(Some(new_value), None) => {
|
|
||||||
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
|
|
||||||
config.source_refresh_interval = Duration::from_secs(secs);
|
|
||||||
}
|
|
||||||
(None, Some(legacy_value)) => {
|
|
||||||
let secs = parse_positive_u64(legacy_value, "RPKI_RTR_REFRESH_INTERVAL_SECS")?;
|
|
||||||
config.source_refresh_interval = Duration::from_secs(secs);
|
|
||||||
warn!(
|
|
||||||
"RPKI_RTR_REFRESH_INTERVAL_SECS is deprecated; use RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
(None, None) => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TIMING_REFRESH_SECS")? {
|
|
||||||
config.timing.refresh = parse_positive_u32(&value, "RPKI_RTR_TIMING_REFRESH_SECS")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TIMING_RETRY_SECS")? {
|
|
||||||
config.timing.retry = parse_positive_u32(&value, "RPKI_RTR_TIMING_RETRY_SECS")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TIMING_EXPIRE_SECS")? {
|
|
||||||
config.timing.expire = parse_positive_u32(&value, "RPKI_RTR_TIMING_EXPIRE_SECS")?;
|
|
||||||
}
|
|
||||||
config
|
|
||||||
.timing
|
|
||||||
.validate()
|
|
||||||
.map_err(|err| anyhow!("invalid RTR timing configuration: {}", err))?;
|
|
||||||
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_MAX_CONNECTIONS")? {
|
|
||||||
config.service_config.max_connections = value
|
|
||||||
.parse()
|
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_CONNECTIONS '{}': {}", value, err))?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_MAX_CONCURRENT_HANDSHAKES")? {
|
|
||||||
config.service_config.max_concurrent_handshakes = value.parse().map_err(|err| {
|
|
||||||
anyhow!(
|
|
||||||
"invalid RPKI_RTR_MAX_CONCURRENT_HANDSHAKES '{}': {}",
|
|
||||||
value,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_NOTIFY_QUEUE_SIZE")? {
|
|
||||||
config.service_config.notify_queue_size = value.parse().map_err(|err| {
|
|
||||||
anyhow!("invalid RPKI_RTR_NOTIFY_QUEUE_SIZE '{}': {}", value, err)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_TCP_KEEPALIVE_SECS")? {
|
|
||||||
let secs: u64 = value.parse().map_err(|err| {
|
|
||||||
anyhow!("invalid RPKI_RTR_TCP_KEEPALIVE_SECS '{}': {}", value, err)
|
|
||||||
})?;
|
|
||||||
config.service_config.tcp_keepalive = if secs == 0 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Duration::from_secs(secs))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_WARN_INSECURE_TCP")? {
|
|
||||||
config.service_config.warn_insecure_tcp =
|
|
||||||
parse_bool(&value, "RPKI_RTR_WARN_INSECURE_TCP")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN")? {
|
|
||||||
config.service_config.require_tls_server_dns_name_san =
|
|
||||||
parse_bool(&value, "RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN")?;
|
|
||||||
}
|
|
||||||
if let Some(value) = env_var("RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH")? {
|
|
||||||
config.service_config.enforce_tls_client_san_ip_match =
|
|
||||||
parse_bool(&value, "RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.service_config.max_connections == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid RPKI_RTR_MAX_CONNECTIONS '{}': must be >= 1",
|
|
||||||
config.service_config.max_connections
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.service_config.max_concurrent_handshakes == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid RPKI_RTR_MAX_CONCURRENT_HANDSHAKES '{}': must be >= 1",
|
|
||||||
config.service_config.max_concurrent_handshakes
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.service_config.max_concurrent_handshakes > config.service_config.max_connections {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid handshake/connection limits: RPKI_RTR_MAX_CONCURRENT_HANDSHAKES ({}) must be <= RPKI_RTR_MAX_CONNECTIONS ({})",
|
|
||||||
config.service_config.max_concurrent_handshakes,
|
|
||||||
config.service_config.max_connections
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runtime_config(&self) -> RuntimeConfig {
|
|
||||||
RuntimeConfig {
|
|
||||||
max_delta: self.max_delta,
|
|
||||||
prune_delta_by_snapshot_size: self.prune_delta_by_snapshot_size,
|
|
||||||
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,
|
|
||||||
timezone: format_timezone(self.timezone),
|
|
||||||
timing: RuntimeTimingConfig::from(self.timing),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuntimeConfig {
|
|
||||||
pub fn apply_patch(&self, patch: RuntimeConfigPatch) -> Result<Self> {
|
|
||||||
let mut next = self.clone();
|
|
||||||
if let Some(max_delta) = patch.max_delta {
|
|
||||||
if max_delta == 0 {
|
|
||||||
return Err(anyhow!("invalid max_delta '{}': must be >= 1", max_delta));
|
|
||||||
}
|
|
||||||
next.max_delta = max_delta;
|
|
||||||
}
|
|
||||||
if let Some(value) = patch.prune_delta_by_snapshot_size {
|
|
||||||
next.prune_delta_by_snapshot_size = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = patch.source_refresh_interval_seconds {
|
|
||||||
if value == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid source_refresh_interval_seconds '{}': must be >= 1",
|
|
||||||
value
|
|
||||||
));
|
|
||||||
}
|
|
||||||
next.source_refresh_interval_seconds = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = patch.runtime_report_interval_seconds {
|
|
||||||
if value == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid runtime_report_interval_seconds '{}': must be >= 1",
|
|
||||||
value
|
|
||||||
));
|
|
||||||
}
|
|
||||||
next.runtime_report_interval_seconds = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = patch.report_history_limit {
|
|
||||||
if value == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"invalid report_history_limit '{}': must be >= 1",
|
|
||||||
value
|
|
||||||
));
|
|
||||||
}
|
|
||||||
next.report_history_limit = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = patch.timezone {
|
|
||||||
next.timezone = format_timezone(parse_timezone(&value, "timezone")?);
|
|
||||||
}
|
|
||||||
if let Some(timing) = patch.timing {
|
|
||||||
let mut next_timing = Timing::from(next.timing);
|
|
||||||
if let Some(value) = timing.refresh {
|
|
||||||
next_timing.refresh = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = timing.retry {
|
|
||||||
next_timing.retry = value;
|
|
||||||
}
|
|
||||||
if let Some(value) = timing.expire {
|
|
||||||
next_timing.expire = value;
|
|
||||||
}
|
|
||||||
next_timing
|
|
||||||
.validate()
|
|
||||||
.map_err(|err| anyhow!("invalid timing: {}", err))?;
|
|
||||||
next.timing = RuntimeTimingConfig::from(next_timing);
|
|
||||||
}
|
|
||||||
Ok(next)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timezone(&self) -> Result<Tz> {
|
|
||||||
parse_timezone(&self.timezone, "timezone")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timing(&self) -> Timing {
|
|
||||||
Timing::from(self.timing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Timing> for RuntimeTimingConfig {
|
|
||||||
fn from(timing: Timing) -> Self {
|
|
||||||
Self {
|
|
||||||
refresh: timing.refresh,
|
|
||||||
retry: timing.retry,
|
|
||||||
expire: timing.expire,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RuntimeTimingConfig> for Timing {
|
|
||||||
fn from(timing: RuntimeTimingConfig) -> Self {
|
|
||||||
Self::new(timing.refresh, timing.retry, timing.expire)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log_startup_config(config: &AppConfig) {
|
|
||||||
info!("starting RTR service");
|
|
||||||
info!("db_path={}", config.db_path);
|
|
||||||
info!("tcp_addr={}", config.tcp_addr);
|
|
||||||
info!("tls_enabled={}", config.enable_tls);
|
|
||||||
info!("ssh_enabled={}", config.enable_ssh);
|
|
||||||
|
|
||||||
if config.enable_tls {
|
|
||||||
info!("tls_addr={}", config.tls_addr);
|
|
||||||
info!("tls_cert_path={}", config.tls_cert_path);
|
|
||||||
info!("tls_key_path={}", config.tls_key_path);
|
|
||||||
info!("tls_client_ca_path={}", config.tls_client_ca_path);
|
|
||||||
}
|
|
||||||
if config.enable_ssh {
|
|
||||||
info!("ssh_addr={}", config.ssh_addr);
|
|
||||||
info!("ssh_host_key_path={}", config.ssh_host_key_path);
|
|
||||||
info!(
|
|
||||||
"ssh_authorized_keys_path={}",
|
|
||||||
config.ssh_authorized_keys_path
|
|
||||||
);
|
|
||||||
info!("ssh_username={}", config.ssh_username);
|
|
||||||
info!("ssh_subsystem_name={}", config.ssh_subsystem_name);
|
|
||||||
info!("ssh_auth_mode={}", config.ssh_auth_mode.as_str());
|
|
||||||
info!("ssh_password_enabled={}", config.ssh_password.is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("ccr_dir={}", config.ccr_dir);
|
|
||||||
info!(
|
|
||||||
"slurm_dir={}",
|
|
||||||
config.slurm_dir.as_deref().unwrap_or("disabled")
|
|
||||||
);
|
|
||||||
info!("max_delta={}", config.max_delta);
|
|
||||||
info!("strict_ccr_validation=true");
|
|
||||||
info!(
|
|
||||||
"source_refresh_interval_secs={}",
|
|
||||||
config.source_refresh_interval.as_secs()
|
|
||||||
);
|
|
||||||
info!("report_dir={}", config.report_dir);
|
|
||||||
info!(
|
|
||||||
"runtime_report_interval_secs={}",
|
|
||||||
config.runtime_report_interval.as_secs()
|
|
||||||
);
|
|
||||||
info!("report_history_limit={}", config.report_history_limit);
|
|
||||||
info!("timezone={}", format_timezone(config.timezone));
|
|
||||||
info!("rtr_timing_refresh_secs={}", config.timing.refresh);
|
|
||||||
info!("rtr_timing_retry_secs={}", config.timing.retry);
|
|
||||||
info!("rtr_timing_expire_secs={}", config.timing.expire);
|
|
||||||
info!(
|
|
||||||
"admin_addr={}",
|
|
||||||
config
|
|
||||||
.admin_addr
|
|
||||||
.map(|addr| addr.to_string())
|
|
||||||
.unwrap_or_else(|| "disabled".to_string())
|
|
||||||
);
|
|
||||||
info!("admin_token_enabled={}", config.admin_token.is_some());
|
|
||||||
info!("max_connections={}", config.service_config.max_connections);
|
|
||||||
info!(
|
|
||||||
"max_concurrent_handshakes={}",
|
|
||||||
config.service_config.max_concurrent_handshakes
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"notify_queue_size={}",
|
|
||||||
config.service_config.notify_queue_size
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"tcp_keepalive_secs={}",
|
|
||||||
config
|
|
||||||
.service_config
|
|
||||||
.tcp_keepalive
|
|
||||||
.map(|duration| duration.as_secs().to_string())
|
|
||||||
.unwrap_or_else(|| "disabled".to_string())
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"warn_insecure_tcp={}",
|
|
||||||
config.service_config.warn_insecure_tcp
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"require_tls_server_dns_name_san={}",
|
|
||||||
config.service_config.require_tls_server_dns_name_san
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"enforce_tls_client_san_ip_match={}",
|
|
||||||
config.service_config.enforce_tls_client_san_ip_match
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_timezone() -> Tz {
|
|
||||||
chrono_tz::Asia::Shanghai
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn format_timezone(timezone: Tz) -> String {
|
|
||||||
timezone.name().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn env_var(name: &str) -> Result<Option<String>> {
|
|
||||||
match env::var(name) {
|
|
||||||
Ok(value) => Ok(Some(value)),
|
|
||||||
Err(env::VarError::NotPresent) => Ok(None),
|
|
||||||
Err(err) => Err(anyhow!("failed to read {}: {}", name, err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_bool(value: &str, name: &str) -> Result<bool> {
|
|
||||||
match value.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"1" | "true" | "yes" | "on" => Ok(true),
|
|
||||||
"0" | "false" | "no" | "off" => Ok(false),
|
|
||||||
_ => Err(anyhow!("invalid {} '{}': expected boolean", name, value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_positive_u64(value: &str, name: &str) -> Result<u64> {
|
|
||||||
let parsed = value
|
|
||||||
.parse::<u64>()
|
|
||||||
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
|
|
||||||
if parsed == 0 {
|
|
||||||
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
|
|
||||||
}
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_positive_usize(value: &str, name: &str) -> Result<usize> {
|
|
||||||
let parsed = value
|
|
||||||
.parse::<usize>()
|
|
||||||
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
|
|
||||||
if parsed == 0 {
|
|
||||||
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
|
|
||||||
}
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_positive_u32(value: &str, name: &str) -> Result<u32> {
|
|
||||||
let parsed = value
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
|
|
||||||
if parsed == 0 {
|
|
||||||
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
|
|
||||||
}
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_timezone(value: &str, name: &str) -> Result<Tz> {
|
|
||||||
let value = value.trim();
|
|
||||||
let normalized = match value.to_ascii_lowercase().as_str() {
|
|
||||||
"shanghai" | "beijing" | "peking" => "Asia/Shanghai",
|
|
||||||
"utc" | "z" => "UTC",
|
|
||||||
_ => value,
|
|
||||||
};
|
|
||||||
|
|
||||||
normalized.parse::<Tz>().map_err(|err| {
|
|
||||||
anyhow!(
|
|
||||||
"invalid {} '{}': expected IANA timezone like Asia/Shanghai, Europe/London, America/New_York, or UTC: {}",
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
use std::convert::TryFrom;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
#[repr(u16)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ErrorCode {
|
|
||||||
CorruptData = 0,
|
|
||||||
InternalError = 1,
|
|
||||||
NoDataAvailable = 2,
|
|
||||||
InvalidRequest = 3,
|
|
||||||
UnsupportedProtocolVersion = 4,
|
|
||||||
UnsupportedPduType = 5,
|
|
||||||
WithdrawalOfUnknownRecord = 6,
|
|
||||||
DuplicateAnnouncement = 7,
|
|
||||||
UnexpectedProtocolVersion = 8,
|
|
||||||
AspaProviderListError = 9,
|
|
||||||
TransportFailed = 10,
|
|
||||||
OrderingError = 11,
|
|
||||||
CacheRestart = 12,
|
|
||||||
CacheShutdown = 13,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum ErrorCodeDisposition {
|
|
||||||
Fatal,
|
|
||||||
NonFatal,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorCodeDisposition {
|
|
||||||
#[inline]
|
|
||||||
pub fn is_fatal(self) -> bool {
|
|
||||||
self == Self::Fatal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct ErrorCodeVersionSpec {
|
|
||||||
pub code: ErrorCode,
|
|
||||||
pub min_version: u8,
|
|
||||||
pub max_version: u8,
|
|
||||||
pub disposition: ErrorCodeDisposition,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ErrorCodeVersionSpec {
|
|
||||||
pub fn contains(self, code: ErrorCode, version: u8) -> bool {
|
|
||||||
self.code == code && (self.min_version..=self.max_version).contains(&version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const ERROR_CODE_VERSION_SPECS: &[ErrorCodeVersionSpec] = &[
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::CorruptData,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::InternalError,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::NoDataAvailable,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::NonFatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::InvalidRequest,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::UnsupportedProtocolVersion,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 1,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::UnsupportedProtocolVersion,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::NonFatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::UnsupportedPduType,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::WithdrawalOfUnknownRecord,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::DuplicateAnnouncement,
|
|
||||||
min_version: 0,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::UnexpectedProtocolVersion,
|
|
||||||
min_version: 1,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::AspaProviderListError,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::TransportFailed,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::OrderingError,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::CacheRestart,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::NonFatal,
|
|
||||||
},
|
|
||||||
ErrorCodeVersionSpec {
|
|
||||||
code: ErrorCode::CacheShutdown,
|
|
||||||
min_version: 2,
|
|
||||||
max_version: 2,
|
|
||||||
disposition: ErrorCodeDisposition::Fatal,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
impl ErrorCode {
|
|
||||||
#[inline]
|
|
||||||
pub fn as_u16(self) -> u16 {
|
|
||||||
self as u16
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn description(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
ErrorCode::CorruptData => "Corrupt Data",
|
|
||||||
|
|
||||||
ErrorCode::InternalError => "Internal Error",
|
|
||||||
|
|
||||||
ErrorCode::NoDataAvailable => "No Data Available",
|
|
||||||
|
|
||||||
ErrorCode::InvalidRequest => "Invalid Request",
|
|
||||||
|
|
||||||
ErrorCode::UnsupportedProtocolVersion => "Unsupported Protocol Version",
|
|
||||||
|
|
||||||
ErrorCode::UnsupportedPduType => "Unsupported PDU Type",
|
|
||||||
|
|
||||||
ErrorCode::WithdrawalOfUnknownRecord => "Withdrawal of Unknown Record",
|
|
||||||
|
|
||||||
ErrorCode::DuplicateAnnouncement => "Duplicate Announcement Received",
|
|
||||||
|
|
||||||
ErrorCode::UnexpectedProtocolVersion => "Unexpected Protocol Version",
|
|
||||||
|
|
||||||
ErrorCode::AspaProviderListError => "ASPA Provider List Error",
|
|
||||||
|
|
||||||
ErrorCode::TransportFailed => "Transport Failure",
|
|
||||||
|
|
||||||
ErrorCode::OrderingError => "Ordering Error",
|
|
||||||
|
|
||||||
ErrorCode::CacheRestart => "Cache Restart",
|
|
||||||
|
|
||||||
ErrorCode::CacheShutdown => "Cache Shutdown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_defined_in_version(self, version: u8) -> bool {
|
|
||||||
ERROR_CODE_VERSION_SPECS
|
|
||||||
.iter()
|
|
||||||
.any(|spec| spec.contains(self, version))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disposition_in_version(self, version: u8) -> Option<ErrorCodeDisposition> {
|
|
||||||
ERROR_CODE_VERSION_SPECS
|
|
||||||
.iter()
|
|
||||||
.find(|spec| spec.contains(self, version))
|
|
||||||
.map(|spec| spec.disposition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<u16> for ErrorCode {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
|
||||||
match value {
|
|
||||||
0 => Ok(ErrorCode::CorruptData),
|
|
||||||
1 => Ok(ErrorCode::InternalError),
|
|
||||||
2 => Ok(ErrorCode::NoDataAvailable),
|
|
||||||
3 => Ok(ErrorCode::InvalidRequest),
|
|
||||||
4 => Ok(ErrorCode::UnsupportedProtocolVersion),
|
|
||||||
5 => Ok(ErrorCode::UnsupportedPduType),
|
|
||||||
6 => Ok(ErrorCode::WithdrawalOfUnknownRecord),
|
|
||||||
7 => Ok(ErrorCode::DuplicateAnnouncement),
|
|
||||||
8 => Ok(ErrorCode::UnexpectedProtocolVersion),
|
|
||||||
9 => Ok(ErrorCode::AspaProviderListError),
|
|
||||||
10 => Ok(ErrorCode::TransportFailed),
|
|
||||||
11 => Ok(ErrorCode::OrderingError),
|
|
||||||
12 => Ok(ErrorCode::CacheRestart),
|
|
||||||
13 => Ok(ErrorCode::CacheShutdown),
|
|
||||||
_ => Err(()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ErrorCode {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "{} ({})", self.description(), *self as u16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
pub mod store;
|
|
||||||
@ -1,294 +0,0 @@
|
|||||||
use crate::data_model::resources::as_resources::Asn;
|
|
||||||
use crate::data_model::resources::ip_resources::IPAddressPrefix;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::fmt::Debug;
|
|
||||||
use std::io;
|
|
||||||
use std::time::Duration;
|
|
||||||
use x509_parser::prelude::FromDer;
|
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
|
|
||||||
)]
|
|
||||||
pub struct Ski([u8; 20]);
|
|
||||||
|
|
||||||
impl AsRef<[u8]> for Ski {
|
|
||||||
fn as_ref(&self) -> &[u8] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ski {
|
|
||||||
pub fn from_bytes(bytes: [u8; 20]) -> Self {
|
|
||||||
Self(bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct RouteOrigin {
|
|
||||||
prefix: IPAddressPrefix,
|
|
||||||
max_length: u8,
|
|
||||||
asn: Asn,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouteOrigin {
|
|
||||||
pub fn new(prefix: IPAddressPrefix, max_length: u8, asn: Asn) -> Self {
|
|
||||||
Self {
|
|
||||||
prefix,
|
|
||||||
max_length,
|
|
||||||
asn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prefix(&self) -> &IPAddressPrefix {
|
|
||||||
&self.prefix
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_length(&self) -> u8 {
|
|
||||||
self.max_length
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn asn(&self) -> Asn {
|
|
||||||
self.asn
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct RouterKey {
|
|
||||||
subject_key_identifier: Ski,
|
|
||||||
asn: Asn,
|
|
||||||
subject_public_key_info: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouterKey {
|
|
||||||
pub fn new(subject_key_identifier: Ski, asn: Asn, subject_public_key_info: Vec<u8>) -> Self {
|
|
||||||
Self {
|
|
||||||
subject_key_identifier,
|
|
||||||
asn,
|
|
||||||
subject_public_key_info,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ski(&self) -> Ski {
|
|
||||||
self.subject_key_identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn asn(&self) -> Asn {
|
|
||||||
self.asn
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spki(&self) -> &[u8] {
|
|
||||||
&self.subject_public_key_info
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate(&self) -> Result<(), io::Error> {
|
|
||||||
if self.asn.into_u32() == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"RouterKey ASN must not be AS0",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.subject_public_key_info.is_empty() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"RouterKey SPKI must not be empty",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let (rem, _) =
|
|
||||||
SubjectPublicKeyInfo::from_der(&self.subject_public_key_info).map_err(|err| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!("RouterKey SPKI is not valid DER: {err}"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if !rem.is_empty() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!("RouterKey SPKI DER has trailing bytes: {}", rem.len()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
|
|
||||||
pub struct Aspa {
|
|
||||||
customer_asn: Asn,
|
|
||||||
provider_asns: Vec<Asn>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Aspa {
|
|
||||||
pub fn new(customer_asn: Asn, mut provider_asns: Vec<Asn>) -> Self {
|
|
||||||
provider_asns.sort();
|
|
||||||
provider_asns.dedup();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
customer_asn,
|
|
||||||
provider_asns,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn customer_asn(&self) -> Asn {
|
|
||||||
self.customer_asn
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn provider_asns(&self) -> &[Asn] {
|
|
||||||
&self.provider_asns
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate_announcement(&self) -> Result<(), io::Error> {
|
|
||||||
if self.customer_asn.into_u32() == 0 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"ASPA customer ASN must not be AS0",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.provider_asns.is_empty() {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"ASPA announcement must contain at least one provider ASN",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.provider_asns.iter().any(|asn| asn.into_u32() == 0)
|
|
||||||
&& !(self.provider_asns.len() == 1 && self.provider_asns[0].into_u32() == 0)
|
|
||||||
{
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
"ASPA provider list containing AS0 must be exactly [0]",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
|
||||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
||||||
pub enum Payload {
|
|
||||||
/// A route origin.
|
|
||||||
RouteOrigin(RouteOrigin),
|
|
||||||
|
|
||||||
/// A BGPsec router key.
|
|
||||||
RouterKey(RouterKey),
|
|
||||||
|
|
||||||
/// An ASPA unit.
|
|
||||||
Aspa(Aspa),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Timing
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Timing {
|
|
||||||
/// The number of seconds until a client should refresh its data.
|
|
||||||
pub refresh: u32,
|
|
||||||
|
|
||||||
/// The number of seconds a client whould wait before retrying to connect.
|
|
||||||
pub retry: u32,
|
|
||||||
|
|
||||||
/// The number of secionds before data expires if not refreshed.
|
|
||||||
pub expire: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Timing {
|
|
||||||
pub const MIN_REFRESH: u32 = 1;
|
|
||||||
pub const MAX_REFRESH: u32 = 86_400;
|
|
||||||
pub const MIN_RETRY: u32 = 1;
|
|
||||||
pub const MAX_RETRY: u32 = 7_200;
|
|
||||||
pub const MIN_EXPIRE: u32 = 600;
|
|
||||||
pub const MAX_EXPIRE: u32 = 172_800;
|
|
||||||
|
|
||||||
pub const fn new(refresh: u32, retry: u32, expire: u32) -> Self {
|
|
||||||
Self {
|
|
||||||
refresh,
|
|
||||||
retry,
|
|
||||||
expire,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn validate(self) -> Result<(), io::Error> {
|
|
||||||
if !(Self::MIN_REFRESH..=Self::MAX_REFRESH).contains(&self.refresh) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"refresh interval {} out of range {}..={}",
|
|
||||||
self.refresh,
|
|
||||||
Self::MIN_REFRESH,
|
|
||||||
Self::MAX_REFRESH
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(Self::MIN_RETRY..=Self::MAX_RETRY).contains(&self.retry) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"retry interval {} out of range {}..={}",
|
|
||||||
self.retry,
|
|
||||||
Self::MIN_RETRY,
|
|
||||||
Self::MAX_RETRY
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !(Self::MIN_EXPIRE..=Self::MAX_EXPIRE).contains(&self.expire) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"expire interval {} out of range {}..={}",
|
|
||||||
self.expire,
|
|
||||||
Self::MIN_EXPIRE,
|
|
||||||
Self::MAX_EXPIRE
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.expire <= self.refresh {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"expire interval {} must be greater than refresh interval {}",
|
|
||||||
self.expire, self.refresh
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.expire <= self.retry {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidData,
|
|
||||||
format!(
|
|
||||||
"expire interval {} must be greater than retry interval {}",
|
|
||||||
self.expire, self.retry
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn refresh(self) -> Duration {
|
|
||||||
Duration::from_secs(u64::from(self.refresh))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn retry(self) -> Duration {
|
|
||||||
Duration::from_secs(u64::from(self.retry))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expire(self) -> Duration {
|
|
||||||
Duration::from_secs(u64::from(self.expire))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Timing {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
refresh: 3600,
|
|
||||||
retry: 600,
|
|
||||||
expire: 7200,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1350
src/rtr/pdu.rs
1350
src/rtr/pdu.rs
File diff suppressed because it is too large
Load Diff
@ -1,777 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use chrono_tz::Tz;
|
|
||||||
use serde::Serialize;
|
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::rtr::cache::{CacheAvailability, CacheMemoryStats, SharedRtrCache, VersionReportStats};
|
|
||||||
use crate::rtr::config::{RuntimeConfig, format_timezone};
|
|
||||||
use crate::rtr::server::{RtrNotifier, RtrServiceStats, RtrTransportConnectionCounts};
|
|
||||||
use crate::source::pipeline::{
|
|
||||||
DataQualityReport, FileFingerprint, SourceFingerprint, SourceLoadReport,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
|
||||||
pub struct ReportConfiguration {
|
|
||||||
source_refresh_interval_seconds: u64,
|
|
||||||
runtime_report_interval_seconds: u64,
|
|
||||||
report_history_limit: usize,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timezone: String,
|
|
||||||
timing: TimingReport,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize)]
|
|
||||||
struct TimingReport {
|
|
||||||
refresh: u32,
|
|
||||||
retry: u32,
|
|
||||||
expire: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReportConfiguration {
|
|
||||||
pub fn new(
|
|
||||||
source_refresh_interval_seconds: u64,
|
|
||||||
runtime_report_interval_seconds: u64,
|
|
||||||
report_history_limit: usize,
|
|
||||||
max_delta: u8,
|
|
||||||
prune_delta_by_snapshot_size: bool,
|
|
||||||
timezone: Tz,
|
|
||||||
timing: (u32, u32, u32),
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
source_refresh_interval_seconds,
|
|
||||||
runtime_report_interval_seconds,
|
|
||||||
report_history_limit,
|
|
||||||
max_delta,
|
|
||||||
prune_delta_by_snapshot_size,
|
|
||||||
timezone: format_timezone(timezone),
|
|
||||||
timing: TimingReport {
|
|
||||||
refresh: timing.0,
|
|
||||||
retry: timing.1,
|
|
||||||
expire: timing.2,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn timezone(&self) -> Tz {
|
|
||||||
self.timezone
|
|
||||||
.parse::<Tz>()
|
|
||||||
.expect("serialized timezone should be a valid IANA timezone")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ReportContext {
|
|
||||||
started_at: DateTime<Utc>,
|
|
||||||
started_instant: Instant,
|
|
||||||
configuration: Arc<RwLock<ReportConfiguration>>,
|
|
||||||
runtime: Arc<RwLock<RuntimeReportState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
struct RuntimeReportState {
|
|
||||||
source: Option<SourceLoadReport>,
|
|
||||||
source_fingerprint: Option<SourceFingerprint>,
|
|
||||||
data_quality: Option<DataQualityReport>,
|
|
||||||
refresh: RefreshReport,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
struct RefreshReport {
|
|
||||||
last_attempt_at: Option<DateTime<Utc>>,
|
|
||||||
last_success_at: Option<DateTime<Utc>>,
|
|
||||||
last_changed_at: Option<DateTime<Utc>>,
|
|
||||||
status: &'static str,
|
|
||||||
changed: Option<bool>,
|
|
||||||
duration_ms: Option<u128>,
|
|
||||||
consecutive_failures: u64,
|
|
||||||
last_error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RefreshReportView {
|
|
||||||
last_attempt_at: Option<DateTime<Tz>>,
|
|
||||||
last_success_at: Option<DateTime<Tz>>,
|
|
||||||
last_changed_at: Option<DateTime<Tz>>,
|
|
||||||
status: &'static str,
|
|
||||||
changed: Option<bool>,
|
|
||||||
duration_ms: Option<u128>,
|
|
||||||
consecutive_failures: u64,
|
|
||||||
last_error: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RefreshReportView {
|
|
||||||
fn from_report(report: RefreshReport, timezone: Tz) -> Self {
|
|
||||||
Self {
|
|
||||||
last_attempt_at: report
|
|
||||||
.last_attempt_at
|
|
||||||
.map(|time| to_report_time(time, timezone)),
|
|
||||||
last_success_at: report
|
|
||||||
.last_success_at
|
|
||||||
.map(|time| to_report_time(time, timezone)),
|
|
||||||
last_changed_at: report
|
|
||||||
.last_changed_at
|
|
||||||
.map(|time| to_report_time(time, timezone)),
|
|
||||||
status: report.status,
|
|
||||||
changed: report.changed,
|
|
||||||
duration_ms: report.duration_ms,
|
|
||||||
consecutive_failures: report.consecutive_failures,
|
|
||||||
last_error: report.last_error,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RefreshReport {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
last_attempt_at: None,
|
|
||||||
last_success_at: None,
|
|
||||||
last_changed_at: None,
|
|
||||||
status: "not_attempted",
|
|
||||||
changed: None,
|
|
||||||
duration_ms: None,
|
|
||||||
consecutive_failures: 0,
|
|
||||||
last_error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReportContext {
|
|
||||||
pub fn new(configuration: ReportConfiguration) -> Self {
|
|
||||||
Self {
|
|
||||||
started_at: Utc::now(),
|
|
||||||
started_instant: Instant::now(),
|
|
||||||
configuration: Arc::new(RwLock::new(configuration)),
|
|
||||||
runtime: Arc::new(RwLock::new(RuntimeReportState::default())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_runtime_config(&self, config: &RuntimeConfig) {
|
|
||||||
let timezone = config
|
|
||||||
.timezone()
|
|
||||||
.expect("runtime config timezone should be validated before report update");
|
|
||||||
let mut configuration = self
|
|
||||||
.configuration
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
*configuration = ReportConfiguration::new(
|
|
||||||
config.source_refresh_interval_seconds,
|
|
||||||
config.runtime_report_interval_seconds,
|
|
||||||
config.report_history_limit,
|
|
||||||
config.max_delta,
|
|
||||||
config.prune_delta_by_snapshot_size,
|
|
||||||
timezone,
|
|
||||||
(
|
|
||||||
config.timing.refresh,
|
|
||||||
config.timing.retry,
|
|
||||||
config.timing.expire,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_refresh_success(
|
|
||||||
&self,
|
|
||||||
attempted_at: DateTime<Utc>,
|
|
||||||
duration_ms: u128,
|
|
||||||
changed: bool,
|
|
||||||
source: SourceLoadReport,
|
|
||||||
data_quality: DataQualityReport,
|
|
||||||
) {
|
|
||||||
let mut state = self
|
|
||||||
.runtime
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
state.source = Some(source);
|
|
||||||
state.data_quality = Some(data_quality);
|
|
||||||
state.refresh.last_attempt_at = Some(attempted_at);
|
|
||||||
state.refresh.last_success_at = Some(Utc::now());
|
|
||||||
if changed {
|
|
||||||
state.refresh.last_changed_at = Some(Utc::now());
|
|
||||||
}
|
|
||||||
state.refresh.status = "success";
|
|
||||||
state.refresh.changed = Some(changed);
|
|
||||||
state.refresh.duration_ms = Some(duration_ms);
|
|
||||||
state.refresh.consecutive_failures = 0;
|
|
||||||
state.refresh.last_error = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_source_fingerprint(&self, fingerprint: SourceFingerprint) {
|
|
||||||
let mut state = self
|
|
||||||
.runtime
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
state.source_fingerprint = Some(fingerprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_refresh_unchanged(&self, attempted_at: DateTime<Utc>, duration_ms: u128) {
|
|
||||||
let mut state = self
|
|
||||||
.runtime
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
state.refresh.last_attempt_at = Some(attempted_at);
|
|
||||||
state.refresh.last_success_at = Some(Utc::now());
|
|
||||||
state.refresh.status = "success";
|
|
||||||
state.refresh.changed = Some(false);
|
|
||||||
state.refresh.duration_ms = Some(duration_ms);
|
|
||||||
state.refresh.consecutive_failures = 0;
|
|
||||||
state.refresh.last_error = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_refresh_failure(
|
|
||||||
&self,
|
|
||||||
attempted_at: DateTime<Utc>,
|
|
||||||
duration_ms: u128,
|
|
||||||
error: &anyhow::Error,
|
|
||||||
) {
|
|
||||||
let mut state = self
|
|
||||||
.runtime
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
||||||
state.refresh.last_attempt_at = Some(attempted_at);
|
|
||||||
state.refresh.status = "failed";
|
|
||||||
state.refresh.changed = None;
|
|
||||||
state.refresh.duration_ms = Some(duration_ms);
|
|
||||||
state.refresh.consecutive_failures = state.refresh.consecutive_failures.saturating_add(1);
|
|
||||||
state.refresh.last_error = Some(error.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_or_warn(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) {
|
|
||||||
if let Err(err) = self.write(report_dir, phase, shared_cache, notifier, service_stats) {
|
|
||||||
warn!(
|
|
||||||
"failed to write RTR report to {}: {:?}",
|
|
||||||
report_dir.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_source_or_warn(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) {
|
|
||||||
if let Err(err) =
|
|
||||||
self.write_source(report_dir, phase, shared_cache, notifier, service_stats)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"failed to write RTR source report to {}: {:?}",
|
|
||||||
report_dir.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_clients_or_warn(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) {
|
|
||||||
if let Err(err) =
|
|
||||||
self.write_clients(report_dir, phase, shared_cache, notifier, service_stats)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"failed to write RTR clients report to {}: {:?}",
|
|
||||||
report_dir.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write_runtime_or_warn(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) {
|
|
||||||
if let Err(err) =
|
|
||||||
self.write_runtime(report_dir, phase, shared_cache, notifier, service_stats)
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"failed to write RTR runtime report to {}: {:?}",
|
|
||||||
report_dir.display(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) -> Result<()> {
|
|
||||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
|
||||||
fs::create_dir_all(report_dir)
|
|
||||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
|
||||||
self.write_source_report(report_dir, &parts.suffix, &parts.source)?;
|
|
||||||
self.write_clients_report(report_dir, &parts.suffix, &parts.clients)?;
|
|
||||||
self.write_runtime_report(report_dir, &parts.suffix, &parts.runtime)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_source(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) -> Result<()> {
|
|
||||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
|
||||||
fs::create_dir_all(report_dir)
|
|
||||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
|
||||||
self.write_source_report(report_dir, &parts.suffix, &parts.source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_clients(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) -> Result<()> {
|
|
||||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
|
||||||
fs::create_dir_all(report_dir)
|
|
||||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
|
||||||
self.write_clients_report(report_dir, &parts.suffix, &parts.clients)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_runtime(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) -> Result<()> {
|
|
||||||
let parts = self.build_reports(phase, shared_cache, notifier, service_stats);
|
|
||||||
fs::create_dir_all(report_dir)
|
|
||||||
.with_context(|| format!("create report directory {}", report_dir.display()))?;
|
|
||||||
self.write_runtime_report(report_dir, &parts.suffix, &parts.runtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_reports(
|
|
||||||
&self,
|
|
||||||
phase: &str,
|
|
||||||
shared_cache: &SharedRtrCache,
|
|
||||||
notifier: &RtrNotifier,
|
|
||||||
service_stats: &RtrServiceStats,
|
|
||||||
) -> ReportParts {
|
|
||||||
let cache = shared_cache.load_full();
|
|
||||||
let configuration = self
|
|
||||||
.configuration
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.clone();
|
|
||||||
let timezone = configuration.timezone();
|
|
||||||
let runtime = self
|
|
||||||
.runtime
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.clone();
|
|
||||||
let availability = match cache.availability() {
|
|
||||||
CacheAvailability::Ready => "ready",
|
|
||||||
CacheAvailability::NoDataAvailable => "no_data_available",
|
|
||||||
};
|
|
||||||
let active_connections = service_stats.active_connections();
|
|
||||||
let connections_by_transport = service_stats.transport_connections();
|
|
||||||
let max_connections = service_stats.max_connections();
|
|
||||||
let generated_at = self.report_now(timezone);
|
|
||||||
let metadata = ReportMetadata {
|
|
||||||
schema_version: 1,
|
|
||||||
generated_at,
|
|
||||||
phase: phase.to_string(),
|
|
||||||
};
|
|
||||||
let service = ServiceReport {
|
|
||||||
started_at: self.to_report_time(self.started_at, timezone),
|
|
||||||
uptime_seconds: self.started_instant.elapsed().as_secs(),
|
|
||||||
active_connections,
|
|
||||||
connections_by_transport: TransportConnectionReport::from(connections_by_transport),
|
|
||||||
session_listeners: notifier.listener_count(),
|
|
||||||
max_connections,
|
|
||||||
connection_utilization: active_connections as f64 / max_connections as f64,
|
|
||||||
};
|
|
||||||
let process = ProcessReport {
|
|
||||||
rss_mib: current_rss_mib(),
|
|
||||||
};
|
|
||||||
let source = runtime
|
|
||||||
.source
|
|
||||||
.map(|source| SourceLoadReportView::from_report(source, timezone));
|
|
||||||
let refresh = RefreshReportView::from_report(runtime.refresh, timezone);
|
|
||||||
let cache = CacheReport {
|
|
||||||
availability,
|
|
||||||
created_at: self.to_report_time(cache.created_at().utc(), timezone),
|
|
||||||
last_update_begin: self.to_report_time(cache.last_update_begin().utc(), timezone),
|
|
||||||
last_update_end: self.to_report_time(cache.last_update_end().utc(), timezone),
|
|
||||||
memory: cache.memory_stats(),
|
|
||||||
versions: cache.version_report_stats(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let source_report = SourceReport {
|
|
||||||
schema_version: metadata.schema_version,
|
|
||||||
generated_at: metadata.generated_at,
|
|
||||||
phase: metadata.phase.clone(),
|
|
||||||
source,
|
|
||||||
source_fingerprint: runtime.source_fingerprint.map(|fingerprint| {
|
|
||||||
SourceFingerprintReport::from_fingerprint(fingerprint, timezone)
|
|
||||||
}),
|
|
||||||
refresh,
|
|
||||||
data_quality: runtime.data_quality,
|
|
||||||
cache,
|
|
||||||
};
|
|
||||||
let clients_report = ClientsReport {
|
|
||||||
schema_version: metadata.schema_version,
|
|
||||||
generated_at: metadata.generated_at,
|
|
||||||
phase: metadata.phase.clone(),
|
|
||||||
service: ServiceReport {
|
|
||||||
started_at: service.started_at,
|
|
||||||
uptime_seconds: service.uptime_seconds,
|
|
||||||
active_connections: service.active_connections,
|
|
||||||
connections_by_transport: service.connections_by_transport,
|
|
||||||
session_listeners: service.session_listeners,
|
|
||||||
max_connections: service.max_connections,
|
|
||||||
connection_utilization: service.connection_utilization,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let runtime_report = RuntimeReport {
|
|
||||||
schema_version: metadata.schema_version,
|
|
||||||
generated_at: metadata.generated_at,
|
|
||||||
phase: metadata.phase,
|
|
||||||
service,
|
|
||||||
process,
|
|
||||||
configuration,
|
|
||||||
};
|
|
||||||
let suffix = report_file_suffix(generated_at);
|
|
||||||
|
|
||||||
ReportParts {
|
|
||||||
suffix,
|
|
||||||
source: source_report,
|
|
||||||
clients: clients_report,
|
|
||||||
runtime: runtime_report,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_source_report(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
suffix: &str,
|
|
||||||
report: &SourceReport,
|
|
||||||
) -> Result<()> {
|
|
||||||
write_rolling_report(
|
|
||||||
report_dir,
|
|
||||||
"rtr-source",
|
|
||||||
suffix,
|
|
||||||
self.report_history_limit(),
|
|
||||||
report,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_clients_report(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
suffix: &str,
|
|
||||||
report: &ClientsReport,
|
|
||||||
) -> Result<()> {
|
|
||||||
write_rolling_report(
|
|
||||||
report_dir,
|
|
||||||
"rtr-clients",
|
|
||||||
suffix,
|
|
||||||
self.report_history_limit(),
|
|
||||||
report,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_runtime_report(
|
|
||||||
&self,
|
|
||||||
report_dir: &Path,
|
|
||||||
suffix: &str,
|
|
||||||
report: &RuntimeReport,
|
|
||||||
) -> Result<()> {
|
|
||||||
write_rolling_report(
|
|
||||||
report_dir,
|
|
||||||
"rtr-runtime",
|
|
||||||
suffix,
|
|
||||||
self.report_history_limit(),
|
|
||||||
report,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_history_limit(&self) -> usize {
|
|
||||||
self.configuration
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
||||||
.report_history_limit
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_now(&self, timezone: Tz) -> DateTime<Tz> {
|
|
||||||
self.to_report_time(Utc::now(), timezone)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_report_time(&self, time: DateTime<Utc>, timezone: Tz) -> DateTime<Tz> {
|
|
||||||
time.with_timezone(&timezone)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
|
||||||
struct ReportMetadata {
|
|
||||||
schema_version: u16,
|
|
||||||
generated_at: DateTime<Tz>,
|
|
||||||
phase: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ReportParts {
|
|
||||||
suffix: String,
|
|
||||||
source: SourceReport,
|
|
||||||
clients: ClientsReport,
|
|
||||||
runtime: RuntimeReport,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SourceReport {
|
|
||||||
schema_version: u16,
|
|
||||||
generated_at: DateTime<Tz>,
|
|
||||||
phase: String,
|
|
||||||
source: Option<SourceLoadReportView>,
|
|
||||||
source_fingerprint: Option<SourceFingerprintReport>,
|
|
||||||
refresh: RefreshReportView,
|
|
||||||
data_quality: Option<DataQualityReport>,
|
|
||||||
cache: CacheReport,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ClientsReport {
|
|
||||||
schema_version: u16,
|
|
||||||
generated_at: DateTime<Tz>,
|
|
||||||
phase: String,
|
|
||||||
service: ServiceReport,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct RuntimeReport {
|
|
||||||
schema_version: u16,
|
|
||||||
generated_at: DateTime<Tz>,
|
|
||||||
phase: String,
|
|
||||||
service: ServiceReport,
|
|
||||||
process: ProcessReport,
|
|
||||||
configuration: ReportConfiguration,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize)]
|
|
||||||
struct ServiceReport {
|
|
||||||
started_at: DateTime<Tz>,
|
|
||||||
uptime_seconds: u64,
|
|
||||||
active_connections: usize,
|
|
||||||
connections_by_transport: TransportConnectionReport,
|
|
||||||
session_listeners: usize,
|
|
||||||
max_connections: usize,
|
|
||||||
connection_utilization: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SourceLoadReportView {
|
|
||||||
ccr_file: String,
|
|
||||||
ccr_file_size_bytes: u64,
|
|
||||||
ccr_modified_at: Option<DateTime<Tz>>,
|
|
||||||
ccr_produced_at: Option<String>,
|
|
||||||
slurm_enabled: bool,
|
|
||||||
slurm_file_count: usize,
|
|
||||||
slurm_files: Vec<String>,
|
|
||||||
slurm_version: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SourceFingerprintReport {
|
|
||||||
ccr: FileFingerprintReport,
|
|
||||||
slurm_files: Vec<FileFingerprintReport>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceFingerprintReport {
|
|
||||||
fn from_fingerprint(fingerprint: SourceFingerprint, timezone: Tz) -> Self {
|
|
||||||
Self {
|
|
||||||
ccr: FileFingerprintReport::from_fingerprint(fingerprint.ccr, timezone),
|
|
||||||
slurm_files: fingerprint
|
|
||||||
.slurm_files
|
|
||||||
.into_iter()
|
|
||||||
.map(|fingerprint| FileFingerprintReport::from_fingerprint(fingerprint, timezone))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct FileFingerprintReport {
|
|
||||||
path: String,
|
|
||||||
len: u64,
|
|
||||||
modified_unix_secs: u64,
|
|
||||||
modified_at: Option<DateTime<Tz>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileFingerprintReport {
|
|
||||||
fn from_fingerprint(fingerprint: FileFingerprint, timezone: Tz) -> Self {
|
|
||||||
Self {
|
|
||||||
path: fingerprint.path,
|
|
||||||
len: fingerprint.len,
|
|
||||||
modified_unix_secs: fingerprint.modified_unix_secs,
|
|
||||||
modified_at: DateTime::from_timestamp(fingerprint.modified_unix_secs as i64, 0)
|
|
||||||
.map(|time| to_report_time(time, timezone)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceLoadReportView {
|
|
||||||
fn from_report(report: SourceLoadReport, timezone: Tz) -> Self {
|
|
||||||
Self {
|
|
||||||
ccr_file: report.ccr_file,
|
|
||||||
ccr_file_size_bytes: report.ccr_file_size_bytes,
|
|
||||||
ccr_modified_at: report
|
|
||||||
.ccr_modified_at
|
|
||||||
.map(|time| to_report_time(time, timezone)),
|
|
||||||
ccr_produced_at: report.ccr_produced_at,
|
|
||||||
slurm_enabled: report.slurm_enabled,
|
|
||||||
slurm_file_count: report.slurm_file_count,
|
|
||||||
slurm_files: report.slurm_files,
|
|
||||||
slurm_version: report.slurm_version,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize)]
|
|
||||||
struct TransportConnectionReport {
|
|
||||||
tcp: usize,
|
|
||||||
tls: usize,
|
|
||||||
ssh: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<RtrTransportConnectionCounts> for TransportConnectionReport {
|
|
||||||
fn from(counts: RtrTransportConnectionCounts) -> Self {
|
|
||||||
Self {
|
|
||||||
tcp: counts.tcp,
|
|
||||||
tls: counts.tls,
|
|
||||||
ssh: counts.ssh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct ProcessReport {
|
|
||||||
rss_mib: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct CacheReport {
|
|
||||||
availability: &'static str,
|
|
||||||
created_at: DateTime<Tz>,
|
|
||||||
last_update_begin: DateTime<Tz>,
|
|
||||||
last_update_end: DateTime<Tz>,
|
|
||||||
memory: CacheMemoryStats,
|
|
||||||
versions: [VersionReportStats; 3],
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_report_time(time: DateTime<Utc>, timezone: Tz) -> DateTime<Tz> {
|
|
||||||
time.with_timezone(&timezone)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_rss_mib() -> Option<u64> {
|
|
||||||
let status = fs::read_to_string("/proc/self/status").ok()?;
|
|
||||||
let vmrss_line = status.lines().find(|line| line.starts_with("VmRSS:"))?;
|
|
||||||
let kb = vmrss_line
|
|
||||||
.split_whitespace()
|
|
||||||
.nth(1)
|
|
||||||
.and_then(|value| value.parse::<u64>().ok())?;
|
|
||||||
Some(kb / 1024)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_file_suffix(time: DateTime<Tz>) -> String {
|
|
||||||
time.format("%Y%m%d%H%M%S%9f").to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_rolling_report<T: Serialize>(
|
|
||||||
report_dir: &Path,
|
|
||||||
prefix: &str,
|
|
||||||
suffix: &str,
|
|
||||||
keep: usize,
|
|
||||||
report: &T,
|
|
||||||
) -> Result<()> {
|
|
||||||
let file_name = format!("{prefix}-{suffix}.json");
|
|
||||||
let target = report_dir.join(&file_name);
|
|
||||||
let temporary = report_dir.join(format!(".{file_name}.tmp"));
|
|
||||||
let json = serde_json::to_vec_pretty(report)
|
|
||||||
.with_context(|| format!("serialize {prefix} RTR report"))?;
|
|
||||||
fs::write(&temporary, json)
|
|
||||||
.with_context(|| format!("write temporary report {}", temporary.display()))?;
|
|
||||||
replace_file(&temporary, &target)?;
|
|
||||||
prune_rolling_reports(report_dir, prefix, keep)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_file(temporary: &Path, target: &Path) -> Result<()> {
|
|
||||||
if let Err(err) = fs::rename(temporary, target) {
|
|
||||||
if target.exists() {
|
|
||||||
fs::remove_file(target)
|
|
||||||
.with_context(|| format!("replace existing report {}", target.display()))?;
|
|
||||||
fs::rename(temporary, target)
|
|
||||||
.with_context(|| format!("move report into {}", target.display()))?;
|
|
||||||
} else {
|
|
||||||
return Err(err).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"move temporary report {} into {}",
|
|
||||||
temporary.display(),
|
|
||||||
target.display()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prune_rolling_reports(report_dir: &Path, prefix: &str, keep: usize) -> Result<()> {
|
|
||||||
let start = format!("{prefix}-");
|
|
||||||
let mut files = Vec::<PathBuf>::new();
|
|
||||||
for entry in fs::read_dir(report_dir)
|
|
||||||
.with_context(|| format!("read report directory {}", report_dir.display()))?
|
|
||||||
{
|
|
||||||
let entry =
|
|
||||||
entry.with_context(|| format!("iterate report directory {}", report_dir.display()))?;
|
|
||||||
let path = entry.path();
|
|
||||||
if !path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if name.starts_with(&start) && name.ends_with(".json") {
|
|
||||||
files.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
files.sort();
|
|
||||||
let remove_count = files.len().saturating_sub(keep);
|
|
||||||
for path in files.into_iter().take(remove_count) {
|
|
||||||
fs::remove_file(&path)
|
|
||||||
.with_context(|| format!("remove old rolling report {}", path.display()))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,394 +0,0 @@
|
|||||||
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,
|
|
||||||
¬ifier,
|
|
||||||
&service_stats,
|
|
||||||
);
|
|
||||||
report_context.write_clients_or_warn(
|
|
||||||
&report_dir,
|
|
||||||
"startup",
|
|
||||||
&shared_cache,
|
|
||||||
¬ifier,
|
|
||||||
&service_stats,
|
|
||||||
);
|
|
||||||
report_context.write_runtime_or_warn(
|
|
||||||
&report_dir,
|
|
||||||
"startup",
|
|
||||||
&shared_cache,
|
|
||||||
¬ifier,
|
|
||||||
&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, ¬ifier, &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, ¬ifier, &service_stats);
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
warn!("RTR client connection change channel closed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_ = runtime_interval.tick() => {
|
|
||||||
log_cache_memory_stats("periodic_observe", &shared_cache, ¬ifier);
|
|
||||||
report_context.write_runtime_or_warn(&report_dir, "runtime_periodic", &shared_cache, ¬ifier, &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,
|
|
||||||
¬ifier,
|
|
||||||
&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,
|
|
||||||
¬ifier,
|
|
||||||
&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(¤t_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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RtrServiceConfig {
|
|
||||||
pub max_connections: usize,
|
|
||||||
pub max_concurrent_handshakes: usize,
|
|
||||||
pub notify_queue_size: usize,
|
|
||||||
pub tcp_keepalive: Option<Duration>,
|
|
||||||
pub warn_insecure_tcp: bool,
|
|
||||||
pub require_tls_server_dns_name_san: bool,
|
|
||||||
pub enforce_tls_client_san_ip_match: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RtrServiceConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
max_connections: 1024,
|
|
||||||
max_concurrent_handshakes: 128,
|
|
||||||
notify_queue_size: 1024,
|
|
||||||
tcp_keepalive: Some(Duration::from_secs(60)),
|
|
||||||
warn_insecure_tcp: true,
|
|
||||||
require_tls_server_dns_name_san: false,
|
|
||||||
enforce_tls_client_san_ip_match: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,240 +0,0 @@
|
|||||||
use std::net::{IpAddr, SocketAddr};
|
|
||||||
use std::sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio::sync::{OwnedSemaphorePermit, broadcast, watch};
|
|
||||||
use tracing::{info, warn};
|
|
||||||
use x509_parser::extensions::GeneralName;
|
|
||||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
|
||||||
|
|
||||||
use tokio_rustls::TlsAcceptor;
|
|
||||||
|
|
||||||
use crate::rtr::cache::SharedRtrCache;
|
|
||||||
use crate::rtr::server::service::RtrServiceControl;
|
|
||||||
use crate::rtr::session::RtrSession;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum RtrTransportKind {
|
|
||||||
Tcp,
|
|
||||||
Tls,
|
|
||||||
Ssh,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct RtrTransportConnectionCounts {
|
|
||||||
pub tcp: usize,
|
|
||||||
pub tls: usize,
|
|
||||||
pub ssh: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RtrTransportConnectionCounters {
|
|
||||||
tcp: AtomicUsize,
|
|
||||||
tls: AtomicUsize,
|
|
||||||
ssh: AtomicUsize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrTransportConnectionCounters {
|
|
||||||
pub fn increment(&self, transport: RtrTransportKind) {
|
|
||||||
self.counter(transport).fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn decrement(&self, transport: RtrTransportKind) {
|
|
||||||
self.counter(transport).fetch_sub(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot(&self) -> RtrTransportConnectionCounts {
|
|
||||||
RtrTransportConnectionCounts {
|
|
||||||
tcp: self.tcp.load(Ordering::Relaxed),
|
|
||||||
tls: self.tls.load(Ordering::Relaxed),
|
|
||||||
ssh: self.ssh.load(Ordering::Relaxed),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn counter(&self, transport: RtrTransportKind) -> &AtomicUsize {
|
|
||||||
match transport {
|
|
||||||
RtrTransportKind::Tcp => &self.tcp,
|
|
||||||
RtrTransportKind::Tls => &self.tls,
|
|
||||||
RtrTransportKind::Ssh => &self.ssh,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectionGuard {
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
connection_change_seq: Arc<AtomicU64>,
|
|
||||||
transport: RtrTransportKind,
|
|
||||||
_permit: OwnedSemaphorePermit,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectionGuard {
|
|
||||||
pub fn new(
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
connection_change_seq: Arc<AtomicU64>,
|
|
||||||
transport: RtrTransportKind,
|
|
||||||
permit: OwnedSemaphorePermit,
|
|
||||||
) -> Self {
|
|
||||||
active_connections.fetch_add(1, Ordering::Relaxed);
|
|
||||||
transport_connections.increment(transport);
|
|
||||||
notify_connection_change(&connection_change_tx, &connection_change_seq);
|
|
||||||
Self {
|
|
||||||
active_connections,
|
|
||||||
transport_connections,
|
|
||||||
connection_change_tx,
|
|
||||||
connection_change_seq,
|
|
||||||
transport,
|
|
||||||
_permit: permit,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_count(&self) -> usize {
|
|
||||||
self.active_connections.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ConnectionGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.active_connections.fetch_sub(1, Ordering::Relaxed);
|
|
||||||
self.transport_connections.decrement(self.transport);
|
|
||||||
notify_connection_change(&self.connection_change_tx, &self.connection_change_seq);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn notify_connection_change(tx: &watch::Sender<u64>, seq: &AtomicU64) {
|
|
||||||
let next = seq.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
|
|
||||||
let _ = tx.send_replace(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_tcp_connection(
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
notify_rx: broadcast::Receiver<()>,
|
|
||||||
shutdown_rx: watch::Receiver<RtrServiceControl>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
|
|
||||||
session.run().await?;
|
|
||||||
|
|
||||||
info!("RTR TCP session completed normally for {}", peer_addr);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_tls_connection(
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
acceptor: TlsAcceptor,
|
|
||||||
enforce_client_san_ip_match: bool,
|
|
||||||
notify_rx: broadcast::Receiver<()>,
|
|
||||||
shutdown_rx: watch::Receiver<RtrServiceControl>,
|
|
||||||
handshake_permit: Option<OwnedSemaphorePermit>,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("RTR TLS handshake started for {}", peer_addr);
|
|
||||||
let tls_stream = acceptor
|
|
||||||
.accept(stream)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("TLS handshake failed for {}", peer_addr))?;
|
|
||||||
info!("RTR TLS handshake completed for {}", peer_addr);
|
|
||||||
match verify_peer_certificate_ip(&tls_stream, peer_addr.ip()) {
|
|
||||||
Ok(()) => info!(
|
|
||||||
"RTR TLS client certificate SAN IP validated for {}",
|
|
||||||
peer_addr
|
|
||||||
),
|
|
||||||
Err(err) => {
|
|
||||||
if enforce_client_san_ip_match {
|
|
||||||
return Err(err).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"TLS client certificate SAN IP validation failed for {}",
|
|
||||||
peer_addr
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
warn!(
|
|
||||||
"RTR TLS client certificate SAN IP validation failed but allowed by configuration for {}: {}",
|
|
||||||
peer_addr, err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
drop(handshake_permit);
|
|
||||||
|
|
||||||
let session = RtrSession::new(cache, tls_stream, notify_rx, shutdown_rx);
|
|
||||||
session.run().await?;
|
|
||||||
|
|
||||||
info!("RTR TLS session completed normally for {}", peer_addr);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_expected_disconnect(err: &anyhow::Error) -> bool {
|
|
||||||
err.chain().any(|cause| {
|
|
||||||
if let Some(io_err) = cause.downcast_ref::<std::io::Error>() {
|
|
||||||
return matches!(
|
|
||||||
io_err.kind(),
|
|
||||||
std::io::ErrorKind::BrokenPipe
|
|
||||||
| std::io::ErrorKind::UnexpectedEof
|
|
||||||
| std::io::ErrorKind::ConnectionAborted
|
|
||||||
| std::io::ErrorKind::ConnectionReset
|
|
||||||
| std::io::ErrorKind::NotConnected
|
|
||||||
) || io_err.raw_os_error() == Some(10054);
|
|
||||||
}
|
|
||||||
false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_peer_certificate_ip(
|
|
||||||
tls_stream: &tokio_rustls::server::TlsStream<TcpStream>,
|
|
||||||
peer_ip: IpAddr,
|
|
||||||
) -> Result<()> {
|
|
||||||
let (_, server_connection) = tls_stream.get_ref();
|
|
||||||
let peer_certs = server_connection
|
|
||||||
.peer_certificates()
|
|
||||||
.ok_or_else(|| anyhow!("missing peer certificate after TLS client authentication"))?;
|
|
||||||
let end_entity = peer_certs
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("peer did not present an end-entity certificate"))?;
|
|
||||||
|
|
||||||
let (_, cert) = X509Certificate::from_der(end_entity.as_ref())
|
|
||||||
.map_err(|err| anyhow!("failed to parse peer certificate: {:?}", err))?;
|
|
||||||
let san = cert
|
|
||||||
.subject_alternative_name()
|
|
||||||
.map_err(|err| anyhow!("failed to parse peer certificate SAN: {:?}", err))?
|
|
||||||
.ok_or_else(|| anyhow!("peer certificate is missing subjectAltName"))?;
|
|
||||||
|
|
||||||
let matched = san.value.general_names.iter().any(|name| match name {
|
|
||||||
GeneralName::IPAddress(bytes) => {
|
|
||||||
let bytes = *bytes;
|
|
||||||
match (peer_ip, bytes.len()) {
|
|
||||||
(IpAddr::V4(ip), 4) => <[u8; 4]>::try_from(bytes)
|
|
||||||
.map(IpAddr::from)
|
|
||||||
.map(|cert_ip| cert_ip == IpAddr::V4(ip))
|
|
||||||
.unwrap_or(false),
|
|
||||||
(IpAddr::V6(ip), 16) => <[u8; 16]>::try_from(bytes)
|
|
||||||
.map(IpAddr::from)
|
|
||||||
.map(|cert_ip| cert_ip == IpAddr::V6(ip))
|
|
||||||
.unwrap_or(false),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if matched {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"RTR TLS client certificate SAN IP mismatch for peer_ip={}",
|
|
||||||
peer_ip
|
|
||||||
);
|
|
||||||
Err(anyhow!(
|
|
||||||
"peer certificate subjectAltName iPAddress does not match {}",
|
|
||||||
peer_ip
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,681 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicU64, AtomicUsize},
|
|
||||||
};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use russh::server::{self, Msg, Session};
|
|
||||||
use russh::{Channel, ChannelId, Disconnect};
|
|
||||||
use socket2::{SockRef, TcpKeepalive};
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
|
||||||
use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, watch};
|
|
||||||
use tokio_rustls::TlsAcceptor;
|
|
||||||
use tracing::{debug, info, warn};
|
|
||||||
|
|
||||||
use rustls::ServerConfig;
|
|
||||||
|
|
||||||
use crate::rtr::cache::SharedRtrCache;
|
|
||||||
use crate::rtr::server::config::RtrServiceConfig;
|
|
||||||
use crate::rtr::server::connection::{
|
|
||||||
ConnectionGuard, RtrTransportConnectionCounters, RtrTransportKind, handle_tcp_connection,
|
|
||||||
handle_tls_connection, is_expected_disconnect,
|
|
||||||
};
|
|
||||||
use crate::rtr::server::service::RtrServiceControl;
|
|
||||||
use crate::rtr::server::ssh::RtrSshRuntimeConfig;
|
|
||||||
use crate::rtr::server::tls::load_rustls_server_config_with_options;
|
|
||||||
use crate::rtr::session::RtrSession;
|
|
||||||
|
|
||||||
type TransportFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
|
|
||||||
|
|
||||||
pub trait TransportAcceptor: Clone + Send + Sync + 'static {
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
fn kind(&self) -> RtrTransportKind;
|
|
||||||
fn requires_handshake_limit(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_connection(
|
|
||||||
&self,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
handshake_permit: Option<OwnedSemaphorePermit>,
|
|
||||||
) -> TransportFuture;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TcpTransport;
|
|
||||||
|
|
||||||
impl TransportAcceptor for TcpTransport {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"TCP"
|
|
||||||
}
|
|
||||||
fn kind(&self) -> RtrTransportKind {
|
|
||||||
RtrTransportKind::Tcp
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_connection(
|
|
||||||
&self,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
_handshake_permit: Option<OwnedSemaphorePermit>,
|
|
||||||
) -> TransportFuture {
|
|
||||||
Box::pin(async move {
|
|
||||||
handle_tcp_connection(
|
|
||||||
cache,
|
|
||||||
stream,
|
|
||||||
peer_addr,
|
|
||||||
notify_tx.subscribe(),
|
|
||||||
shutdown_tx.subscribe(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct TlsTransport {
|
|
||||||
acceptor: TlsAcceptor,
|
|
||||||
enforce_client_san_ip_match: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransportAcceptor for TlsTransport {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"TLS"
|
|
||||||
}
|
|
||||||
fn kind(&self) -> RtrTransportKind {
|
|
||||||
RtrTransportKind::Tls
|
|
||||||
}
|
|
||||||
fn requires_handshake_limit(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_connection(
|
|
||||||
&self,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
handshake_permit: Option<OwnedSemaphorePermit>,
|
|
||||||
) -> TransportFuture {
|
|
||||||
let acceptor = self.acceptor.clone();
|
|
||||||
let enforce_client_san_ip_match = self.enforce_client_san_ip_match;
|
|
||||||
Box::pin(async move {
|
|
||||||
handle_tls_connection(
|
|
||||||
cache,
|
|
||||||
stream,
|
|
||||||
peer_addr,
|
|
||||||
acceptor,
|
|
||||||
enforce_client_san_ip_match,
|
|
||||||
notify_tx.subscribe(),
|
|
||||||
shutdown_tx.subscribe(),
|
|
||||||
handshake_permit,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct SshTransport {
|
|
||||||
runtime: Arc<RtrSshRuntimeConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransportAcceptor for SshTransport {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"SSH"
|
|
||||||
}
|
|
||||||
fn kind(&self) -> RtrTransportKind {
|
|
||||||
RtrTransportKind::Ssh
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_connection(
|
|
||||||
&self,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
stream: TcpStream,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
_handshake_permit: Option<OwnedSemaphorePermit>,
|
|
||||||
) -> TransportFuture {
|
|
||||||
let runtime = self.runtime.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let handler = RtrSshHandler::new(
|
|
||||||
cache,
|
|
||||||
notify_tx.subscribe(),
|
|
||||||
shutdown_tx.subscribe(),
|
|
||||||
peer_addr,
|
|
||||||
runtime.authorized_keys.clone(),
|
|
||||||
runtime.username.clone(),
|
|
||||||
runtime.subsystem_name.clone(),
|
|
||||||
runtime.password.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let running = server::run_stream(runtime.server_config.clone(), stream, handler)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("failed to start SSH session for {}", peer_addr))?;
|
|
||||||
|
|
||||||
let handle = running.handle();
|
|
||||||
let mut connection_shutdown_rx = shutdown_tx.subscribe();
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
session_res = running => {
|
|
||||||
session_res.map_err(|err| anyhow!(err))
|
|
||||||
}
|
|
||||||
changed = connection_shutdown_rx.changed() => {
|
|
||||||
match changed {
|
|
||||||
Ok(()) if connection_shutdown_rx.borrow().shutdown_reason().is_some() => {
|
|
||||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
||||||
let _ = handle
|
|
||||||
.disconnect(
|
|
||||||
Disconnect::ByApplication,
|
|
||||||
"RTR service shutdown".to_string(),
|
|
||||||
"".to_string(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Ok(()) | Err(_) => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RtrServer {
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
connection_limiter: Arc<Semaphore>,
|
|
||||||
handshake_limiter: Arc<Semaphore>,
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
connection_change_seq: Arc<AtomicU64>,
|
|
||||||
config: RtrServiceConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrServer {
|
|
||||||
pub fn new(
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
connection_limiter: Arc<Semaphore>,
|
|
||||||
handshake_limiter: Arc<Semaphore>,
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
connection_change_seq: Arc<AtomicU64>,
|
|
||||||
config: RtrServiceConfig,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
bind_addr,
|
|
||||||
cache,
|
|
||||||
notify_tx,
|
|
||||||
shutdown_tx,
|
|
||||||
connection_limiter,
|
|
||||||
handshake_limiter,
|
|
||||||
active_connections,
|
|
||||||
transport_connections,
|
|
||||||
connection_change_tx,
|
|
||||||
connection_change_seq,
|
|
||||||
config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind_addr(&self) -> SocketAddr {
|
|
||||||
self.bind_addr
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache(&self) -> SharedRtrCache {
|
|
||||||
self.cache.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_connections(&self) -> usize {
|
|
||||||
self.active_connections
|
|
||||||
.load(std::sync::atomic::Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_tcp(self) -> Result<()> {
|
|
||||||
self.run_with_transport(TcpTransport).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_tls_from_pem(
|
|
||||||
self,
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tls_config = Arc::new(load_rustls_server_config_with_options(
|
|
||||||
cert_path,
|
|
||||||
key_path,
|
|
||||||
client_ca_path,
|
|
||||||
self.config.require_tls_server_dns_name_san,
|
|
||||||
)?);
|
|
||||||
self.run_tls(tls_config).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_tls(self, tls_config: Arc<ServerConfig>) -> Result<()> {
|
|
||||||
let transport = TlsTransport {
|
|
||||||
acceptor: TlsAcceptor::from(tls_config),
|
|
||||||
enforce_client_san_ip_match: self.config.enforce_tls_client_san_ip_match,
|
|
||||||
};
|
|
||||||
self.run_with_transport(transport).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn run_ssh(self, runtime_config: Arc<RtrSshRuntimeConfig>) -> Result<()> {
|
|
||||||
let transport = SshTransport {
|
|
||||||
runtime: runtime_config,
|
|
||||||
};
|
|
||||||
self.run_with_transport(transport).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_with_transport<T>(self, transport: T) -> Result<()>
|
|
||||||
where
|
|
||||||
T: TransportAcceptor,
|
|
||||||
{
|
|
||||||
let listener = TcpListener::bind(self.bind_addr).await.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to bind {} RTR server on {}",
|
|
||||||
transport.name(),
|
|
||||||
self.bind_addr
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"RTR {} server listening on {}",
|
|
||||||
transport.name(),
|
|
||||||
self.bind_addr
|
|
||||||
);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
changed = shutdown_rx.changed() => {
|
|
||||||
match changed {
|
|
||||||
Ok(()) => {
|
|
||||||
if let Some(reason) = shutdown_rx.borrow().shutdown_reason() {
|
|
||||||
info!(
|
|
||||||
"RTR {} listener {} shutting down: reason={}",
|
|
||||||
transport.name(),
|
|
||||||
self.bind_addr,
|
|
||||||
reason.as_str()
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
info!(
|
|
||||||
"RTR {} listener {} shutdown channel closed",
|
|
||||||
transport.name(),
|
|
||||||
self.bind_addr
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accept_res = listener.accept() => {
|
|
||||||
let (stream, peer_addr) = match accept_res {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("RTR {} accept failed: {}", transport.name(), err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
|
|
||||||
warn!(
|
|
||||||
"failed to configure TCP keepalive for {} peer {}: {}",
|
|
||||||
transport.name(),
|
|
||||||
peer_addr,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let permit = match self.connection_limiter.clone().try_acquire_owned() {
|
|
||||||
Ok(permit) => permit,
|
|
||||||
Err(_) => {
|
|
||||||
warn!(
|
|
||||||
"RTR {} connection rejected for {}: max connections reached ({})",
|
|
||||||
transport.name(),
|
|
||||||
peer_addr,
|
|
||||||
self.config.max_connections
|
|
||||||
);
|
|
||||||
drop(stream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let handshake_permit = if transport.requires_handshake_limit() {
|
|
||||||
match self.handshake_limiter.clone().try_acquire_owned() {
|
|
||||||
Ok(permit) => Some(permit),
|
|
||||||
Err(_) => {
|
|
||||||
warn!(
|
|
||||||
"RTR {} connection rejected for {}: max concurrent handshakes reached ({})",
|
|
||||||
transport.name(),
|
|
||||||
peer_addr,
|
|
||||||
self.config.max_concurrent_handshakes
|
|
||||||
);
|
|
||||||
drop(stream);
|
|
||||||
drop(permit);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let cache = self.cache.clone();
|
|
||||||
let notify_tx = self.notify_tx.clone();
|
|
||||||
let shutdown_tx = self.shutdown_tx.clone();
|
|
||||||
let active_connections = self.active_connections.clone();
|
|
||||||
let transport_connections = self.transport_connections.clone();
|
|
||||||
let connection_change_tx = self.connection_change_tx.clone();
|
|
||||||
let connection_change_seq = self.connection_change_seq.clone();
|
|
||||||
let transport_instance = transport.clone();
|
|
||||||
let transport_kind = transport_instance.kind();
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
"RTR {} client connected: peer_addr={}, active_connections(before_spawn)={}",
|
|
||||||
transport_instance.name(),
|
|
||||||
peer_addr,
|
|
||||||
self.active_connections()
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let guard = ConnectionGuard::new(
|
|
||||||
active_connections,
|
|
||||||
transport_connections,
|
|
||||||
connection_change_tx,
|
|
||||||
connection_change_seq,
|
|
||||||
transport_kind,
|
|
||||||
permit,
|
|
||||||
);
|
|
||||||
info!(
|
|
||||||
"RTR {} connection established: peer_addr={}, active_connections={}",
|
|
||||||
transport_instance.name(),
|
|
||||||
peer_addr,
|
|
||||||
guard.active_count()
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Err(err) = transport_instance
|
|
||||||
.handle_connection(
|
|
||||||
cache,
|
|
||||||
stream,
|
|
||||||
peer_addr,
|
|
||||||
notify_tx,
|
|
||||||
shutdown_tx,
|
|
||||||
handshake_permit,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let active_after_close = guard.active_count().saturating_sub(1);
|
|
||||||
if is_expected_disconnect(&err) {
|
|
||||||
info!(
|
|
||||||
"RTR {} session closed by peer: peer_addr={}, active_connections={}, err={}",
|
|
||||||
transport_instance.name(),
|
|
||||||
peer_addr,
|
|
||||||
active_after_close,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"RTR {} session closed with error: peer_addr={}, active_connections={}, err={}",
|
|
||||||
transport_instance.name(),
|
|
||||||
peer_addr,
|
|
||||||
active_after_close,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let active_after_close = guard.active_count().saturating_sub(1);
|
|
||||||
info!(
|
|
||||||
"RTR {} session closed cleanly: peer_addr={}, active_connections={}",
|
|
||||||
transport_instance.name(),
|
|
||||||
peer_addr,
|
|
||||||
active_after_close
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RtrSshHandler {
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
notify_rx: broadcast::Receiver<()>,
|
|
||||||
shutdown_rx: watch::Receiver<RtrServiceControl>,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
authorized_keys: Arc<Vec<russh::keys::ssh_key::PublicKey>>,
|
|
||||||
username: Arc<str>,
|
|
||||||
subsystem_name: Arc<str>,
|
|
||||||
password: Option<Arc<str>>,
|
|
||||||
channels: HashMap<ChannelId, Channel<Msg>>,
|
|
||||||
subsystem_started: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrSshHandler {
|
|
||||||
fn new(
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
notify_rx: broadcast::Receiver<()>,
|
|
||||||
shutdown_rx: watch::Receiver<RtrServiceControl>,
|
|
||||||
peer_addr: SocketAddr,
|
|
||||||
authorized_keys: Arc<Vec<russh::keys::ssh_key::PublicKey>>,
|
|
||||||
username: Arc<str>,
|
|
||||||
subsystem_name: Arc<str>,
|
|
||||||
password: Option<Arc<str>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
cache,
|
|
||||||
notify_rx,
|
|
||||||
shutdown_rx,
|
|
||||||
peer_addr,
|
|
||||||
authorized_keys,
|
|
||||||
username,
|
|
||||||
subsystem_name,
|
|
||||||
password,
|
|
||||||
channels: HashMap::new(),
|
|
||||||
subsystem_started: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_authorized_key(&self, key: &russh::keys::ssh_key::PublicKey) -> bool {
|
|
||||||
self.authorized_keys
|
|
||||||
.iter()
|
|
||||||
.any(|allowed| allowed.key_data() == key.key_data())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_expected_user(&self, user: &str) -> bool {
|
|
||||||
user == self.username.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl server::Handler for RtrSshHandler {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
async fn auth_none(&mut self, _user: &str) -> Result<server::Auth, Self::Error> {
|
|
||||||
Ok(server::Auth::reject())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auth_password(
|
|
||||||
&mut self,
|
|
||||||
user: &str,
|
|
||||||
password: &str,
|
|
||||||
) -> Result<server::Auth, Self::Error> {
|
|
||||||
let accepted = self.is_expected_user(user)
|
|
||||||
&& self
|
|
||||||
.password
|
|
||||||
.as_deref()
|
|
||||||
.map(|expected| expected == password)
|
|
||||||
.unwrap_or(false);
|
|
||||||
if accepted {
|
|
||||||
info!(
|
|
||||||
"RTR SSH password auth accepted: peer_addr={}, user={}",
|
|
||||||
self.peer_addr, user
|
|
||||||
);
|
|
||||||
Ok(server::Auth::Accept)
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"RTR SSH password auth rejected: peer_addr={}, user={}",
|
|
||||||
self.peer_addr, user
|
|
||||||
);
|
|
||||||
Ok(server::Auth::reject())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auth_publickey_offered(
|
|
||||||
&mut self,
|
|
||||||
user: &str,
|
|
||||||
public_key: &russh::keys::ssh_key::PublicKey,
|
|
||||||
) -> Result<server::Auth, Self::Error> {
|
|
||||||
if self.is_expected_user(user) && self.is_authorized_key(public_key) {
|
|
||||||
Ok(server::Auth::Accept)
|
|
||||||
} else {
|
|
||||||
Ok(server::Auth::reject())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn auth_publickey(
|
|
||||||
&mut self,
|
|
||||||
user: &str,
|
|
||||||
public_key: &russh::keys::ssh_key::PublicKey,
|
|
||||||
) -> Result<server::Auth, Self::Error> {
|
|
||||||
if self.is_expected_user(user) && self.is_authorized_key(public_key) {
|
|
||||||
info!(
|
|
||||||
"RTR SSH publickey auth accepted: peer_addr={}, user={}",
|
|
||||||
self.peer_addr, user
|
|
||||||
);
|
|
||||||
Ok(server::Auth::Accept)
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"RTR SSH publickey auth rejected: peer_addr={}, user={}",
|
|
||||||
self.peer_addr, user
|
|
||||||
);
|
|
||||||
Ok(server::Auth::reject())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn channel_open_session(
|
|
||||||
&mut self,
|
|
||||||
channel: Channel<Msg>,
|
|
||||||
_session: &mut Session,
|
|
||||||
) -> Result<bool, Self::Error> {
|
|
||||||
if self.subsystem_started {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
self.channels.insert(channel.id(), channel);
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn subsystem_request(
|
|
||||||
&mut self,
|
|
||||||
channel: ChannelId,
|
|
||||||
name: &str,
|
|
||||||
session: &mut Session,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
if name != self.subsystem_name.as_ref() {
|
|
||||||
let _ = session.channel_failure(channel);
|
|
||||||
warn!(
|
|
||||||
"RTR SSH subsystem rejected: peer_addr={}, requested={}, expected={}",
|
|
||||||
self.peer_addr, name, self.subsystem_name
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(channel) = self.channels.remove(&channel) else {
|
|
||||||
let _ = session.channel_failure(channel);
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
session.channel_success(channel.id())?;
|
|
||||||
self.subsystem_started = true;
|
|
||||||
|
|
||||||
let cache = self.cache.clone();
|
|
||||||
let notify_rx = self.notify_rx.resubscribe();
|
|
||||||
let shutdown_rx = self.shutdown_rx.clone();
|
|
||||||
let peer_addr = self.peer_addr;
|
|
||||||
let subsystem = self.subsystem_name.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let stream = channel.into_stream();
|
|
||||||
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
|
|
||||||
if let Err(err) = session.run().await {
|
|
||||||
warn!(
|
|
||||||
"RTR SSH subsystem session closed with error: peer_addr={}, subsystem={}, err={}",
|
|
||||||
peer_addr, subsystem, err
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"RTR SSH subsystem session completed: peer_addr={}, subsystem={}",
|
|
||||||
peer_addr, subsystem
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shell_request(
|
|
||||||
&mut self,
|
|
||||||
channel: ChannelId,
|
|
||||||
session: &mut Session,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
let _ = session.channel_failure(channel);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn exec_request(
|
|
||||||
&mut self,
|
|
||||||
channel: ChannelId,
|
|
||||||
_data: &[u8],
|
|
||||||
session: &mut Session,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
let _ = session.channel_failure(channel);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn pty_request(
|
|
||||||
&mut self,
|
|
||||||
channel: ChannelId,
|
|
||||||
_term: &str,
|
|
||||||
_col_width: u32,
|
|
||||||
_row_height: u32,
|
|
||||||
_pix_width: u32,
|
|
||||||
_pix_height: u32,
|
|
||||||
_modes: &[(russh::Pty, u32)],
|
|
||||||
session: &mut Session,
|
|
||||||
) -> Result<(), Self::Error> {
|
|
||||||
let _ = session.channel_failure(channel);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_keepalive(stream: &tokio::net::TcpStream, keepalive: Option<Duration>) -> Result<()> {
|
|
||||||
let Some(keepalive) = keepalive else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
let socket = SockRef::from(stream);
|
|
||||||
let keepalive = TcpKeepalive::new().with_time(keepalive);
|
|
||||||
socket
|
|
||||||
.set_tcp_keepalive(&keepalive)
|
|
||||||
.context("unable to apply TCP keepalive settings")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod connection;
|
|
||||||
pub mod listener;
|
|
||||||
pub mod notifier;
|
|
||||||
pub mod service;
|
|
||||||
pub mod ssh;
|
|
||||||
pub mod tls;
|
|
||||||
|
|
||||||
pub use config::RtrServiceConfig;
|
|
||||||
pub use connection::RtrTransportConnectionCounts;
|
|
||||||
pub use listener::RtrServer;
|
|
||||||
pub use notifier::RtrNotifier;
|
|
||||||
pub use service::{
|
|
||||||
RtrService, RtrServiceControl, RtrServiceStats, RtrShutdownHandle, RtrShutdownReason,
|
|
||||||
RunningRtrService,
|
|
||||||
};
|
|
||||||
pub use tls::load_rustls_server_config;
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
use tokio::sync::broadcast;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RtrNotifier {
|
|
||||||
tx: broadcast::Sender<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrNotifier {
|
|
||||||
pub fn new(tx: broadcast::Sender<()>) -> Self {
|
|
||||||
Self { tx }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_cache_updated(&self) -> usize {
|
|
||||||
self.tx.send(()).unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn listener_count(&self) -> usize {
|
|
||||||
self.tx.receiver_count()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,422 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{AtomicU64, AtomicUsize, Ordering},
|
|
||||||
};
|
|
||||||
|
|
||||||
use tokio::sync::{Semaphore, broadcast, watch};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tracing::{error, warn};
|
|
||||||
|
|
||||||
use crate::rtr::cache::SharedRtrCache;
|
|
||||||
use crate::rtr::server::config::RtrServiceConfig;
|
|
||||||
use crate::rtr::server::connection::{
|
|
||||||
RtrTransportConnectionCounters, RtrTransportConnectionCounts,
|
|
||||||
};
|
|
||||||
use crate::rtr::server::listener::RtrServer;
|
|
||||||
use crate::rtr::server::notifier::RtrNotifier;
|
|
||||||
use crate::rtr::server::ssh::{SshAuthMode, load_rtr_ssh_runtime_config};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum RtrShutdownReason {
|
|
||||||
CloseOnly,
|
|
||||||
CacheRestart,
|
|
||||||
CacheShutdown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrShutdownReason {
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::CloseOnly => "close-only",
|
|
||||||
Self::CacheRestart => "cache-restart",
|
|
||||||
Self::CacheShutdown => "cache-shutdown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_env() -> Self {
|
|
||||||
match env::var("RPKI_RTR_STOP_REASON") {
|
|
||||||
Ok(value) => Self::parse(&value).unwrap_or(Self::CacheRestart),
|
|
||||||
Err(_) => Self::CacheRestart,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse(value: &str) -> Option<Self> {
|
|
||||||
match value.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"close-only" | "close_only" | "close" => Some(Self::CloseOnly),
|
|
||||||
"restart" | "cache-restart" | "cache_restart" => Some(Self::CacheRestart),
|
|
||||||
"shutdown" | "cache-shutdown" | "cache_shutdown" => Some(Self::CacheShutdown),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
||||||
pub enum RtrServiceControl {
|
|
||||||
Running,
|
|
||||||
Shutdown(RtrShutdownReason),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrServiceControl {
|
|
||||||
pub fn shutdown_reason(self) -> Option<RtrShutdownReason> {
|
|
||||||
match self {
|
|
||||||
Self::Running => None,
|
|
||||||
Self::Shutdown(reason) => Some(reason),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RtrShutdownHandle {
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrShutdownHandle {
|
|
||||||
pub fn shutdown(&self, reason: RtrShutdownReason) {
|
|
||||||
let _ = self.shutdown_tx.send(RtrServiceControl::Shutdown(reason));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RtrService {
|
|
||||||
cache: SharedRtrCache,
|
|
||||||
notify_tx: broadcast::Sender<()>,
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
connection_limiter: Arc<Semaphore>,
|
|
||||||
handshake_limiter: Arc<Semaphore>,
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
connection_change_seq: Arc<AtomicU64>,
|
|
||||||
config: RtrServiceConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RtrServiceStats {
|
|
||||||
active_connections: Arc<AtomicUsize>,
|
|
||||||
transport_connections: Arc<RtrTransportConnectionCounters>,
|
|
||||||
connection_change_tx: watch::Sender<u64>,
|
|
||||||
max_connections: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrServiceStats {
|
|
||||||
pub fn active_connections(&self) -> usize {
|
|
||||||
self.active_connections.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn transport_connections(&self) -> RtrTransportConnectionCounts {
|
|
||||||
self.transport_connections.snapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_connections(&self) -> usize {
|
|
||||||
self.max_connections
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_connection_changes(&self) -> watch::Receiver<u64> {
|
|
||||||
self.connection_change_tx.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RtrService {
|
|
||||||
pub fn new(cache: SharedRtrCache) -> Self {
|
|
||||||
Self::with_config(cache, RtrServiceConfig::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_config(cache: SharedRtrCache, config: RtrServiceConfig) -> Self {
|
|
||||||
let (notify_tx, _) = broadcast::channel(config.notify_queue_size);
|
|
||||||
let (shutdown_tx, _) = watch::channel(RtrServiceControl::Running);
|
|
||||||
let (connection_change_tx, _) = watch::channel(0);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
cache,
|
|
||||||
notify_tx,
|
|
||||||
shutdown_tx,
|
|
||||||
connection_limiter: Arc::new(Semaphore::new(config.max_connections)),
|
|
||||||
handshake_limiter: Arc::new(Semaphore::new(config.max_concurrent_handshakes)),
|
|
||||||
active_connections: Arc::new(AtomicUsize::new(0)),
|
|
||||||
transport_connections: Arc::new(RtrTransportConnectionCounters::default()),
|
|
||||||
connection_change_tx,
|
|
||||||
connection_change_seq: Arc::new(AtomicU64::new(0)),
|
|
||||||
config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache(&self) -> SharedRtrCache {
|
|
||||||
self.cache.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notifier(&self) -> RtrNotifier {
|
|
||||||
RtrNotifier::new(self.notify_tx.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown_handle(&self) -> RtrShutdownHandle {
|
|
||||||
RtrShutdownHandle {
|
|
||||||
shutdown_tx: self.shutdown_tx.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn notify_cache_updated(&self) {
|
|
||||||
let _ = self.notify_tx.send(());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn active_connections(&self) -> usize {
|
|
||||||
self.active_connections.load(Ordering::Relaxed)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn transport_connections(&self) -> RtrTransportConnectionCounts {
|
|
||||||
self.transport_connections.snapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn max_connections(&self) -> usize {
|
|
||||||
self.config.max_connections
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stats(&self) -> RtrServiceStats {
|
|
||||||
RtrServiceStats {
|
|
||||||
active_connections: self.active_connections.clone(),
|
|
||||||
transport_connections: self.transport_connections.clone(),
|
|
||||||
connection_change_tx: self.connection_change_tx.clone(),
|
|
||||||
max_connections: self.config.max_connections,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tcp_server(&self, bind_addr: SocketAddr) -> RtrServer {
|
|
||||||
RtrServer::new(
|
|
||||||
bind_addr,
|
|
||||||
self.cache.clone(),
|
|
||||||
self.notify_tx.clone(),
|
|
||||||
self.shutdown_tx.clone(),
|
|
||||||
self.connection_limiter.clone(),
|
|
||||||
self.handshake_limiter.clone(),
|
|
||||||
self.active_connections.clone(),
|
|
||||||
self.transport_connections.clone(),
|
|
||||||
self.connection_change_tx.clone(),
|
|
||||||
self.connection_change_seq.clone(),
|
|
||||||
self.config.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tls_server(&self, bind_addr: SocketAddr) -> RtrServer {
|
|
||||||
RtrServer::new(
|
|
||||||
bind_addr,
|
|
||||||
self.cache.clone(),
|
|
||||||
self.notify_tx.clone(),
|
|
||||||
self.shutdown_tx.clone(),
|
|
||||||
self.connection_limiter.clone(),
|
|
||||||
self.handshake_limiter.clone(),
|
|
||||||
self.active_connections.clone(),
|
|
||||||
self.transport_connections.clone(),
|
|
||||||
self.connection_change_tx.clone(),
|
|
||||||
self.connection_change_seq.clone(),
|
|
||||||
self.config.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ssh_server(&self, bind_addr: SocketAddr) -> RtrServer {
|
|
||||||
RtrServer::new(
|
|
||||||
bind_addr,
|
|
||||||
self.cache.clone(),
|
|
||||||
self.notify_tx.clone(),
|
|
||||||
self.shutdown_tx.clone(),
|
|
||||||
self.connection_limiter.clone(),
|
|
||||||
self.handshake_limiter.clone(),
|
|
||||||
self.active_connections.clone(),
|
|
||||||
self.transport_connections.clone(),
|
|
||||||
self.connection_change_tx.clone(),
|
|
||||||
self.connection_change_seq.clone(),
|
|
||||||
self.config.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_tcp(&self, bind_addr: SocketAddr) -> JoinHandle<()> {
|
|
||||||
if self.config.warn_insecure_tcp {
|
|
||||||
warn!(
|
|
||||||
"starting plain TCP RTR service on {}. Per draft-ietf-sidrops-8210bis-26 Section 9, unsecured TCP must only be used on a trusted and controlled network",
|
|
||||||
bind_addr
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let server = self.tcp_server(bind_addr);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = server.run_tcp().await {
|
|
||||||
error!("RTR TCP server {} exited with error: {:?}", bind_addr, err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_tls_from_pem(
|
|
||||||
&self,
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let cert_path = cert_path.as_ref().to_path_buf();
|
|
||||||
let key_path = key_path.as_ref().to_path_buf();
|
|
||||||
let client_ca_path = client_ca_path.as_ref().to_path_buf();
|
|
||||||
let server = self.tls_server(bind_addr);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = server
|
|
||||||
.run_tls_from_pem(cert_path, key_path, client_ca_path)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
error!("RTR TLS server {} exited with error: {:?}", bind_addr, err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_tcp_and_tls_from_pem(
|
|
||||||
&self,
|
|
||||||
tcp_bind_addr: SocketAddr,
|
|
||||||
tls_bind_addr: SocketAddr,
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
) -> RunningRtrService {
|
|
||||||
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
|
||||||
let tls_handle =
|
|
||||||
self.spawn_tls_from_pem(tls_bind_addr, cert_path, key_path, client_ca_path);
|
|
||||||
|
|
||||||
RunningRtrService {
|
|
||||||
shutdown_tx: self.shutdown_tx.clone(),
|
|
||||||
handles: vec![tcp_handle, tls_handle],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn spawn_ssh_from_openssh(
|
|
||||||
&self,
|
|
||||||
bind_addr: SocketAddr,
|
|
||||||
host_key_path: impl AsRef<Path>,
|
|
||||||
authorized_keys_path: impl AsRef<Path>,
|
|
||||||
username: &str,
|
|
||||||
subsystem_name: &str,
|
|
||||||
auth_mode: SshAuthMode,
|
|
||||||
password: Option<&str>,
|
|
||||||
) -> JoinHandle<()> {
|
|
||||||
let host_key_path = host_key_path.as_ref().to_path_buf();
|
|
||||||
let authorized_keys_path = authorized_keys_path.as_ref().to_path_buf();
|
|
||||||
let username = username.to_string();
|
|
||||||
let subsystem_name = subsystem_name.to_string();
|
|
||||||
let password = password.map(ToString::to_string);
|
|
||||||
let inactivity_timeout = Some(std::time::Duration::from_secs(3600));
|
|
||||||
let keepalive_interval = self.config.tcp_keepalive;
|
|
||||||
let server = self.ssh_server(bind_addr);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let runtime_config = match load_rtr_ssh_runtime_config(
|
|
||||||
&host_key_path,
|
|
||||||
&authorized_keys_path,
|
|
||||||
&username,
|
|
||||||
&subsystem_name,
|
|
||||||
auth_mode,
|
|
||||||
password.as_deref(),
|
|
||||||
inactivity_timeout,
|
|
||||||
keepalive_interval,
|
|
||||||
) {
|
|
||||||
Ok(cfg) => Arc::new(cfg),
|
|
||||||
Err(err) => {
|
|
||||||
error!(
|
|
||||||
"RTR SSH server {} failed to load configuration: {:?}",
|
|
||||||
bind_addr, err
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = server.run_ssh(runtime_config).await {
|
|
||||||
error!("RTR SSH server {} exited with error: {:?}", bind_addr, err);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn spawn_tcp_and_ssh_from_openssh(
|
|
||||||
&self,
|
|
||||||
tcp_bind_addr: SocketAddr,
|
|
||||||
ssh_bind_addr: SocketAddr,
|
|
||||||
host_key_path: impl AsRef<Path>,
|
|
||||||
authorized_keys_path: impl AsRef<Path>,
|
|
||||||
username: &str,
|
|
||||||
subsystem_name: &str,
|
|
||||||
auth_mode: SshAuthMode,
|
|
||||||
password: Option<&str>,
|
|
||||||
) -> RunningRtrService {
|
|
||||||
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
|
||||||
let ssh_handle = self.spawn_ssh_from_openssh(
|
|
||||||
ssh_bind_addr,
|
|
||||||
host_key_path,
|
|
||||||
authorized_keys_path,
|
|
||||||
username,
|
|
||||||
subsystem_name,
|
|
||||||
auth_mode,
|
|
||||||
password,
|
|
||||||
);
|
|
||||||
|
|
||||||
RunningRtrService {
|
|
||||||
shutdown_tx: self.shutdown_tx.clone(),
|
|
||||||
handles: vec![tcp_handle, ssh_handle],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn spawn_tcp_tls_and_ssh_from_pem_and_openssh(
|
|
||||||
&self,
|
|
||||||
tcp_bind_addr: SocketAddr,
|
|
||||||
tls_bind_addr: SocketAddr,
|
|
||||||
ssh_bind_addr: SocketAddr,
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
host_key_path: impl AsRef<Path>,
|
|
||||||
authorized_keys_path: impl AsRef<Path>,
|
|
||||||
username: &str,
|
|
||||||
subsystem_name: &str,
|
|
||||||
auth_mode: SshAuthMode,
|
|
||||||
password: Option<&str>,
|
|
||||||
) -> RunningRtrService {
|
|
||||||
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
|
||||||
let tls_handle =
|
|
||||||
self.spawn_tls_from_pem(tls_bind_addr, cert_path, key_path, client_ca_path);
|
|
||||||
let ssh_handle = self.spawn_ssh_from_openssh(
|
|
||||||
ssh_bind_addr,
|
|
||||||
host_key_path,
|
|
||||||
authorized_keys_path,
|
|
||||||
username,
|
|
||||||
subsystem_name,
|
|
||||||
auth_mode,
|
|
||||||
password,
|
|
||||||
);
|
|
||||||
|
|
||||||
RunningRtrService {
|
|
||||||
shutdown_tx: self.shutdown_tx.clone(),
|
|
||||||
handles: vec![tcp_handle, tls_handle, ssh_handle],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn spawn_tcp_only(&self, tcp_bind_addr: SocketAddr) -> RunningRtrService {
|
|
||||||
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
|
||||||
|
|
||||||
RunningRtrService {
|
|
||||||
shutdown_tx: self.shutdown_tx.clone(),
|
|
||||||
handles: vec![tcp_handle],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RunningRtrService {
|
|
||||||
shutdown_tx: watch::Sender<RtrServiceControl>,
|
|
||||||
handles: Vec<JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RunningRtrService {
|
|
||||||
pub fn shutdown(&self, reason: RtrShutdownReason) {
|
|
||||||
let _ = self.shutdown_tx.send(RtrServiceControl::Shutdown(reason));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn wait(self) {
|
|
||||||
for handle in self.handles {
|
|
||||||
let _ = handle.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
|
||||||
use russh::keys;
|
|
||||||
use russh::keys::PrivateKey;
|
|
||||||
use russh::keys::ssh_key::{self, AuthorizedKeys};
|
|
||||||
use russh::server::Config as RusshServerConfig;
|
|
||||||
use russh::{MethodKind, MethodSet};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum SshAuthMode {
|
|
||||||
Key,
|
|
||||||
Password,
|
|
||||||
Both,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshAuthMode {
|
|
||||||
pub fn parse(value: &str) -> Option<Self> {
|
|
||||||
match value.trim().to_ascii_lowercase().as_str() {
|
|
||||||
"key" | "publickey" => Some(Self::Key),
|
|
||||||
"password" => Some(Self::Password),
|
|
||||||
"both" => Some(Self::Both),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Key => "key",
|
|
||||||
Self::Password => "password",
|
|
||||||
Self::Both => "both",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RtrSshRuntimeConfig {
|
|
||||||
pub server_config: Arc<RusshServerConfig>,
|
|
||||||
pub authorized_keys: Arc<Vec<ssh_key::PublicKey>>,
|
|
||||||
pub username: Arc<str>,
|
|
||||||
pub subsystem_name: Arc<str>,
|
|
||||||
pub password: Option<Arc<str>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_rtr_ssh_runtime_config(
|
|
||||||
host_key_path: impl AsRef<Path>,
|
|
||||||
authorized_keys_path: impl AsRef<Path>,
|
|
||||||
username: &str,
|
|
||||||
subsystem_name: &str,
|
|
||||||
auth_mode: SshAuthMode,
|
|
||||||
password: Option<&str>,
|
|
||||||
inactivity_timeout: Option<Duration>,
|
|
||||||
keepalive_interval: Option<Duration>,
|
|
||||||
) -> Result<RtrSshRuntimeConfig> {
|
|
||||||
if username.trim().is_empty() {
|
|
||||||
bail!("SSH username must not be empty");
|
|
||||||
}
|
|
||||||
if subsystem_name.trim().is_empty() {
|
|
||||||
bail!("SSH subsystem name must not be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
let host_key = load_host_key(host_key_path.as_ref())?;
|
|
||||||
let authorized_keys = if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) {
|
|
||||||
load_authorized_keys(authorized_keys_path.as_ref())?
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
let password = password.map(str::trim).filter(|value| !value.is_empty());
|
|
||||||
if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) && password.is_none() {
|
|
||||||
bail!(
|
|
||||||
"SSH auth mode '{}' requires non-empty password",
|
|
||||||
auth_mode.as_str()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut methods = MethodSet::empty();
|
|
||||||
if matches!(auth_mode, SshAuthMode::Key | SshAuthMode::Both) {
|
|
||||||
methods.push(MethodKind::PublicKey);
|
|
||||||
}
|
|
||||||
if matches!(auth_mode, SshAuthMode::Password | SshAuthMode::Both) {
|
|
||||||
methods.push(MethodKind::Password);
|
|
||||||
}
|
|
||||||
|
|
||||||
let server_config = RusshServerConfig {
|
|
||||||
methods,
|
|
||||||
keys: vec![host_key],
|
|
||||||
inactivity_timeout,
|
|
||||||
keepalive_interval,
|
|
||||||
keepalive_max: 3,
|
|
||||||
auth_rejection_time: Duration::from_secs(1),
|
|
||||||
auth_rejection_time_initial: Some(Duration::from_secs(0)),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(RtrSshRuntimeConfig {
|
|
||||||
server_config: Arc::new(server_config),
|
|
||||||
authorized_keys: Arc::new(authorized_keys),
|
|
||||||
username: Arc::from(username.trim()),
|
|
||||||
subsystem_name: Arc::from(subsystem_name.trim()),
|
|
||||||
password: password.map(Arc::from),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_host_key(path: &Path) -> Result<PrivateKey> {
|
|
||||||
keys::load_secret_key(path, None).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to load SSH host private key from {} (OpenSSH private key expected)",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_authorized_keys(path: &Path) -> Result<Vec<ssh_key::PublicKey>> {
|
|
||||||
let entries = AuthorizedKeys::read_file(path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to read SSH authorized_keys file from {}",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut keys = Vec::with_capacity(entries.len());
|
|
||||||
for entry in entries {
|
|
||||||
keys.push(entry.public_key().clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if keys.is_empty() {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"SSH authorized_keys file {} does not contain any usable keys",
|
|
||||||
path.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys)
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
use std::fs::File;
|
|
||||||
use std::io::BufReader;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
|
||||||
use rustls::server::WebPkiClientVerifier;
|
|
||||||
use rustls::{RootCertStore, ServerConfig};
|
|
||||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
|
|
||||||
use tracing::warn;
|
|
||||||
use x509_parser::extensions::GeneralName;
|
|
||||||
use x509_parser::prelude::{FromDer, X509Certificate};
|
|
||||||
|
|
||||||
pub fn load_rustls_server_config(
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
) -> Result<ServerConfig> {
|
|
||||||
load_rustls_server_config_with_options(cert_path, key_path, client_ca_path, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_rustls_server_config_with_options(
|
|
||||||
cert_path: impl AsRef<Path>,
|
|
||||||
key_path: impl AsRef<Path>,
|
|
||||||
client_ca_path: impl AsRef<Path>,
|
|
||||||
require_dns_name_san: bool,
|
|
||||||
) -> Result<ServerConfig> {
|
|
||||||
let cert_path: PathBuf = cert_path.as_ref().to_path_buf();
|
|
||||||
let key_path: PathBuf = key_path.as_ref().to_path_buf();
|
|
||||||
let client_ca_path: PathBuf = client_ca_path.as_ref().to_path_buf();
|
|
||||||
|
|
||||||
let certs = load_certs(&cert_path)
|
|
||||||
.with_context(|| format!("failed to load certs from {}", cert_path.display()))?;
|
|
||||||
validate_server_certificate_dns_name_san(&certs, &cert_path, require_dns_name_san)?;
|
|
||||||
|
|
||||||
let key = load_private_key(&key_path)
|
|
||||||
.with_context(|| format!("failed to load private key from {}", key_path.display()))?;
|
|
||||||
|
|
||||||
let client_ca_certs = load_certs(&client_ca_path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to load client CA certs from {}",
|
|
||||||
client_ca_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut client_roots = RootCertStore::empty();
|
|
||||||
let (added, _) = client_roots.add_parsable_certificates(client_ca_certs);
|
|
||||||
if added == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"no valid client CA certificates found in {}",
|
|
||||||
client_ca_path.display()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let client_verifier = WebPkiClientVerifier::builder(Arc::new(client_roots))
|
|
||||||
.build()
|
|
||||||
.map_err(|e| anyhow!("invalid client certificate verifier configuration: {}", e))?;
|
|
||||||
|
|
||||||
let config = ServerConfig::builder()
|
|
||||||
.with_client_cert_verifier(client_verifier)
|
|
||||||
.with_single_cert(certs, key)
|
|
||||||
.map_err(|e| anyhow!("invalid certificate/key pair: {}", e))?;
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_server_certificate_dns_name_san(
|
|
||||||
certs: &[CertificateDer<'static>],
|
|
||||||
cert_path: &Path,
|
|
||||||
require_dns_name_san: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
let leaf = certs
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("missing end-entity certificate in {}", cert_path.display()))?;
|
|
||||||
let (_, cert) = X509Certificate::from_der(leaf.as_ref())
|
|
||||||
.map_err(|err| anyhow!("failed to parse server certificate: {:?}", err))?;
|
|
||||||
let has_dns_name_san = cert
|
|
||||||
.subject_alternative_name()
|
|
||||||
.map_err(|err| anyhow!("failed to parse server certificate SAN: {:?}", err))?
|
|
||||||
.map(|san| {
|
|
||||||
san.value
|
|
||||||
.general_names
|
|
||||||
.iter()
|
|
||||||
.any(|name| matches!(name, GeneralName::DNSName(_)))
|
|
||||||
})
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if has_dns_name_san {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = format!(
|
|
||||||
"server certificate {} does not contain a subjectAltName dNSName entry; draft-ietf-sidrops-8210bis-25 Section 9.2 requires routers to authenticate the cache using DNS-ID rather than CN-ID",
|
|
||||||
cert_path.display()
|
|
||||||
);
|
|
||||||
|
|
||||||
if require_dns_name_san {
|
|
||||||
Err(anyhow!(message))
|
|
||||||
} else {
|
|
||||||
warn!("{}", message);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
|
|
||||||
let file = File::open(path)?;
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let certs = rustls_pemfile::certs(&mut reader).collect::<std::result::Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
if certs.is_empty() {
|
|
||||||
return Err(anyhow!("no certificates found in {}", path.display()));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(certs)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_private_key(path: &Path) -> Result<PrivateKeyDer<'static>> {
|
|
||||||
let file = File::open(path)?;
|
|
||||||
let mut reader = BufReader::new(file);
|
|
||||||
|
|
||||||
let key = rustls_pemfile::private_key(&mut reader)?
|
|
||||||
.ok_or_else(|| anyhow!("no private key found in {}", path.display()))?;
|
|
||||||
|
|
||||||
Ok(key)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user