增加ssh
增加deploy下细分的tcp、tls、ssh
This commit is contained in:
parent
99250f8aa9
commit
b60d579a38
@ -31,5 +31,6 @@ rustls = "0.23"
|
|||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
rustls-pki-types = "1.14.0"
|
rustls-pki-types = "1.14.0"
|
||||||
socket2 = "0.5"
|
socket2 = "0.5"
|
||||||
|
russh = { version = "0.60.0", default-features = false, features = ["ring", "rsa"] }
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
rpki_rs = { package = "rpki", version = "0.18", features = ["rtr", "crypto"] }
|
rpki_rs = { package = "rpki", version = "0.19.2", features = ["rtr", "crypto"] }
|
||||||
|
|||||||
143
README.md
143
README.md
@ -75,6 +75,14 @@ RTR Server 运行时从 `CCR` 目录中扫描最新的 `.ccr` 文件作为输入
|
|||||||
| `RPKI_RTR_TLS_CERT_PATH` | TLS 服务端证书路径。 | `./certs/server.crt` | `./certs/server-dns.crt` |
|
| `RPKI_RTR_TLS_CERT_PATH` | TLS 服务端证书路径。 | `./certs/server.crt` | `./certs/server-dns.crt` |
|
||||||
| `RPKI_RTR_TLS_KEY_PATH` | TLS 服务端私钥路径。 | `./certs/server.key` | `./certs/server-dns.key` |
|
| `RPKI_RTR_TLS_KEY_PATH` | TLS 服务端私钥路径。 | `./certs/server.key` | `./certs/server-dns.key` |
|
||||||
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | 用于校验 router 客户端证书的 CA 证书路径。 | `./certs/client-ca.crt` | `./certs/client-ca.crt` |
|
| `RPKI_RTR_TLS_CLIENT_CA_PATH` | 用于校验 router 客户端证书的 CA 证书路径。 | `./certs/client-ca.crt` | `./certs/client-ca.crt` |
|
||||||
|
| `RPKI_RTR_ENABLE_SSH` | 是否额外启用进程内原生 SSH 监听。支持 `true/false`、`1/0`、`yes/no`、`on/off`。 | `false` | `true` |
|
||||||
|
| `RPKI_RTR_SSH_ADDR` | SSH 监听地址。 | `0.0.0.0:22` | `0.0.0.0:22` |
|
||||||
|
| `RPKI_RTR_SSH_PORT` | SSH 监听端口(仅覆盖 `RPKI_RTR_SSH_ADDR` 中的端口)。 | `22` | `2022` |
|
||||||
|
| `RPKI_RTR_SSH_HOST_KEY_PATH` | OpenSSH host 私钥路径。 | `./certs/ssh_host_ed25519_key` | `./certs/ssh_host_ed25519_key` |
|
||||||
|
| `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH` | 允许接入的 router 公钥列表(authorized_keys)。 | `./certs/rtr-authorized_keys` | `./certs/rtr-authorized_keys` |
|
||||||
|
| `RPKI_RTR_SSH_USERNAME` | SSH 用户名白名单。 | `rpki-rtr` | `rpki-rtr` |
|
||||||
|
| `RPKI_RTR_SSH_SUBSYSTEM_NAME` | SSH 子系统名称。 | `rpki-rtr` | `rpki-rtr` |
|
||||||
|
| `RPKI_RTR_SSH_PASSWORD` | 可选的 SSH password 认证口令。未设置时仅允许 publickey;设置后同时允许 publickey 与 password。 | `未设置` | `test-password` |
|
||||||
| `RPKI_RTR_MAX_DELTA` | 最多保留多少条 delta。 | `100` | `100` |
|
| `RPKI_RTR_MAX_DELTA` | 最多保留多少条 delta。 | `100` | `100` |
|
||||||
| `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE` | 是否启用“累计 delta 估算 wire size 不小于 snapshot 时,继续裁剪最老 delta”的策略。 | `false` | `false` |
|
| `RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE` | 是否启用“累计 delta 估算 wire size 不小于 snapshot 时,继续裁剪最老 delta”的策略。 | `false` | `false` |
|
||||||
| `RPKI_RTR_STRICT_CCR_VALIDATION` | 是否对 CCR 中的非法 VRP / VAP 采用严格模式;`true` 表示整份 CCR 拒绝,`false` 表示跳过非法项并告警。 | `false` | `false` |
|
| `RPKI_RTR_STRICT_CCR_VALIDATION` | 是否对 CCR 中的非法 VRP / VAP 采用严格模式;`true` 表示整份 CCR 拒绝,`false` 表示跳过非法项并告警。 | `false` | `false` |
|
||||||
@ -113,36 +121,24 @@ docker compose -f deploy/server/docker-compose.yml down
|
|||||||
|
|
||||||
### 本地运行(推荐先用脚本)
|
### 本地运行(推荐先用脚本)
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
sh ./scripts/start-rtr-server-tcp.sh
|
docker compose -f deploy/server/docker-compose.yml up -d --build
|
||||||
sh ./scripts/start-rtr-server-tls.sh
|
docker compose -f deploy/server/docker-compose.yml logs -f rpki-rtr
|
||||||
|
docker compose -f deploy/server/docker-compose.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本入口:
|
|
||||||
|
|
||||||
- [`scripts/start-rtr-server-tcp.sh`](scripts/start-rtr-server-tcp.sh)
|
|
||||||
- [`scripts/start-rtr-server-tls.sh`](scripts/start-rtr-server-tls.sh)
|
|
||||||
- [`scripts/start-rtr-server.sh`](scripts/start-rtr-server.sh)
|
|
||||||
|
|
||||||
### 本地手动运行(最小示例)
|
### 本地手动运行(最小示例)
|
||||||
|
|
||||||
纯 TCP:
|
纯 TCP:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
export RPKI_RTR_ENABLE_TLS=false
|
docker compose -f deploy/server/docker-compose.yml up -d --build
|
||||||
export RPKI_RTR_CCR_DIR=./data
|
|
||||||
cargo run --bin rpki
|
|
||||||
```
|
```
|
||||||
|
|
||||||
TLS / mutual TLS:
|
TLS / mutual TLS:
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
export RPKI_RTR_ENABLE_TLS=true
|
docker compose -f deploy/server/docker-compose.yml -f deploy/server/docker-compose.tls.yml up -d --build
|
||||||
export RPKI_RTR_CCR_DIR=./data
|
|
||||||
export RPKI_RTR_TLS_CERT_PATH=./certs/server-dns.crt
|
|
||||||
export RPKI_RTR_TLS_KEY_PATH=./certs/server-dns.key
|
|
||||||
export RPKI_RTR_TLS_CLIENT_CA_PATH=./certs/client-ca.crt
|
|
||||||
cargo run --bin rpki
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## CCR 输入说明
|
## CCR 输入说明
|
||||||
@ -174,16 +170,6 @@ cargo run --bin rpki
|
|||||||
2. 再用 `rpki-rs-test-client` 做可重复的自动化步骤校验。
|
2. 再用 `rpki-rs-test-client` 做可重复的自动化步骤校验。
|
||||||
3. 最后用 `FRR` 做黑盒互通验证,确认真实客户端接入行为。
|
3. 最后用 `FRR` 做黑盒互通验证,确认真实客户端接入行为。
|
||||||
|
|
||||||
### rtr_debug_client(本地)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
- 适合手工排查:你可以快速切换 TCP/TLS、版本号、请求类型来观察响应差异。
|
|
||||||
- 适合问题定位:当服务端日志出现异常时,可用最小参数复现问题流量。
|
|
||||||
|
|
||||||
### rtr_debug_client(Docker)
|
### rtr_debug_client(Docker)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -372,3 +358,100 @@ docker exec -it frr-rpki-client vtysh -c "show rpki prefix-table"
|
|||||||
- `deploy/server/DEPLOYMENT.md`
|
- `deploy/server/DEPLOYMENT.md`
|
||||||
- `deploy/frr/README.md`
|
- `deploy/frr/README.md`
|
||||||
- `deploy/frr/README.zh.md`
|
- `deploy/frr/README.zh.md`
|
||||||
|
|
||||||
|
## 传输模式说明(TCP / TLS / 原生 SSH)
|
||||||
|
|
||||||
|
当前 `rpki` 进程内支持三种传输:
|
||||||
|
|
||||||
|
- TCP(默认开启)
|
||||||
|
- TLS(可选开启,mTLS)
|
||||||
|
- 原生 SSH(可选开启,进程内实现,不再使用外部 `sshd Subsystem` 桥接)
|
||||||
|
|
||||||
|
### TCP 模式
|
||||||
|
|
||||||
|
默认监听:
|
||||||
|
- `RPKI_RTR_TCP_ADDR=0.0.0.0:323`
|
||||||
|
|
||||||
|
最小启动(仅 TCP):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export RPKI_RTR_ENABLE_TLS=false
|
||||||
|
export RPKI_RTR_ENABLE_SSH=false
|
||||||
|
cargo run --bin rpki
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 仅建议部署在受信任、可控网络中。
|
||||||
|
|
||||||
|
### TLS 模式(mTLS)
|
||||||
|
|
||||||
|
相关环境变量:
|
||||||
|
- `RPKI_RTR_ENABLE_TLS=true`
|
||||||
|
- `RPKI_RTR_TLS_ADDR`(默认 `0.0.0.0:324`)
|
||||||
|
- `RPKI_RTR_TLS_CERT_PATH`
|
||||||
|
- `RPKI_RTR_TLS_KEY_PATH`
|
||||||
|
- `RPKI_RTR_TLS_CLIENT_CA_PATH`
|
||||||
|
|
||||||
|
最小启动(TCP + TLS):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export RPKI_RTR_ENABLE_TLS=true
|
||||||
|
export RPKI_RTR_ENABLE_SSH=false
|
||||||
|
export RPKI_RTR_TLS_CERT_PATH=./certs/server-dns.crt
|
||||||
|
export RPKI_RTR_TLS_KEY_PATH=./certs/server-dns.key
|
||||||
|
export RPKI_RTR_TLS_CLIENT_CA_PATH=./certs/client-ca.crt
|
||||||
|
cargo run --bin rpki
|
||||||
|
```
|
||||||
|
|
||||||
|
### 原生 SSH 模式(进程内)
|
||||||
|
|
||||||
|
与 `draft-ietf-sidrops-8210bis-25` 对齐要点:
|
||||||
|
- 使用 SSHv2
|
||||||
|
- 使用 subsystem(默认 `rpki-rtr`)
|
||||||
|
- 使用 public key 认证
|
||||||
|
- 服务端拒绝 `none`
|
||||||
|
- 服务端默认不启用 `password`,配置 `RPKI_RTR_SSH_PASSWORD` 后可选启用(draft 中为 MAY)
|
||||||
|
|
||||||
|
相关环境变量:
|
||||||
|
- `RPKI_RTR_ENABLE_SSH=true`
|
||||||
|
- `RPKI_RTR_SSH_ADDR`(默认 `0.0.0.0:22`)
|
||||||
|
- `RPKI_RTR_SSH_PORT`(默认 `22`,设置后会覆盖 `RPKI_RTR_SSH_ADDR` 的端口)
|
||||||
|
- `RPKI_RTR_SSH_HOST_KEY_PATH`(OpenSSH host 私钥)
|
||||||
|
- `RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH`(允许接入的 router 公钥列表)
|
||||||
|
- `RPKI_RTR_SSH_USERNAME`(默认 `rpki-rtr`)
|
||||||
|
- `RPKI_RTR_SSH_SUBSYSTEM_NAME`(默认 `rpki-rtr`)
|
||||||
|
- `RPKI_RTR_SSH_PASSWORD`(可选;设置后启用 password 认证)
|
||||||
|
|
||||||
|
密钥准备示例:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh-keygen -t ed25519 -N '' -f ./certs/ssh_host_ed25519_key
|
||||||
|
ssh-keygen -t ed25519 -N '' -f ./certs/rtr_client_ed25519_key
|
||||||
|
cp ./certs/rtr_client_ed25519_key.pub ./certs/rtr-authorized_keys
|
||||||
|
```
|
||||||
|
|
||||||
|
最小启动(TCP + SSH):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export RPKI_RTR_ENABLE_TLS=false
|
||||||
|
export RPKI_RTR_ENABLE_SSH=true
|
||||||
|
export RPKI_RTR_SSH_ADDR=0.0.0.0:22
|
||||||
|
# or only override the port:
|
||||||
|
# export RPKI_RTR_SSH_PORT=2022
|
||||||
|
export RPKI_RTR_SSH_HOST_KEY_PATH=./certs/ssh_host_ed25519_key
|
||||||
|
export RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH=./certs/rtr-authorized_keys
|
||||||
|
export RPKI_RTR_SSH_USERNAME=rpki-rtr
|
||||||
|
export RPKI_RTR_SSH_SUBSYSTEM_NAME=rpki-rtr
|
||||||
|
# 可选:启用 password 认证(同时仍支持 publickey)
|
||||||
|
# export RPKI_RTR_SSH_PASSWORD=test-password
|
||||||
|
cargo run --bin rpki
|
||||||
|
```
|
||||||
|
|
||||||
|
连通性检查(OpenSSH):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -i ./certs/rtr_client_ed25519_key -p 22 -s rpki-rtr@127.0.0.1 rpki-rtr
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 该命令主要用于验证 SSH 子系统通道可建立,不等价于完整 RTR 协议回归测试。
|
||||||
|
|||||||
@ -166,3 +166,43 @@ docker compose -f deploy/frr/docker-compose.yml down
|
|||||||
```bash
|
```bash
|
||||||
docker compose -f deploy/frr/docker-compose.yml logs -f frr-rpki-client
|
docker compose -f deploy/frr/docker-compose.yml logs -f frr-rpki-client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) BIRD Client
|
||||||
|
|
||||||
|
路径:
|
||||||
|
- `deploy/bird/Dockerfile`
|
||||||
|
- `deploy/bird/docker-compose.yml`
|
||||||
|
- `deploy/bird/docker-compose.tls.yml`
|
||||||
|
- `deploy/bird/bird.conf.example`
|
||||||
|
- `deploy/bird/bird.conf.tls.example`
|
||||||
|
- `deploy/bird/README.md`
|
||||||
|
- `deploy/bird/README.zh.md`
|
||||||
|
|
||||||
|
启动:
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/bird/docker-compose.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
观察活动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f bird-rpki-client
|
||||||
|
```
|
||||||
|
|
||||||
|
停止:
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/bird/docker-compose.yml down
|
||||||
|
```
|
||||||
|
|
||||||
|
日志:
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/bird/docker-compose.yml logs -f bird-rpki-client
|
||||||
|
```
|
||||||
|
|
||||||
|
TLS/mTLS:
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/bird/docker-compose.yml -f deploy/bird/docker-compose.tls.yml up --build
|
||||||
|
docker logs -f bird-rpki-client
|
||||||
|
```
|
||||||
|
|||||||
10
deploy/bird/Dockerfile
Normal file
10
deploy/bird/Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends bird2 ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
75
deploy/bird/README.md
Normal file
75
deploy/bird/README.md
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# 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`
|
||||||
|
- TLS: `0.0.0.0:324`
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `Dockerfile`: builds a minimal BIRD2 runtime image.
|
||||||
|
- `bird.conf.example`: sample `/etc/bird/bird.conf`.
|
||||||
|
- `bird.conf.tls.example`: sample TLS/mTLS `/etc/bird/bird.conf`.
|
||||||
|
- `entrypoint.sh`: starts BIRD in foreground mode.
|
||||||
|
- `docker-compose.yml`: one-click local TCP test client.
|
||||||
|
- `docker-compose.tls.yml`: compose override for TLS/mTLS.
|
||||||
|
|
||||||
|
By default, the container prints periodic RPKI protocol snapshots to logs
|
||||||
|
every 5 seconds.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS/mTLS quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
-f deploy/bird/docker-compose.yml \
|
||||||
|
-f deploy/bird/docker-compose.tls.yml \
|
||||||
|
up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
In detached mode, observe with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f bird-rpki-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This setup targets RTR over TCP (`remote "127.0.0.1" port 323`).
|
||||||
|
- `network_mode: host` expects your RTR server to be reachable at
|
||||||
|
`127.0.0.1:323` from the Docker host.
|
||||||
|
- TLS override mounts `../../certs` into `/etc/bird/certs`.
|
||||||
|
- Observation is controlled by env vars:
|
||||||
|
`OBSERVE_INTERVAL` (seconds, default `5`) and `OBSERVE_PROTO`.
|
||||||
|
- If your environment does not support Docker host networking, switch to a
|
||||||
|
bridge network and replace `remote` addresses accordingly.
|
||||||
63
deploy/bird/README.zh.md
Normal file
63
deploy/bird/README.zh.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# BIRD 最小化 RTR 客户端配置
|
||||||
|
|
||||||
|
本目录提供一个最小化 BIRD 配置,用于和本仓库 RTR Server 做黑盒互通测试。
|
||||||
|
|
||||||
|
本仓库默认 RTR 监听地址:
|
||||||
|
- TCP: `0.0.0.0:323`
|
||||||
|
- TLS: `0.0.0.0:324`
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
- `Dockerfile`: 构建最小 BIRD2 运行镜像。
|
||||||
|
- `bird.conf.example`: `/etc/bird/bird.conf` 的 TCP 示例。
|
||||||
|
- `bird.conf.tls.example`: `/etc/bird/bird.conf` 的 TLS/mTLS 示例。
|
||||||
|
- `entrypoint.sh`: 前台启动 BIRD。
|
||||||
|
- `docker-compose.yml`: TCP 一键启动。
|
||||||
|
- `docker-compose.tls.yml`: TLS/mTLS 覆盖文件。
|
||||||
|
|
||||||
|
容器默认每 5 秒向日志输出一次 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## TLS/mTLS 快速启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose \
|
||||||
|
-f deploy/bird/docker-compose.yml \
|
||||||
|
-f deploy/bird/docker-compose.tls.yml \
|
||||||
|
up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 当前 compose 使用 `network_mode: host`,要求容器可通过 `127.0.0.1` 访问宿主机 RTR Server。
|
||||||
|
- TLS 覆盖文件会把 `../../certs` 挂载到容器内 `/etc/bird/certs`。
|
||||||
|
- 观测频率由环境变量控制:`OBSERVE_INTERVAL`(秒,默认 `5`)和 `OBSERVE_PROTO`。
|
||||||
|
- 若你运行在 Docker Desktop(非 Linux 原生 host network 场景),建议改为自定义 bridge 网络并把 `remote` 地址改成可达的 server 容器名或宿主地址。
|
||||||
15
deploy/bird/bird.conf.example
Normal file
15
deploy/bird/bird.conf.example
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
log stderr all;
|
||||||
|
router id 192.0.2.2;
|
||||||
|
|
||||||
|
roa4 table rtr_roa_v4;
|
||||||
|
roa6 table rtr_roa_v6;
|
||||||
|
|
||||||
|
protocol device {
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol rpki rpki_tcp {
|
||||||
|
roa4 { table rtr_roa_v4; };
|
||||||
|
roa6 { table rtr_roa_v6; };
|
||||||
|
|
||||||
|
remote "127.0.0.1" port 323;
|
||||||
|
}
|
||||||
21
deploy/bird/bird.conf.tls.example
Normal file
21
deploy/bird/bird.conf.tls.example
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
log stderr all;
|
||||||
|
router id 192.0.2.2;
|
||||||
|
|
||||||
|
roa4 table rtr_roa_v4;
|
||||||
|
roa6 table rtr_roa_v6;
|
||||||
|
|
||||||
|
protocol device {
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol rpki rpki_tls {
|
||||||
|
roa4 { table rtr_roa_v4; };
|
||||||
|
roa6 { table rtr_roa_v6; };
|
||||||
|
|
||||||
|
remote "127.0.0.1" port 324;
|
||||||
|
|
||||||
|
transport tls {
|
||||||
|
ca file "/etc/bird/certs/client-ca.crt";
|
||||||
|
cert file "/etc/bird/certs/client-good.crt";
|
||||||
|
key file "/etc/bird/certs/client-good.key";
|
||||||
|
};
|
||||||
|
}
|
||||||
7
deploy/bird/docker-compose.tls.yml
Normal file
7
deploy/bird/docker-compose.tls.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
bird-rpki-client:
|
||||||
|
environment:
|
||||||
|
OBSERVE_PROTO: rpki_tls
|
||||||
|
volumes:
|
||||||
|
- ./bird.conf.tls.example:/etc/bird/bird.conf:ro
|
||||||
|
- ../../certs:/etc/bird/certs:ro
|
||||||
13
deploy/bird/docker-compose.yml
Normal file
13
deploy/bird/docker-compose.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
bird-rpki-client:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: bird-rpki-client
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
OBSERVE_INTERVAL: "5"
|
||||||
|
OBSERVE_PROTO: rpki_tcp
|
||||||
|
volumes:
|
||||||
|
- ./bird.conf.example:/etc/bird/bird.conf:ro
|
||||||
30
deploy/bird/entrypoint.sh
Normal file
30
deploy/bird/entrypoint.sh
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
mkdir -p /run/bird
|
||||||
|
|
||||||
|
SOCK_PATH="/run/bird/bird.ctl"
|
||||||
|
PROTO="${OBSERVE_PROTO:-rpki_tcp}"
|
||||||
|
INTERVAL="${OBSERVE_INTERVAL:-5}"
|
||||||
|
|
||||||
|
bird -f -c /etc/bird/bird.conf -s "$SOCK_PATH" &
|
||||||
|
BIRD_PID="$!"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
case "$INTERVAL" in
|
||||||
|
''|*[!0-9]*)
|
||||||
|
INTERVAL=0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$INTERVAL" -gt 0 ]; then
|
||||||
|
while kill -0 "$BIRD_PID" 2>/dev/null; do
|
||||||
|
echo "==== $(date -u +"%Y-%m-%dT%H:%M:%SZ") RPKI snapshot ($PROTO) ===="
|
||||||
|
birdc -s "$SOCK_PATH" show protocols all "$PROTO" || true
|
||||||
|
birdc -s "$SOCK_PATH" show roa count || true
|
||||||
|
sleep "$INTERVAL"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait "$BIRD_PID"
|
||||||
@ -20,5 +20,8 @@ RUN apt-get update \
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
||||||
|
COPY deploy/client/entrypoint.sh /usr/local/bin/rtr-debug-client-entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/rtr_debug_client"]
|
RUN chmod +x /usr/local/bin/rtr-debug-client-entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/rtr-debug-client-entrypoint.sh"]
|
||||||
|
|||||||
@ -5,28 +5,38 @@ services:
|
|||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
rtr-client-2:
|
rtr-client-2:
|
||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
rtr-client-3:
|
rtr-client-3:
|
||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
rtr-client-4:
|
rtr-client-4:
|
||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
rtr-client-5:
|
rtr-client-5:
|
||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
30
deploy/client/docker-compose.ssh.yml
Normal file
30
deploy/client/docker-compose.ssh.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rtr-debug-client:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/client/Dockerfile
|
||||||
|
image: rpki-rtr-debug-client:latest
|
||||||
|
network_mode: host
|
||||||
|
command:
|
||||||
|
[
|
||||||
|
"127.0.0.1:${RPKI_RTR_SSH_PORT:-22}",
|
||||||
|
"2",
|
||||||
|
"reset",
|
||||||
|
"--ssh",
|
||||||
|
"--ssh-user",
|
||||||
|
"rpki-rtr",
|
||||||
|
"--ssh-key",
|
||||||
|
"/app/certs/rtr-client.key",
|
||||||
|
"--ssh-server-key",
|
||||||
|
"/app/certs/ssh_host_rsa_key.pub",
|
||||||
|
"--keep-after-error",
|
||||||
|
"--summary-only"
|
||||||
|
]
|
||||||
|
volumes:
|
||||||
|
- ../../certs:/app/certs:ro
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
13
deploy/client/docker-compose.tcp.yml
Normal file
13
deploy/client/docker-compose.tcp.yml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
rtr-debug-client:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/client/Dockerfile
|
||||||
|
image: rpki-rtr-debug-client:latest
|
||||||
|
network_mode: host
|
||||||
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
|
restart: no
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
32
deploy/client/docker-compose.tls.yml
Normal file
32
deploy/client/docker-compose.tls.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rtr-debug-client:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/client/Dockerfile
|
||||||
|
image: rpki-rtr-debug-client:latest
|
||||||
|
network_mode: host
|
||||||
|
command:
|
||||||
|
[
|
||||||
|
"127.0.0.1:324",
|
||||||
|
"2",
|
||||||
|
"reset",
|
||||||
|
"--tls",
|
||||||
|
"--ca-cert",
|
||||||
|
"/app/certs/client-ca.crt",
|
||||||
|
"--server-name",
|
||||||
|
"localhost",
|
||||||
|
"--client-cert",
|
||||||
|
"/app/certs/client-good.crt",
|
||||||
|
"--client-key",
|
||||||
|
"/app/certs/client-good.key",
|
||||||
|
"--keep-after-error",
|
||||||
|
"--summary-only"
|
||||||
|
]
|
||||||
|
volumes:
|
||||||
|
- ../../tests/fixtures/tls:/app/certs:ro
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
@ -6,6 +6,8 @@ services:
|
|||||||
image: rpki-rtr-debug-client:latest
|
image: rpki-rtr-debug-client:latest
|
||||||
network_mode: host
|
network_mode: host
|
||||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||||
|
volumes:
|
||||||
|
- ../../logs/client:/app/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
|||||||
10
deploy/client/entrypoint.sh
Normal file
10
deploy/client/entrypoint.sh
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
mkdir -p /app/logs
|
||||||
|
|
||||||
|
log_name="${HOSTNAME:-rtr-debug-client}"
|
||||||
|
stdout_log="/app/logs/${log_name}.stdout.log"
|
||||||
|
stderr_log="/app/logs/${log_name}.stderr.log"
|
||||||
|
|
||||||
|
exec /usr/local/bin/rtr_debug_client "$@" >>"$stdout_log" 2>>"$stderr_log"
|
||||||
@ -2,8 +2,25 @@ FROM rust:1.89-bookworm AS builder
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
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 \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends \
|
&& apt-get install -y --fix-missing --no-install-recommends \
|
||||||
|
-o Acquire::Retries=10 \
|
||||||
|
-o Acquire::http::Timeout=60 \
|
||||||
build-essential \
|
build-essential \
|
||||||
cmake \
|
cmake \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@ -19,8 +36,27 @@ RUN cargo build --release --bin rpki
|
|||||||
|
|
||||||
FROM debian:bookworm-slim AS runtime
|
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 \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates supervisor \
|
&& apt-get install -y --fix-missing --no-install-recommends \
|
||||||
|
-o Acquire::Retries=10 \
|
||||||
|
-o Acquire::http::Timeout=60 \
|
||||||
|
ca-certificates \
|
||||||
|
supervisor \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@ -28,7 +64,7 @@ WORKDIR /app
|
|||||||
COPY --from=builder /build/target/release/rpki /usr/local/bin/rpki
|
COPY --from=builder /build/target/release/rpki /usr/local/bin/rpki
|
||||||
COPY deploy/server/supervisord.conf /etc/supervisor/conf.d/rpki-rtr.conf
|
COPY deploy/server/supervisord.conf /etc/supervisor/conf.d/rpki-rtr.conf
|
||||||
|
|
||||||
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /var/log/supervisor
|
RUN mkdir -p /app/data /app/rtr-db /app/certs /app/slurm /app/logs /var/log/supervisor
|
||||||
|
|
||||||
ENV RPKI_RTR_ENABLE_TLS=false \
|
ENV RPKI_RTR_ENABLE_TLS=false \
|
||||||
RPKI_RTR_TCP_ADDR=0.0.0.0:323 \
|
RPKI_RTR_TCP_ADDR=0.0.0.0:323 \
|
||||||
@ -41,4 +77,4 @@ ENV RPKI_RTR_ENABLE_TLS=false \
|
|||||||
|
|
||||||
EXPOSE 323 324
|
EXPOSE 323 324
|
||||||
|
|
||||||
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/rpki-rtr.conf"]
|
CMD ["supervisord", "-n", "-c", "/etc/supervisor/conf.d/rpki-rtr.conf"]
|
||||||
35
deploy/server/docker-compose.ssh.yml
Normal file
35
deploy/server/docker-compose.ssh.yml
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rpki-rtr:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/server/Dockerfile
|
||||||
|
image: rpki-rtr:latest
|
||||||
|
container_name: rpki-rtr-ssh
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "323:323"
|
||||||
|
- "${RPKI_RTR_SSH_PORT:-22}:${RPKI_RTR_SSH_PORT:-22}"
|
||||||
|
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_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 authentication in addition to publickey
|
||||||
|
# RPKI_RTR_SSH_PASSWORD: "test-password"
|
||||||
|
RPKI_RTR_DB_PATH: "/app/rtr-db"
|
||||||
|
RPKI_RTR_CCR_DIR: "/app/data"
|
||||||
|
RPKI_RTR_SLURM_DIR: "/app/slurm"
|
||||||
|
RPKI_RTR_STRICT_CCR_VALIDATION: "false"
|
||||||
|
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
|
||||||
|
volumes:
|
||||||
|
- ../../data:/app/data:ro
|
||||||
|
- ../../rtr-db:/app/rtr-db
|
||||||
|
- ../../data:/app/slurm:ro
|
||||||
|
- ../../certs:/app/certs:ro
|
||||||
|
- ../../logs/server:/app/logs
|
||||||
27
deploy/server/docker-compose.tcp.yml
Normal file
27
deploy/server/docker-compose.tcp.yml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rpki-rtr:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/server/Dockerfile
|
||||||
|
image: rpki-rtr:latest
|
||||||
|
container_name: rpki-rtr-tcp
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "323:323"
|
||||||
|
environment:
|
||||||
|
RPKI_RTR_ENABLE_TLS: "false"
|
||||||
|
RPKI_RTR_ENABLE_SSH: "false"
|
||||||
|
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||||
|
RPKI_RTR_DB_PATH: "/app/rtr-db"
|
||||||
|
RPKI_RTR_CCR_DIR: "/app/data"
|
||||||
|
RPKI_RTR_SLURM_DIR: "/app/slurm"
|
||||||
|
RPKI_RTR_STRICT_CCR_VALIDATION: "false"
|
||||||
|
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
|
||||||
|
RPKI_RTR_MAX_CONNECTIONS: "100000"
|
||||||
|
volumes:
|
||||||
|
- ../../data:/app/data:ro
|
||||||
|
- ../../rtr-db:/app/rtr-db
|
||||||
|
- ../../data:/app/slurm:ro
|
||||||
|
- ../../logs/server:/app/logs
|
||||||
32
deploy/server/docker-compose.tls.yml
Normal file
32
deploy/server/docker-compose.tls.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
rpki-rtr:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: deploy/server/Dockerfile
|
||||||
|
image: rpki-rtr:latest
|
||||||
|
container_name: rpki-rtr-tls
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "323:323"
|
||||||
|
- "324:324"
|
||||||
|
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: "/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_DB_PATH: "/app/rtr-db"
|
||||||
|
RPKI_RTR_CCR_DIR: "/app/data"
|
||||||
|
RPKI_RTR_SLURM_DIR: "/app/slurm"
|
||||||
|
RPKI_RTR_STRICT_CCR_VALIDATION: "false"
|
||||||
|
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
|
||||||
|
volumes:
|
||||||
|
- ../../data:/app/data:ro
|
||||||
|
- ../../rtr-db:/app/rtr-db
|
||||||
|
- ../../data:/app/slurm:ro
|
||||||
|
- ../../tests/fixtures/tls:/app/certs:ro
|
||||||
|
- ../../logs/server:/app/logs
|
||||||
@ -11,6 +11,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "323:323"
|
- "323:323"
|
||||||
- "324:324"
|
- "324:324"
|
||||||
|
# SSH mode example:
|
||||||
|
# - "22:22"
|
||||||
environment:
|
environment:
|
||||||
RPKI_RTR_ENABLE_TLS: "false"
|
RPKI_RTR_ENABLE_TLS: "false"
|
||||||
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
RPKI_RTR_TCP_ADDR: "0.0.0.0:323"
|
||||||
@ -20,9 +22,21 @@ services:
|
|||||||
RPKI_RTR_SLURM_DIR: "/app/slurm"
|
RPKI_RTR_SLURM_DIR: "/app/slurm"
|
||||||
RPKI_RTR_STRICT_CCR_VALIDATION: "false"
|
RPKI_RTR_STRICT_CCR_VALIDATION: "false"
|
||||||
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
|
RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300"
|
||||||
|
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_ed25519_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:
|
volumes:
|
||||||
- ../../data:/app/data:ro
|
- ../../data:/app/data:ro
|
||||||
- ../../rtr-db:/app/rtr-db
|
- ../../rtr-db:/app/rtr-db
|
||||||
- ../../data:/app/slurm:ro
|
- ../../data:/app/slurm:ro
|
||||||
|
- ../../logs/server:/app/logs
|
||||||
# TLS mode example:
|
# TLS mode example:
|
||||||
# - ../../certs:/app/certs:ro
|
# - ../../certs:/app/certs:ro
|
||||||
|
|||||||
@ -12,7 +12,9 @@ startretries=3
|
|||||||
stopsignal=TERM
|
stopsignal=TERM
|
||||||
stopasgroup=true
|
stopasgroup=true
|
||||||
killasgroup=true
|
killasgroup=true
|
||||||
stdout_logfile=/dev/fd/1
|
stdout_logfile=/app/logs/rpki-rtr.stdout.log
|
||||||
stdout_logfile_maxbytes=0
|
stdout_logfile_maxbytes=50MB
|
||||||
stderr_logfile=/dev/fd/2
|
stdout_logfile_backups=10
|
||||||
stderr_logfile_maxbytes=0
|
stderr_logfile=/app/logs/rpki-rtr.stderr.log
|
||||||
|
stderr_logfile_maxbytes=50MB
|
||||||
|
stderr_logfile_backups=10
|
||||||
|
|||||||
@ -1,42 +1,41 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::net::IpAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
|
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
|
||||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
|
|
||||||
use rpki_rs::rtr::client::{Client, PayloadError, PayloadTarget};
|
use rpki_rs::rtr::client::{Client, PayloadError, PayloadTarget};
|
||||||
use rpki_rs::rtr::payload::{Action, Payload, Timing};
|
use rpki_rs::rtr::payload::{Action, Payload, Timing};
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT_SECS: u64 = 10;
|
|
||||||
const DEFAULT_STEPS: usize = 1;
|
const DEFAULT_STEPS: usize = 1;
|
||||||
|
const DEFAULT_STEP_TIMEOUT_SECS: u64 = 300;
|
||||||
|
const DEFAULT_PROGRESS_EVERY: u64 = 10_000;
|
||||||
|
|
||||||
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
|
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||||
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||||
|
|
||||||
type DynStream = Box<dyn AsyncStream>;
|
type DynStream = Box<dyn AsyncStream>;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Config {
|
struct Config {
|
||||||
addr: String,
|
addr: String,
|
||||||
version: u8,
|
|
||||||
mode: QueryMode,
|
|
||||||
steps: usize,
|
steps: usize,
|
||||||
follow: bool,
|
follow: bool,
|
||||||
transport: TransportConfig,
|
transport: TransportConfig,
|
||||||
assert_substr: Vec<String>,
|
assert_substr: Vec<String>,
|
||||||
assert_min_records: Option<usize>,
|
assert_min_records: Option<usize>,
|
||||||
print_records: bool,
|
print_records: bool,
|
||||||
}
|
step_timeout_secs: u64,
|
||||||
|
progress_every: u64,
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
enum QueryMode {
|
|
||||||
Reset,
|
|
||||||
SerialAuto,
|
|
||||||
Serial { session_id: u16, serial: u32 },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@ -50,54 +49,74 @@ impl Config {
|
|||||||
let mut assert_substr = Vec::new();
|
let mut assert_substr = Vec::new();
|
||||||
let mut assert_min_records = None;
|
let mut assert_min_records = None;
|
||||||
let mut print_records = false;
|
let mut print_records = false;
|
||||||
|
let mut step_timeout_secs = DEFAULT_STEP_TIMEOUT_SECS;
|
||||||
|
let mut progress_every = DEFAULT_PROGRESS_EVERY;
|
||||||
|
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--version" => {
|
"-h" | "--help" => {
|
||||||
let _ = args.next().ok_or_else(|| {
|
print_usage();
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--version requires value")
|
std::process::exit(0);
|
||||||
})?;
|
|
||||||
// rpki-rs v0.18 client only exposes Client::new without
|
|
||||||
// initial-version override. Keep this option as reserved.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"--steps" => {
|
"--steps" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--steps requires value")
|
io::Error::new(io::ErrorKind::InvalidInput, "--steps requires value")
|
||||||
})?;
|
})?;
|
||||||
steps = parse_usize_arg(&v, "--steps")?;
|
steps = parse_usize_arg(&v, "--steps")?;
|
||||||
|
if steps == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--steps must be >= 1",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"--follow" => {
|
"--follow" => {
|
||||||
follow = true;
|
follow = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
"--tls" => {
|
"--tls" => {
|
||||||
if matches!(transport, TransportConfig::Tcp) {
|
ensure_tls(&mut transport)?;
|
||||||
transport = TransportConfig::Tls(TlsConfig::default());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
"--ca-cert" => {
|
"--ca-cert" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--ca-cert requires path")
|
io::Error::new(io::ErrorKind::InvalidInput, "--ca-cert requires path")
|
||||||
})?;
|
})?;
|
||||||
ensure_tls(&mut transport)?.ca_cert = Some(PathBuf::from(v));
|
ensure_tls(&mut transport)?.ca_cert = Some(PathBuf::from(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
"--server-name" => {
|
"--server-name" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--server-name requires value")
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--server-name requires value",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
ensure_tls(&mut transport)?.server_name = Some(v);
|
ensure_tls(&mut transport)?.server_name = Some(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
"--client-cert" => {
|
"--client-cert" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--client-cert requires path")
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--client-cert requires path",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
ensure_tls(&mut transport)?.client_cert = Some(PathBuf::from(v));
|
ensure_tls(&mut transport)?.client_cert = Some(PathBuf::from(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
"--client-key" => {
|
"--client-key" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--client-key requires path")
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--client-key requires path",
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
ensure_tls(&mut transport)?.client_key = Some(PathBuf::from(v));
|
ensure_tls(&mut transport)?.client_key = Some(PathBuf::from(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
"--assert-substr" => {
|
"--assert-substr" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
@ -107,6 +126,7 @@ impl Config {
|
|||||||
})?;
|
})?;
|
||||||
assert_substr.push(v);
|
assert_substr.push(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
"--assert-min-records" => {
|
"--assert-min-records" => {
|
||||||
let v = args.next().ok_or_else(|| {
|
let v = args.next().ok_or_else(|| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
@ -116,21 +136,71 @@ impl Config {
|
|||||||
})?;
|
})?;
|
||||||
assert_min_records = Some(parse_usize_arg(&v, "--assert-min-records")?);
|
assert_min_records = Some(parse_usize_arg(&v, "--assert-min-records")?);
|
||||||
}
|
}
|
||||||
|
|
||||||
"--print-records" => {
|
"--print-records" => {
|
||||||
print_records = true;
|
print_records = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"--step-timeout-secs" => {
|
||||||
|
let v = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--step-timeout-secs requires value",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
step_timeout_secs = parse_u64_arg(&v, "--step-timeout-secs")?;
|
||||||
|
if step_timeout_secs == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--step-timeout-secs must be >= 1",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"--progress-every" => {
|
||||||
|
let v = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--progress-every requires value",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
progress_every = parse_u64_arg(&v, "--progress-every")?;
|
||||||
|
if progress_every == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--progress-every must be >= 1",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明确拒绝当前 wrapper 不支持的能力
|
||||||
|
"--version" => {
|
||||||
|
let _ = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--version requires value")
|
||||||
|
})?;
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--version is not exposed by this rpki client wrapper",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
"--timeout" => {
|
"--timeout" => {
|
||||||
let _ = args.next().ok_or_else(|| {
|
let _ = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--timeout requires value")
|
io::Error::new(io::ErrorKind::InvalidInput, "--timeout requires value")
|
||||||
})?;
|
})?;
|
||||||
// This binary relies on rpki-rs client's built-in IO timeout.
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--timeout is not exposed by this rpki client wrapper; use --step-timeout-secs instead",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
_ if arg.starts_with("--") => {
|
_ if arg.starts_with("--") => {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("unknown option '{}'", arg),
|
format!("unknown option '{}'", arg),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => positional.push(arg),
|
_ => positional.push(arg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,56 +209,72 @@ impl Config {
|
|||||||
let addr = positional
|
let addr = positional
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or_else(|| "127.0.0.1:323".to_string());
|
.unwrap_or_else(|| "127.0.0.1:323".to_string());
|
||||||
let version = positional
|
|
||||||
.next()
|
if let Some(extra) = positional.next() {
|
||||||
.map(|v| parse_u8_arg(&v, "version"))
|
return Err(io::Error::new(
|
||||||
.transpose()?
|
io::ErrorKind::InvalidInput,
|
||||||
.unwrap_or(2);
|
format!(
|
||||||
let mode = match positional.next().as_deref() {
|
"unexpected positional argument '{}'; only optional [addr] is supported",
|
||||||
None | Some("reset") => QueryMode::Reset,
|
extra
|
||||||
Some("serial") if positional.clone().next().is_none() => QueryMode::SerialAuto,
|
),
|
||||||
Some("serial") => {
|
));
|
||||||
let session_id = parse_u16_arg(
|
}
|
||||||
&positional.next().ok_or_else(|| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"serial mode requires session_id and serial",
|
|
||||||
)
|
|
||||||
})?,
|
|
||||||
"session_id",
|
|
||||||
)?;
|
|
||||||
let serial = parse_u32_arg(
|
|
||||||
&positional.next().ok_or_else(|| {
|
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "serial mode requires serial")
|
|
||||||
})?,
|
|
||||||
"serial",
|
|
||||||
)?;
|
|
||||||
QueryMode::Serial { session_id, serial }
|
|
||||||
}
|
|
||||||
Some(other) => {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!("invalid mode '{}', expected 'reset' or 'serial'", other),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let transport = finalize_transport(transport, &addr)?;
|
let transport = finalize_transport(transport, &addr)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
addr,
|
addr,
|
||||||
version,
|
|
||||||
mode,
|
|
||||||
steps,
|
steps,
|
||||||
follow,
|
follow,
|
||||||
transport,
|
transport,
|
||||||
assert_substr,
|
assert_substr,
|
||||||
assert_min_records,
|
assert_min_records,
|
||||||
print_records,
|
print_records,
|
||||||
|
step_timeout_secs,
|
||||||
|
progress_every,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_usage() {
|
||||||
|
eprintln!(
|
||||||
|
"\
|
||||||
|
rpki-rs-test-client
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
rpki-rs-test-client [OPTIONS] [ADDR]
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
rpki-rs-test-client
|
||||||
|
rpki-rs-test-client 127.0.0.1:323 --steps 1
|
||||||
|
rpki-rs-test-client 127.0.0.1:323 --steps 1 --step-timeout-secs 600
|
||||||
|
rpki-rs-test-client 127.0.0.1:323 --follow
|
||||||
|
rpki-rs-test-client 127.0.0.1:323 --assert-min-records 1 --assert-substr 192.0.2.
|
||||||
|
rpki-rs-test-client 127.0.0.1:3324 --tls --ca-cert certs/ca.pem --server-name localhost
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--steps <N> Number of client.step() calls to perform (default: 1)
|
||||||
|
--follow Keep calling step() forever
|
||||||
|
--tls Enable TLS
|
||||||
|
--ca-cert <PATH> CA certificate PEM file (required in TLS mode)
|
||||||
|
--server-name <NAME> TLS server name; required when ADDR host is an IP
|
||||||
|
--client-cert <PATH> Client certificate PEM file (optional, with --client-key)
|
||||||
|
--client-key <PATH> Client private key PEM file (optional, with --client-cert)
|
||||||
|
--assert-substr <TEXT> Assert final stable record dump contains substring
|
||||||
|
--assert-min-records <N> Assert final record count >= N
|
||||||
|
--print-records Print records after each successful step
|
||||||
|
--step-timeout-secs <N> Timeout for each step() call in seconds (default: 300)
|
||||||
|
--progress-every <N> Print apply progress every N updates (default: 10000)
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Not supported by this wrapper:
|
||||||
|
--version
|
||||||
|
--timeout
|
||||||
|
explicit serial bootstrap via session_id/serial
|
||||||
|
"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum TransportConfig {
|
enum TransportConfig {
|
||||||
Tcp,
|
Tcp,
|
||||||
@ -207,6 +293,7 @@ fn ensure_tls(transport: &mut TransportConfig) -> io::Result<&mut TlsConfig> {
|
|||||||
if matches!(transport, TransportConfig::Tcp) {
|
if matches!(transport, TransportConfig::Tcp) {
|
||||||
*transport = TransportConfig::Tls(TlsConfig::default());
|
*transport = TransportConfig::Tls(TlsConfig::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
match transport {
|
match transport {
|
||||||
TransportConfig::Tls(cfg) => Ok(cfg),
|
TransportConfig::Tls(cfg) => Ok(cfg),
|
||||||
TransportConfig::Tcp => Err(io::Error::other("tls configuration unavailable")),
|
TransportConfig::Tcp => Err(io::Error::other("tls configuration unavailable")),
|
||||||
@ -234,16 +321,26 @@ fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<Tran
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let server_name = cfg
|
let server_name = match cfg.server_name.take() {
|
||||||
.server_name
|
Some(name) => name,
|
||||||
.take()
|
None => {
|
||||||
.or_else(|| default_server_name_for_addr(addr))
|
let host = parse_host_from_addr(addr).ok_or_else(|| {
|
||||||
.ok_or_else(|| {
|
io::Error::new(
|
||||||
io::Error::new(
|
io::ErrorKind::InvalidInput,
|
||||||
io::ErrorKind::InvalidInput,
|
"failed to parse host from address",
|
||||||
"TLS mode requires --server-name or parseable host",
|
)
|
||||||
)
|
})?;
|
||||||
})?;
|
|
||||||
|
if host.parse::<IpAddr>().is_ok() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"TLS with IP address requires explicit --server-name",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
host
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(TransportConfig::Tls(TlsConfig {
|
Ok(TransportConfig::Tls(TlsConfig {
|
||||||
server_name: Some(server_name),
|
server_name: Some(server_name),
|
||||||
@ -255,169 +352,231 @@ fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<Tran
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Clone)]
|
||||||
struct InMemoryTarget {
|
struct SharedTarget {
|
||||||
records: Vec<Payload>,
|
inner: Arc<Mutex<TargetState>>,
|
||||||
timing: Option<Timing>,
|
|
||||||
announced: u64,
|
|
||||||
withdrawn: u64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InMemoryTarget {
|
#[derive(Debug)]
|
||||||
|
struct TargetState {
|
||||||
|
records: BTreeSet<Payload>,
|
||||||
|
timing: Option<Timing>,
|
||||||
|
announced_seen: u64,
|
||||||
|
withdrawn_seen: u64,
|
||||||
|
updates_applied_total: u64,
|
||||||
|
progress_every: u64,
|
||||||
|
apply_batches: u64,
|
||||||
|
last_apply_started_at: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TargetState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
records: BTreeSet::new(),
|
||||||
|
timing: None,
|
||||||
|
announced_seen: 0,
|
||||||
|
withdrawn_seen: 0,
|
||||||
|
updates_applied_total: 0,
|
||||||
|
progress_every: DEFAULT_PROGRESS_EVERY,
|
||||||
|
apply_batches: 0,
|
||||||
|
last_apply_started_at: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct TargetSnapshot {
|
||||||
|
records: Vec<Payload>,
|
||||||
|
timing: Option<Timing>,
|
||||||
|
announced_seen: u64,
|
||||||
|
withdrawn_seen: u64,
|
||||||
|
updates_applied_total: u64,
|
||||||
|
apply_batches: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharedTarget {
|
||||||
|
fn new(progress_every: u64) -> Self {
|
||||||
|
let state = TargetState {
|
||||||
|
progress_every,
|
||||||
|
..TargetState::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(Mutex::new(state)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot(&self) -> TargetSnapshot {
|
||||||
|
let guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
TargetSnapshot {
|
||||||
|
records: guard.records.iter().cloned().collect(),
|
||||||
|
timing: guard.timing,
|
||||||
|
announced_seen: guard.announced_seen,
|
||||||
|
withdrawn_seen: guard.withdrawn_seen,
|
||||||
|
updates_applied_total: guard.updates_applied_total,
|
||||||
|
apply_batches: guard.apply_batches,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payload_to_stable_text(payload: &Payload) -> String {
|
||||||
|
format!("{:?}", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TargetSnapshot {
|
||||||
fn dump_text(&self) -> String {
|
fn dump_text(&self) -> String {
|
||||||
self.records
|
self.records
|
||||||
.iter()
|
.iter()
|
||||||
.map(|p| format!("{:?}", p))
|
.map(SharedTarget::payload_to_stable_text)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadTarget for InMemoryTarget {
|
impl PayloadTarget for SharedTarget {
|
||||||
type Update = Vec<(Action, Payload)>;
|
type Update = Vec<(Action, Payload)>;
|
||||||
|
|
||||||
fn start(&mut self, reset: bool) -> Self::Update {
|
fn start(&mut self, reset: bool) -> Self::Update {
|
||||||
if reset {
|
if reset {
|
||||||
self.records.clear();
|
let mut guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
println!(
|
||||||
|
"[target] start reset=true | clearing existing records={}",
|
||||||
|
guard.records.len()
|
||||||
|
);
|
||||||
|
guard.records.clear();
|
||||||
|
} else {
|
||||||
|
let guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
println!(
|
||||||
|
"[target] start reset=false | current records={}",
|
||||||
|
guard.records.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply(&mut self, update: Self::Update, timing: Timing) -> Result<(), PayloadError> {
|
fn apply(&mut self, update: Self::Update, timing: Timing) -> Result<(), PayloadError> {
|
||||||
|
let total = update.len() as u64;
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
guard.apply_batches += 1;
|
||||||
|
guard.last_apply_started_at = Some(Instant::now());
|
||||||
|
println!(
|
||||||
|
"[target] apply batch #{} started | updates={} | current records={}",
|
||||||
|
guard.apply_batches,
|
||||||
|
total,
|
||||||
|
guard.records.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let started = Instant::now();
|
||||||
|
let mut local_processed: u64 = 0;
|
||||||
|
|
||||||
|
let progress_every = {
|
||||||
|
let guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
guard.progress_every
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut guard = self.inner.lock().expect("target mutex poisoned");
|
||||||
|
|
||||||
for (action, payload) in update {
|
for (action, payload) in update {
|
||||||
match action {
|
match action {
|
||||||
Action::Announce => {
|
Action::Announce => {
|
||||||
self.announced += 1;
|
guard.announced_seen += 1;
|
||||||
if self.records.iter().any(|p| p == &payload) {
|
if !guard.records.insert(payload) {
|
||||||
return Err(PayloadError::DuplicateAnnounce);
|
return Err(PayloadError::DuplicateAnnounce);
|
||||||
}
|
}
|
||||||
self.records.push(payload);
|
|
||||||
}
|
}
|
||||||
Action::Withdraw => {
|
Action::Withdraw => {
|
||||||
self.withdrawn += 1;
|
guard.withdrawn_seen += 1;
|
||||||
if let Some(pos) = self.records.iter().position(|p| p == &payload) {
|
if !guard.records.remove(&payload) {
|
||||||
self.records.swap_remove(pos);
|
|
||||||
} else {
|
|
||||||
return Err(PayloadError::UnknownWithdraw);
|
return Err(PayloadError::UnknownWithdraw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local_processed += 1;
|
||||||
|
guard.updates_applied_total += 1;
|
||||||
|
|
||||||
|
if local_processed % progress_every == 0 || local_processed == total {
|
||||||
|
println!(
|
||||||
|
"[target] apply progress | batch_processed={}/{} | total_updates_seen={} | records={} | elapsed={:.2?}",
|
||||||
|
local_processed,
|
||||||
|
total,
|
||||||
|
guard.updates_applied_total,
|
||||||
|
guard.records.len(),
|
||||||
|
started.elapsed(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.timing = Some(timing);
|
|
||||||
|
guard.timing = Some(timing);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"[target] apply batch complete | updates={} | records={} | announced_seen={} | withdrawn_seen={} | elapsed={:.2?}",
|
||||||
|
total,
|
||||||
|
guard.records.len(),
|
||||||
|
guard.announced_seen,
|
||||||
|
guard.withdrawn_seen,
|
||||||
|
started.elapsed(),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
fn print_step_summary(
|
||||||
async fn main() -> io::Result<()> {
|
step_no: usize,
|
||||||
let config = Config::from_args()?;
|
before: &TargetSnapshot,
|
||||||
println!("== rpki_rs_test_client ==");
|
after: &TargetSnapshot,
|
||||||
println!("target : {}", config.addr);
|
print_records: bool,
|
||||||
println!("version : {}", config.version);
|
) {
|
||||||
println!(
|
println!(
|
||||||
"mode : {}",
|
"[step] {} ok | records: {} -> {} | delta announce={} withdraw={} | delta updates={} | apply_batches={}",
|
||||||
match config.mode {
|
step_no,
|
||||||
QueryMode::Reset => "reset".to_string(),
|
before.records.len(),
|
||||||
QueryMode::SerialAuto => "serial(auto)".to_string(),
|
after.records.len(),
|
||||||
QueryMode::Serial { session_id, serial } => {
|
after.announced_seen.saturating_sub(before.announced_seen),
|
||||||
format!("serial sid={} serial={}", session_id, serial)
|
after.withdrawn_seen.saturating_sub(before.withdrawn_seen),
|
||||||
|
after
|
||||||
|
.updates_applied_total
|
||||||
|
.saturating_sub(before.updates_applied_total),
|
||||||
|
after.apply_batches,
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(timing) = after.timing {
|
||||||
|
println!(
|
||||||
|
"[step] {} timing | refresh={} retry={} expire={}",
|
||||||
|
step_no, timing.refresh, timing.retry, timing.expire
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if print_records {
|
||||||
|
println!("-- records after step {} --", step_no);
|
||||||
|
if after.records.is_empty() {
|
||||||
|
println!("(empty)");
|
||||||
|
} else {
|
||||||
|
for rec in &after.records {
|
||||||
|
println!("{}", SharedTarget::payload_to_stable_text(rec));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
|
||||||
println!("steps : {}", config.steps);
|
|
||||||
println!("follow : {}", config.follow);
|
|
||||||
println!(
|
|
||||||
"timeout : {}s (from rpki-rs client IO timeout)",
|
|
||||||
DEFAULT_TIMEOUT_SECS
|
|
||||||
);
|
|
||||||
|
|
||||||
if config.version != 2 {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"rpki-rs v0.18 client API does not expose initial-version override; please use version 2",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if matches!(config.mode, QueryMode::Serial { .. }) {
|
|
||||||
return Err(io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
"rpki-rs v0.18 Client::new cannot bootstrap explicit serial state; use reset mode",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let stream = connect_stream(&config).await?;
|
|
||||||
let target = InMemoryTarget::default();
|
|
||||||
let mut client = Client::new(stream, target, None);
|
|
||||||
|
|
||||||
let bootstrap_steps = match config.mode {
|
|
||||||
QueryMode::SerialAuto if config.steps < 2 => 2,
|
|
||||||
_ => config.steps,
|
|
||||||
};
|
|
||||||
if bootstrap_steps != config.steps {
|
|
||||||
println!(
|
|
||||||
"steps adjusted : {} -> {} (serial(auto) needs at least 2 steps)",
|
|
||||||
config.steps, bootstrap_steps
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx in 0..bootstrap_steps {
|
|
||||||
client.step().await.map_err(|err| {
|
|
||||||
io::Error::new(err.kind(), format!("step {} failed: {}", idx + 1, err))
|
|
||||||
})?;
|
|
||||||
println!("[step] bootstrap {} ok", idx + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.follow {
|
|
||||||
println!("[follow] enabled, entering continuous step loop");
|
|
||||||
let mut step_index = bootstrap_steps;
|
|
||||||
loop {
|
|
||||||
step_index += 1;
|
|
||||||
client.step().await.map_err(|err| {
|
|
||||||
io::Error::new(err.kind(), format!("step {} failed: {}", step_index, err))
|
|
||||||
})?;
|
|
||||||
println!("[step] follow {} ok", step_index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let negotiated_state = client.state();
|
|
||||||
let target = client.into_target();
|
|
||||||
println!("state : {:?}", negotiated_state);
|
|
||||||
if let Some(timing) = target.timing {
|
|
||||||
println!(
|
|
||||||
"timing : refresh={} retry={} expire={}",
|
|
||||||
timing.refresh, timing.retry, timing.expire
|
|
||||||
);
|
|
||||||
}
|
|
||||||
println!("records : {}", target.records.len());
|
|
||||||
println!(
|
|
||||||
"updates : announce={} withdraw={}",
|
|
||||||
target.announced, target.withdrawn
|
|
||||||
);
|
|
||||||
|
|
||||||
if config.print_records {
|
|
||||||
println!("-- records --");
|
|
||||||
for rec in &target.records {
|
|
||||||
println!("{:?}", rec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run_assertions(&config, &target)?;
|
|
||||||
println!("[assert] passed");
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_assertions(config: &Config, target: &InMemoryTarget) -> io::Result<()> {
|
fn run_assertions(config: &Config, snapshot: &TargetSnapshot) -> io::Result<()> {
|
||||||
if let Some(min) = config.assert_min_records
|
if let Some(min) = config.assert_min_records
|
||||||
&& target.records.len() < min
|
&& snapshot.records.len() < min
|
||||||
{
|
{
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
"assertion failed: records {} < {}",
|
"assertion failed: records {} < {}",
|
||||||
target.records.len(),
|
snapshot.records.len(),
|
||||||
min
|
min
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.assert_substr.is_empty() {
|
if !config.assert_substr.is_empty() {
|
||||||
let dump = target.dump_text();
|
let dump = snapshot.dump_text();
|
||||||
for needle in &config.assert_substr {
|
for needle in &config.assert_substr {
|
||||||
if !dump.contains(needle) {
|
if !dump.contains(needle) {
|
||||||
return Err(io::Error::other(format!(
|
return Err(io::Error::other(format!(
|
||||||
@ -427,6 +586,136 @@ fn run_assertions(config: &Config, target: &InMemoryTarget) -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> io::Result<()> {
|
||||||
|
let config = Config::from_args()?;
|
||||||
|
|
||||||
|
println!("== rpki-rs-test-client ==");
|
||||||
|
println!("target : {}", config.addr);
|
||||||
|
println!("steps : {}", config.steps);
|
||||||
|
println!("follow : {}", config.follow);
|
||||||
|
println!("step_timeout_secs : {}", config.step_timeout_secs);
|
||||||
|
println!("progress_every : {}", config.progress_every);
|
||||||
|
|
||||||
|
match &config.transport {
|
||||||
|
TransportConfig::Tcp => {
|
||||||
|
println!("transport : tcp");
|
||||||
|
}
|
||||||
|
TransportConfig::Tls(tls) => {
|
||||||
|
println!("transport : tls");
|
||||||
|
println!(
|
||||||
|
"server_name : {}",
|
||||||
|
tls.server_name.as_deref().unwrap_or("<unset>")
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"ca_cert : {}",
|
||||||
|
tls.ca_cert
|
||||||
|
.as_ref()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.unwrap_or_else(|| "<unset>".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = connect_stream(&config).await?;
|
||||||
|
let target = SharedTarget::new(config.progress_every);
|
||||||
|
let inspect = target.clone();
|
||||||
|
|
||||||
|
let mut client = Client::new(stream, target, None);
|
||||||
|
|
||||||
|
for idx in 0..config.steps {
|
||||||
|
let step_no = idx + 1;
|
||||||
|
let before = inspect.snapshot();
|
||||||
|
|
||||||
|
println!("[step] {} begin", step_no);
|
||||||
|
let step_started = Instant::now();
|
||||||
|
|
||||||
|
timeout(Duration::from_secs(config.step_timeout_secs), client.step())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
format!(
|
||||||
|
"step {} timed out after {}s",
|
||||||
|
step_no, config.step_timeout_secs
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.map_err(|err| io::Error::new(err.kind(), format!("step {} failed: {}", step_no, err)))?;
|
||||||
|
|
||||||
|
let after = inspect.snapshot();
|
||||||
|
print_step_summary(step_no, &before, &after, config.print_records);
|
||||||
|
println!(
|
||||||
|
"[step] {} finished in {:.2?}",
|
||||||
|
step_no,
|
||||||
|
step_started.elapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.follow {
|
||||||
|
let mut step_index = config.steps;
|
||||||
|
println!("[follow] enabled, entering continuous step loop");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
step_index += 1;
|
||||||
|
let before = inspect.snapshot();
|
||||||
|
|
||||||
|
println!("[step] {} begin", step_index);
|
||||||
|
let step_started = Instant::now();
|
||||||
|
|
||||||
|
timeout(Duration::from_secs(config.step_timeout_secs), client.step())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::TimedOut,
|
||||||
|
format!(
|
||||||
|
"step {} timed out after {}s",
|
||||||
|
step_index, config.step_timeout_secs
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.map_err(|err| {
|
||||||
|
io::Error::new(err.kind(), format!("step {} failed: {}", step_index, err))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let after = inspect.snapshot();
|
||||||
|
print_step_summary(step_index, &before, &after, config.print_records);
|
||||||
|
println!(
|
||||||
|
"[step] {} finished in {:.2?}",
|
||||||
|
step_index,
|
||||||
|
step_started.elapsed()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = client.state();
|
||||||
|
let final_snapshot = inspect.snapshot();
|
||||||
|
|
||||||
|
println!("state : {:?}", state);
|
||||||
|
if let Some(timing) = final_snapshot.timing {
|
||||||
|
println!(
|
||||||
|
"timing : refresh={} retry={} expire={}",
|
||||||
|
timing.refresh, timing.retry, timing.expire
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!("records : {}", final_snapshot.records.len());
|
||||||
|
println!(
|
||||||
|
"updates_seen : announce={} withdraw={}",
|
||||||
|
final_snapshot.announced_seen, final_snapshot.withdrawn_seen
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"updates_applied : {}",
|
||||||
|
final_snapshot.updates_applied_total
|
||||||
|
);
|
||||||
|
println!("apply_batches : {}", final_snapshot.apply_batches);
|
||||||
|
|
||||||
|
run_assertions(&config, &final_snapshot)?;
|
||||||
|
println!("[assert] passed");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -440,22 +729,26 @@ async fn connect_stream(config: &Config) -> io::Result<DynStream> {
|
|||||||
async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream> {
|
async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream> {
|
||||||
let stream = TcpStream::connect(addr).await?;
|
let stream = TcpStream::connect(addr).await?;
|
||||||
let connector = build_tls_connector(tls)?;
|
let connector = build_tls_connector(tls)?;
|
||||||
|
|
||||||
let server_name_str = tls
|
let server_name_str = tls
|
||||||
.server_name
|
.server_name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing TLS server name"))?;
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing TLS server name"))?;
|
||||||
|
|
||||||
let server_name = ServerName::try_from(server_name_str.clone()).map_err(|err| {
|
let server_name = ServerName::try_from(server_name_str.clone()).map_err(|err| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
format!("invalid TLS server name '{}': {}", server_name_str, err),
|
format!("invalid TLS server name '{}': {}", server_name_str, err),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let tls_stream = connector.connect(server_name, stream).await.map_err(|err| {
|
let tls_stream = connector.connect(server_name, stream).await.map_err(|err| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
io::ErrorKind::ConnectionAborted,
|
io::ErrorKind::ConnectionAborted,
|
||||||
format!("TLS handshake failed: {}", err),
|
format!("TLS handshake failed: {}", err),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(Box::new(tls_stream))
|
Ok(Box::new(tls_stream))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -464,10 +757,11 @@ fn build_tls_connector(tls: &TlsConfig) -> io::Result<TlsConnector> {
|
|||||||
.ca_cert
|
.ca_cert
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing TLS CA cert"))?;
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing TLS CA cert"))?;
|
||||||
|
|
||||||
let ca_certs = load_certs(ca_cert_path)?;
|
let ca_certs = load_certs(ca_cert_path)?;
|
||||||
|
|
||||||
let mut roots = RootCertStore::empty();
|
let mut roots = RootCertStore::empty();
|
||||||
let (added, _) = roots.add_parsable_certificates(ca_certs);
|
let (added, _ignored) = roots.add_parsable_certificates(ca_certs);
|
||||||
if added == 0 {
|
if added == 0 {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
@ -476,6 +770,7 @@ fn build_tls_connector(tls: &TlsConfig) -> io::Result<TlsConnector> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let builder = RustlsClientConfig::builder().with_root_certificates(roots);
|
let builder = RustlsClientConfig::builder().with_root_certificates(roots);
|
||||||
|
|
||||||
let cfg = match (&tls.client_cert, &tls.client_key) {
|
let cfg = match (&tls.client_cert, &tls.client_key) {
|
||||||
(Some(cert_path), Some(key_path)) => {
|
(Some(cert_path), Some(key_path)) => {
|
||||||
let certs = load_certs(cert_path)?;
|
let certs = load_certs(cert_path)?;
|
||||||
@ -490,6 +785,7 @@ fn build_tls_connector(tls: &TlsConfig) -> io::Result<TlsConnector> {
|
|||||||
(None, None) => builder.with_no_client_auth(),
|
(None, None) => builder.with_no_client_auth(),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(TlsConnector::from(Arc::new(cfg)))
|
Ok(TlsConnector::from(Arc::new(cfg)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,12 +794,14 @@ fn load_certs(path: &Path) -> io::Result<Vec<CertificateDer<'static>>> {
|
|||||||
let certs = rustls_pemfile::certs(&mut reader)
|
let certs = rustls_pemfile::certs(&mut reader)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
|
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
|
||||||
|
|
||||||
if certs.is_empty() {
|
if certs.is_empty() {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
format!("no certs found in {}", path.display()),
|
format!("no certs found in {}", path.display()),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(certs)
|
Ok(certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -519,40 +817,13 @@ fn load_private_key(path: &Path) -> io::Result<PrivateKeyDer<'static>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_server_name_for_addr(addr: &str) -> Option<String> {
|
fn parse_host_from_addr(addr: &str) -> Option<String> {
|
||||||
if let Some(rest) = addr.strip_prefix('[') {
|
if let Some(rest) = addr.strip_prefix('[') {
|
||||||
return rest.split(']').next().map(str::to_string);
|
return rest.split(']').next().map(str::to_string);
|
||||||
}
|
}
|
||||||
addr.rsplit_once(':').map(|(host, _)| host.to_string())
|
addr.rsplit_once(':').map(|(host, _)| host.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_u8_arg(value: &str, name: &str) -> io::Result<u8> {
|
|
||||||
value.parse::<u8>().map_err(|err| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!("invalid {} '{}': {}", name, value, err),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_u16_arg(value: &str, name: &str) -> io::Result<u16> {
|
|
||||||
value.parse::<u16>().map_err(|err| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!("invalid {} '{}': {}", name, value, err),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_u32_arg(value: &str, name: &str) -> io::Result<u32> {
|
|
||||||
value.parse::<u32>().map_err(|err| {
|
|
||||||
io::Error::new(
|
|
||||||
io::ErrorKind::InvalidInput,
|
|
||||||
format!("invalid {} '{}': {}", name, value, err),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_usize_arg(value: &str, name: &str) -> io::Result<usize> {
|
fn parse_usize_arg(value: &str, name: &str) -> io::Result<usize> {
|
||||||
value.parse::<usize>().map_err(|err| {
|
value.parse::<usize>().map_err(|err| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
@ -561,3 +832,12 @@ fn parse_usize_arg(value: &str, name: &str) -> io::Result<usize> {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_u64_arg(value: &str, name: &str) -> io::Result<u64> {
|
||||||
|
value.parse::<u64>().map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid {} '{}': {}", name, value, err),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,48 +1,11 @@
|
|||||||
# rtr_debug_client
|
# rtr_debug_client
|
||||||
|
|
||||||
`rtr_debug_client` 是一个轻量级的 RTR 调试客户端,用于手工联调和协议行为观察。
|
`rtr_debug_client` 是用于 RTR 协议联调的命令行调试客户端,支持 `TCP`、`TLS`、`SSH` 三种传输。
|
||||||
|
|
||||||
它适合以下场景:
|
它用于:
|
||||||
- 在开发阶段验证 RTR server 的行为
|
- 手动发送 `Reset Query`、`Serial Query`
|
||||||
- 发送 `Reset Query` 和 `Serial Query`
|
- 持续接收并打印服务端 PDU
|
||||||
- 观察服务端返回的各类 PDU
|
- 观察 `session_id`、`serial`、`EndOfData` timing hint、`ErrorReport` 等状态变化
|
||||||
- 检查会话状态、`session_id`、`serial` 的变化
|
|
||||||
- 排查 `ErrorReport`、`CacheReset`、`SerialNotify`、`RouterKey`、`ASPA`
|
|
||||||
- 联调纯 TCP 和 TLS 两种 RTR 传输方式
|
|
||||||
|
|
||||||
它不是生产级 router client,而是一个便于调试和观察协议细节的小工具。
|
|
||||||
|
|
||||||
## 当前支持的能力
|
|
||||||
|
|
||||||
当前版本支持:
|
|
||||||
- 纯 TCP 连接
|
|
||||||
- TLS 连接
|
|
||||||
- TLS 服务端证书校验
|
|
||||||
- 可选的 TLS 客户端证书认证
|
|
||||||
- 发送 `Reset Query`
|
|
||||||
- 发送 `Serial Query`
|
|
||||||
- 保持长连接持续接收服务端 PDU
|
|
||||||
- 格式化展示以下 PDU:
|
|
||||||
- `Serial Notify`
|
|
||||||
- `Serial Query`
|
|
||||||
- `Reset Query`
|
|
||||||
- `Cache Response`
|
|
||||||
- `IPv4 Prefix`
|
|
||||||
- `IPv6 Prefix`
|
|
||||||
- `Router Key`
|
|
||||||
- `ASPA`
|
|
||||||
- `End of Data`
|
|
||||||
- `Cache Reset`
|
|
||||||
- `Error Report`
|
|
||||||
- 结构化展示 `ErrorReport`:
|
|
||||||
- 错误码及语义名称
|
|
||||||
- encapsulated PDU 的 header 摘要
|
|
||||||
- encapsulated PDU 原始 hex
|
|
||||||
- arbitrary text 是否为 UTF-8
|
|
||||||
- arbitrary text 内容
|
|
||||||
- 根据 `EndOfData` 的 timing hint 自动轮询
|
|
||||||
- 收到 `ErrorReport` 后默认暂停自动轮询
|
|
||||||
- 通过 `--keep-after-error` 保持错误后的自动轮询
|
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
@ -52,8 +15,6 @@ cargo build --bin rtr_debug_client
|
|||||||
|
|
||||||
## 基本用法
|
## 基本用法
|
||||||
|
|
||||||
基本形式:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
||||||
```
|
```
|
||||||
@ -62,32 +23,22 @@ cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id>
|
|||||||
- `addr`: `127.0.0.1:323`
|
- `addr`: `127.0.0.1:323`
|
||||||
- `version`: `1`
|
- `version`: `1`
|
||||||
- `mode`: `reset`
|
- `mode`: `reset`
|
||||||
- `timeout`: `30`
|
- `--timeout`: `30`
|
||||||
- `poll`: `600`
|
- `--poll`: `600`
|
||||||
|
|
||||||
## TCP 示例
|
## TCP 用法
|
||||||
|
|
||||||
发送 `Reset Query`:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 reset
|
||||||
```
|
```
|
||||||
|
|
||||||
发送 `Serial Query`:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- 127.0.0.1:323 1 serial 42 100
|
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:323 1 reset --keep-after-error
|
|
||||||
```
|
|
||||||
|
|
||||||
## TLS 示例
|
|
||||||
|
|
||||||
只做服务端证书校验:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- \
|
cargo run --bin rtr_debug_client -- \
|
||||||
@ -97,7 +48,7 @@ cargo run --bin rtr_debug_client -- \
|
|||||||
--server-name localhost
|
--server-name localhost
|
||||||
```
|
```
|
||||||
|
|
||||||
双向 TLS 认证:
|
双向 TLS:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- \
|
cargo run --bin rtr_debug_client -- \
|
||||||
@ -109,127 +60,91 @@ cargo run --bin rtr_debug_client -- \
|
|||||||
--client-key tests/fixtures/tls/client-good.key
|
--client-key tests/fixtures/tls/client-good.key
|
||||||
```
|
```
|
||||||
|
|
||||||
双向 TLS + 错误后继续自动轮询:
|
## 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
|
```sh
|
||||||
cargo run --bin rtr_debug_client -- \
|
cargo run --bin rtr_debug_client -- \
|
||||||
127.0.0.1:324 1 reset \
|
127.0.0.1:22 1 reset \
|
||||||
--tls \
|
--ssh \
|
||||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
--ssh-user rpki-rtr \
|
||||||
--server-name localhost \
|
--ssh-key certs/rtr-client.key \
|
||||||
--client-cert tests/fixtures/tls/client-good.crt \
|
--ssh-known-hosts certs/known_hosts
|
||||||
--client-key tests/fixtures/tls/client-good.key \
|
|
||||||
--keep-after-error
|
|
||||||
```
|
```
|
||||||
|
|
||||||
说明:
|
### 2. 使用固定服务端公钥校验
|
||||||
- 开启 `--tls` 时必须提供 `--ca-cert`
|
|
||||||
- 如果目标地址本身不适合直接作为 TLS 名称,显式提供 `--server-name`
|
|
||||||
- 客户端认证必须同时提供 `--client-cert` 和 `--client-key`
|
|
||||||
|
|
||||||
## 命令行参数
|
```sh
|
||||||
|
cargo run --bin rtr_debug_client -- \
|
||||||
|
127.0.0.1:22 1 reset \
|
||||||
|
--ssh \
|
||||||
|
--ssh-user rpki-rtr \
|
||||||
|
--ssh-key certs/rtr-client.key \
|
||||||
|
--ssh-server-key certs/ssh_host_ed25519_key.pub
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 自定义 subsystem 名称
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --bin rtr_debug_client -- \
|
||||||
|
127.0.0.1:22 1 reset \
|
||||||
|
--ssh \
|
||||||
|
--ssh-user rpki-rtr \
|
||||||
|
--ssh-key certs/rtr-client.key \
|
||||||
|
--ssh-known-hosts certs/known_hosts \
|
||||||
|
--ssh-subsystem rpki-rtr
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
|
||||||
|
通用参数:
|
||||||
|
- `--timeout <secs>`:读取 PDU 超时时间(秒)
|
||||||
|
- `--poll <secs>`:默认自动轮询间隔(秒)
|
||||||
|
- `--keep-after-error`:收到 `ErrorReport` 后不暂停自动轮询
|
||||||
|
- `--summary-only`:仅打印摘要,抑制 payload PDU 详细内容
|
||||||
|
|
||||||
|
TLS 参数:
|
||||||
- `--tls`
|
- `--tls`
|
||||||
使用 TLS 而不是纯 TCP。
|
|
||||||
|
|
||||||
- `--ca-cert <path>`
|
- `--ca-cert <path>`
|
||||||
用于校验服务端证书的 CA 证书文件,PEM 格式。
|
|
||||||
|
|
||||||
- `--server-name <name>`
|
- `--server-name <name>`
|
||||||
TLS 握手时用于校验证书的服务端名称。
|
|
||||||
|
|
||||||
- `--client-cert <path>`
|
- `--client-cert <path>`
|
||||||
双向 TLS 时使用的客户端证书,PEM 格式。
|
|
||||||
|
|
||||||
- `--client-key <path>`
|
- `--client-key <path>`
|
||||||
与 `--client-cert` 配套的客户端私钥,PEM 格式。
|
|
||||||
|
|
||||||
- `--timeout <secs>`
|
SSH 参数:
|
||||||
等待下一个 PDU 的读取超时时间,单位秒。
|
- `--ssh`
|
||||||
|
- `--ssh-user <name>`
|
||||||
|
- `--ssh-key <path>`(OpenSSH 私钥)
|
||||||
|
- `--ssh-subsystem <name>`(默认 `rpki-rtr`)
|
||||||
|
- `--ssh-known-hosts <path>` 或 `--ssh-server-key <path>`(二选一,必须提供)
|
||||||
|
|
||||||
- `--poll <secs>`
|
## SSH 连通性测试建议
|
||||||
在尚未拿到 `EndOfData` timing hint 前,默认使用的自动轮询间隔。默认值为 `600` 秒,对齐 draft 第 6 节的默认 Retry Interval。
|
|
||||||
|
|
||||||
- `--keep-after-error`
|
如果你已经在 Docker 中启动了支持 SSH 的 RTR server,可按以下方式验证:
|
||||||
收到 `ErrorReport` 后不暂停自动轮询。
|
|
||||||
|
|
||||||
## 运行中可用命令
|
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`
|
- `help`
|
||||||
显示帮助。
|
|
||||||
|
|
||||||
- `state`
|
- `state`
|
||||||
打印当前客户端状态。
|
- `version` / `version <n>`
|
||||||
|
|
||||||
- `reset`
|
- `reset`
|
||||||
发送 `Reset Query`。
|
- `serial` / `serial <sid> <serial>`
|
||||||
|
- `timeout` / `timeout <secs>`
|
||||||
- `serial`
|
- `poll` / `poll <secs>` / `poll pause` / `poll resume`
|
||||||
使用当前 `session_id` 和 `serial` 发送 `Serial Query`。
|
|
||||||
|
|
||||||
- `serial <sid> <serial>`
|
|
||||||
使用显式参数发送 `Serial Query`。
|
|
||||||
|
|
||||||
- `timeout`
|
|
||||||
查看当前读取超时设置。
|
|
||||||
|
|
||||||
- `timeout <secs>`
|
|
||||||
修改读取超时。
|
|
||||||
|
|
||||||
- `poll`
|
|
||||||
查看当前自动轮询间隔、轮询来源以及暂停状态。
|
|
||||||
|
|
||||||
- `poll <secs>`
|
|
||||||
手工覆盖当前轮询间隔。
|
|
||||||
|
|
||||||
- `poll pause`
|
|
||||||
暂停自动轮询。
|
|
||||||
|
|
||||||
- `poll resume`
|
|
||||||
恢复自动轮询。
|
|
||||||
|
|
||||||
- `keep-after-error`
|
- `keep-after-error`
|
||||||
查看当前是否启用了错误后持续轮询。
|
- `output` / `output verbose` / `output summary`
|
||||||
|
|
||||||
- `quit`
|
- `quit`
|
||||||
退出客户端。
|
|
||||||
|
|
||||||
## 自动轮询行为
|
|
||||||
|
|
||||||
客户端会保持连接,并周期性地向服务端发起下一次查询。
|
|
||||||
|
|
||||||
选择下一次轮询间隔的优先级如下:
|
|
||||||
1. `retry`,当最近一次 `ErrorReport` 是 `No Data Available` 或 `Transport Failure`
|
|
||||||
2. `refresh`,如果已经从 `EndOfData` 中拿到
|
|
||||||
3. 启动参数里的默认轮询间隔
|
|
||||||
|
|
||||||
收到 `ErrorReport` 后的默认行为:
|
|
||||||
- 默认暂停自动轮询
|
|
||||||
- 连接保持不关,方便继续观察
|
|
||||||
- 你可以手工输入 `reset`、`serial` 或 `poll resume` 继续
|
|
||||||
|
|
||||||
如果带了 `--keep-after-error`:
|
|
||||||
- 收到 `ErrorReport` 后不会暂停
|
|
||||||
- 会继续按当前有效轮询间隔自动轮询
|
|
||||||
|
|
||||||
特殊情况:
|
|
||||||
- 当最近一次错误是 `No Data Available` 或 `Transport Failure` 时,恢复自动轮询后会优先参考 `retry`,而不是继续只看 `refresh`
|
|
||||||
|
|
||||||
## ErrorReport 展示内容
|
|
||||||
|
|
||||||
`ErrorReport` 会展示以下内容:
|
|
||||||
- 错误码及其语义名称
|
|
||||||
- encapsulated PDU 长度
|
|
||||||
- encapsulated PDU 的 header 摘要
|
|
||||||
- PDU 类型
|
|
||||||
- version
|
|
||||||
- length
|
|
||||||
- field1(按类型解释为 `session_id` 或 `error_code`)
|
|
||||||
- encapsulated PDU 原始 hex
|
|
||||||
- arbitrary text 长度
|
|
||||||
- arbitrary text 是否是 UTF-8
|
|
||||||
- arbitrary text 内容
|
|
||||||
|
|
||||||
这样在排查协议问题时,不需要先手工拆原始 hex,就能快速知道是哪一个请求触发了错误。
|
|
||||||
|
|||||||
@ -2,11 +2,20 @@ use std::env;
|
|||||||
use std::future::pending;
|
use std::future::pending;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use russh::client;
|
||||||
|
use russh::keys::{
|
||||||
|
PrivateKeyWithHashAlg, check_known_hosts_path, load_public_key, load_secret_key,
|
||||||
|
};
|
||||||
|
use russh::{ChannelStream, client::Msg as SshClientMsg};
|
||||||
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
|
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
|
||||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||||
use tokio::io::{self as tokio_io, AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader, WriteHalf};
|
use tokio::io::{
|
||||||
|
self as tokio_io, AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader, ReadBuf, WriteHalf,
|
||||||
|
};
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::{Duration, Instant, timeout};
|
use tokio::time::{Duration, Instant, timeout};
|
||||||
use tokio_rustls::TlsConnector;
|
use tokio_rustls::TlsConnector;
|
||||||
@ -19,8 +28,29 @@ use crate::pretty::{parse_end_of_data_info, parse_serial_notify_serial, print_pd
|
|||||||
use crate::protocol::{PduHeader, PduType, QueryMode};
|
use crate::protocol::{PduHeader, PduType, QueryMode};
|
||||||
use crate::wire::{read_pdu, send_reset_query, send_serial_query};
|
use crate::wire::{read_pdu, send_reset_query, send_serial_query};
|
||||||
|
|
||||||
|
macro_rules! println {
|
||||||
|
() => {
|
||||||
|
::std::println!();
|
||||||
|
};
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z");
|
||||||
|
::std::println!("[{}] {}", ts, format_args!($($arg)*));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! eprintln {
|
||||||
|
() => {
|
||||||
|
::std::eprintln!();
|
||||||
|
};
|
||||||
|
($($arg:tt)*) => {{
|
||||||
|
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f%:z");
|
||||||
|
::std::eprintln!("[{}] {}", ts, format_args!($($arg)*));
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
|
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
|
||||||
const DEFAULT_POLL_INTERVAL_SECS: u64 = 600;
|
const DEFAULT_POLL_INTERVAL_SECS: u64 = 600;
|
||||||
|
const DEFAULT_SSH_SUBSYSTEM_NAME: &str = "rpki-rtr";
|
||||||
|
|
||||||
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
|
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||||
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||||
@ -60,7 +90,9 @@ async fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
print_help();
|
if config.output_mode == OutputMode::Verbose {
|
||||||
|
print_help();
|
||||||
|
}
|
||||||
|
|
||||||
let mut state = ClientState::new(
|
let mut state = ClientState::new(
|
||||||
config.version,
|
config.version,
|
||||||
@ -78,7 +110,9 @@ async fn main() -> io::Result<()> {
|
|||||||
let stream = loop {
|
let stream = loop {
|
||||||
match connect_stream(&config).await {
|
match connect_stream(&config).await {
|
||||||
Ok(stream) => {
|
Ok(stream) => {
|
||||||
println!("connected to {}", config.addr);
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("connected to {}", config.addr);
|
||||||
|
}
|
||||||
break stream;
|
break stream;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -175,10 +209,12 @@ async fn main() -> io::Result<()> {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!(
|
if state.output_mode == OutputMode::Verbose {
|
||||||
"[timeout] no PDU received in {}s, connection kept open.",
|
println!(
|
||||||
state.read_timeout_secs
|
"[timeout] no PDU received in {}s, connection kept open.",
|
||||||
);
|
state.read_timeout_secs
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,7 +224,9 @@ async fn main() -> io::Result<()> {
|
|||||||
if reconnect {
|
if reconnect {
|
||||||
let delay = state.reconnect_delay_secs();
|
let delay = state.reconnect_delay_secs();
|
||||||
state.current_session_id = None;
|
state.current_session_id = None;
|
||||||
println!("[reconnect] transport disconnected, retry after {}s", delay);
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("[reconnect] transport disconnected, retry after {}s", delay);
|
||||||
|
}
|
||||||
|
|
||||||
let reconnect_sleep = tokio::time::sleep(Duration::from_secs(delay));
|
let reconnect_sleep = tokio::time::sleep(Duration::from_secs(delay));
|
||||||
tokio::pin!(reconnect_sleep);
|
tokio::pin!(reconnect_sleep);
|
||||||
@ -235,7 +273,9 @@ async fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if reconnect_now {
|
if reconnect_now {
|
||||||
println!("[reconnect] user requested immediate reconnect");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("[reconnect] user requested immediate reconnect");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,28 +292,36 @@ async fn send_resume_query(
|
|||||||
state.serial = None;
|
state.serial = None;
|
||||||
state.current_session_id = None;
|
state.current_session_id = None;
|
||||||
send_reset_query(writer, state.version).await?;
|
send_reset_query(writer, state.version).await?;
|
||||||
println!("reconnected, send Reset Query (forced)");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("reconnected, send Reset Query (forced)");
|
||||||
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
match (state.session_id, state.serial) {
|
match (state.session_id, state.serial) {
|
||||||
(Some(session_id), Some(serial)) => {
|
(Some(session_id), Some(serial)) => {
|
||||||
println!(
|
if state.output_mode == OutputMode::Verbose {
|
||||||
"reconnected, send Serial Query with session_id={}, serial={}",
|
println!(
|
||||||
session_id, serial
|
"reconnected, send Serial Query with session_id={}, serial={}",
|
||||||
);
|
session_id, serial
|
||||||
|
);
|
||||||
|
}
|
||||||
send_serial_query(writer, state.version, session_id, serial).await?;
|
send_serial_query(writer, state.version, session_id, serial).await?;
|
||||||
}
|
}
|
||||||
_ => match mode {
|
_ => match mode {
|
||||||
QueryMode::Reset => {
|
QueryMode::Reset => {
|
||||||
send_reset_query(writer, state.version).await?;
|
send_reset_query(writer, state.version).await?;
|
||||||
println!("sent Reset Query");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("sent Reset Query");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
QueryMode::Serial { session_id, serial } => {
|
QueryMode::Serial { session_id, serial } => {
|
||||||
state.session_id = Some(*session_id);
|
state.session_id = Some(*session_id);
|
||||||
state.serial = Some(*serial);
|
state.serial = Some(*serial);
|
||||||
send_serial_query(writer, state.version, *session_id, *serial).await?;
|
send_serial_query(writer, state.version, *session_id, *serial).await?;
|
||||||
println!("sent Serial Query");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("sent Serial Query");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -317,8 +365,6 @@ async fn handle_incoming_pdu(
|
|||||||
state.session_id = Some(session_id);
|
state.session_id = Some(session_id);
|
||||||
state.current_session_id = Some(session_id);
|
state.current_session_id = Some(session_id);
|
||||||
|
|
||||||
println!();
|
|
||||||
|
|
||||||
if let Some(eod) = eod {
|
if let Some(eod) = eod {
|
||||||
state.serial = Some(eod.serial);
|
state.serial = Some(eod.serial);
|
||||||
state.refresh = eod.refresh;
|
state.refresh = eod.refresh;
|
||||||
@ -326,31 +372,44 @@ async fn handle_incoming_pdu(
|
|||||||
state.expire = eod.expire;
|
state.expire = eod.expire;
|
||||||
state.last_error_code = None;
|
state.last_error_code = None;
|
||||||
|
|
||||||
println!(
|
|
||||||
"updated client state: session_id={}, serial={}",
|
|
||||||
session_id, eod.serial
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(refresh) = eod.refresh {
|
|
||||||
println!("refresh : {}", refresh);
|
|
||||||
}
|
|
||||||
if let Some(retry) = eod.retry {
|
|
||||||
println!("retry : {}", retry);
|
|
||||||
}
|
|
||||||
if let Some(expire) = eod.expire {
|
|
||||||
println!("expire : {}", expire);
|
|
||||||
}
|
|
||||||
|
|
||||||
state.schedule_next_poll();
|
state.schedule_next_poll();
|
||||||
println!(
|
if state.output_mode == OutputMode::Verbose {
|
||||||
"next auto poll scheduled after {}s",
|
println!();
|
||||||
state.effective_poll_secs()
|
println!(
|
||||||
);
|
"updated client state: session_id={}, serial={}",
|
||||||
|
session_id, eod.serial
|
||||||
|
);
|
||||||
|
if let Some(refresh) = eod.refresh {
|
||||||
|
println!("refresh : {}", refresh);
|
||||||
|
}
|
||||||
|
if let Some(retry) = eod.retry {
|
||||||
|
println!("retry : {}", retry);
|
||||||
|
}
|
||||||
|
if let Some(expire) = eod.expire {
|
||||||
|
println!("expire : {}", expire);
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"next auto poll scheduled after {}s",
|
||||||
|
state.effective_poll_secs()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"EndOfData: session_id={}, serial={}, next_poll={}s",
|
||||||
|
session_id,
|
||||||
|
eod.serial,
|
||||||
|
state.effective_poll_secs()
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!(
|
if state.output_mode == OutputMode::Verbose {
|
||||||
"updated client state: session_id={}, serial=<unknown>",
|
println!();
|
||||||
session_id
|
println!(
|
||||||
);
|
"updated client state: session_id={}, serial=<unknown>",
|
||||||
|
session_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("EndOfData: session_id={}, serial=<unknown>", session_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.output_mode == OutputMode::SummaryOnly
|
if state.output_mode == OutputMode::SummaryOnly
|
||||||
@ -363,8 +422,10 @@ async fn handle_incoming_pdu(
|
|||||||
state.skipped_payload_pdu_count_in_round = 0;
|
state.skipped_payload_pdu_count_in_round = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("received EndOfData, keep connection open.");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
println!();
|
println!("received EndOfData, keep connection open.");
|
||||||
|
println!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PduType::SerialNotify => {
|
PduType::SerialNotify => {
|
||||||
@ -442,27 +503,35 @@ async fn handle_incoming_pdu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_poll_tick(writer: &mut ClientWriter, state: &mut ClientState) -> io::Result<()> {
|
async fn handle_poll_tick(writer: &mut ClientWriter, state: &mut ClientState) -> io::Result<()> {
|
||||||
println!();
|
if state.output_mode == OutputMode::Verbose {
|
||||||
println!(
|
println!();
|
||||||
"[auto-poll] timer fired (interval={}s)",
|
println!(
|
||||||
state.effective_poll_secs()
|
"[auto-poll] timer fired (interval={}s)",
|
||||||
);
|
state.effective_poll_secs()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
match (state.session_id, state.serial) {
|
match (state.session_id, state.serial) {
|
||||||
(Some(session_id), Some(serial)) => {
|
(Some(session_id), Some(serial)) => {
|
||||||
println!(
|
if state.output_mode == OutputMode::Verbose {
|
||||||
"[auto-poll] send Serial Query with session_id={}, serial={}",
|
println!(
|
||||||
session_id, serial
|
"[auto-poll] send Serial Query with session_id={}, serial={}",
|
||||||
);
|
session_id, serial
|
||||||
|
);
|
||||||
|
}
|
||||||
send_serial_query(writer, state.version, session_id, serial).await?;
|
send_serial_query(writer, state.version, session_id, serial).await?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("[auto-poll] local state incomplete, send Reset Query");
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!("[auto-poll] local state incomplete, send Reset Query");
|
||||||
|
}
|
||||||
send_reset_query(writer, state.version).await?;
|
send_reset_query(writer, state.version).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!();
|
if state.output_mode == OutputMode::Verbose {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -869,11 +938,30 @@ impl Config {
|
|||||||
|
|
||||||
while let Some(arg) = args.next() {
|
while let Some(arg) = args.next() {
|
||||||
match arg.as_str() {
|
match arg.as_str() {
|
||||||
"--tls" => {
|
"--tls" => match transport {
|
||||||
if matches!(transport, TransportConfig::Tcp) {
|
TransportConfig::Tcp => {
|
||||||
transport = TransportConfig::Tls(TlsConfig::default());
|
transport = TransportConfig::Tls(TlsConfig::default());
|
||||||
}
|
}
|
||||||
}
|
TransportConfig::Tls(_) => {}
|
||||||
|
TransportConfig::Ssh(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--tls cannot be used together with --ssh",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"--ssh" => match transport {
|
||||||
|
TransportConfig::Tcp => {
|
||||||
|
transport = TransportConfig::Ssh(SshConfig::default());
|
||||||
|
}
|
||||||
|
TransportConfig::Ssh(_) => {}
|
||||||
|
TransportConfig::Tls(_) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh cannot be used together with --tls",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
},
|
||||||
"--ca-cert" => {
|
"--ca-cert" => {
|
||||||
let path = args.next().ok_or_else(|| {
|
let path = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--ca-cert requires a path")
|
io::Error::new(io::ErrorKind::InvalidInput, "--ca-cert requires a path")
|
||||||
@ -901,6 +989,45 @@ impl Config {
|
|||||||
})?;
|
})?;
|
||||||
ensure_tls_config(&mut transport)?.server_name = Some(name);
|
ensure_tls_config(&mut transport)?.server_name = Some(name);
|
||||||
}
|
}
|
||||||
|
"--ssh-user" => {
|
||||||
|
let user = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--ssh-user requires a value")
|
||||||
|
})?;
|
||||||
|
ensure_ssh_config(&mut transport)?.user = Some(user);
|
||||||
|
}
|
||||||
|
"--ssh-key" => {
|
||||||
|
let path = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(io::ErrorKind::InvalidInput, "--ssh-key requires a path")
|
||||||
|
})?;
|
||||||
|
ensure_ssh_config(&mut transport)?.private_key = Some(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
"--ssh-subsystem" => {
|
||||||
|
let subsystem = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh-subsystem requires a value",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
ensure_ssh_config(&mut transport)?.subsystem = Some(subsystem);
|
||||||
|
}
|
||||||
|
"--ssh-known-hosts" => {
|
||||||
|
let path = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh-known-hosts requires a path",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
ensure_ssh_config(&mut transport)?.known_hosts = Some(PathBuf::from(path));
|
||||||
|
}
|
||||||
|
"--ssh-server-key" => {
|
||||||
|
let path = args.next().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh-server-key requires a path",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
ensure_ssh_config(&mut transport)?.server_key = Some(PathBuf::from(path));
|
||||||
|
}
|
||||||
"--timeout" => {
|
"--timeout" => {
|
||||||
let secs = args.next().ok_or_else(|| {
|
let secs = args.next().ok_or_else(|| {
|
||||||
io::Error::new(io::ErrorKind::InvalidInput, "--timeout requires seconds")
|
io::Error::new(io::ErrorKind::InvalidInput, "--timeout requires seconds")
|
||||||
@ -1034,6 +1161,7 @@ fn should_print_pdu(output_mode: OutputMode, header: &PduHeader) -> bool {
|
|||||||
enum TransportConfig {
|
enum TransportConfig {
|
||||||
Tcp,
|
Tcp,
|
||||||
Tls(TlsConfig),
|
Tls(TlsConfig),
|
||||||
|
Ssh(SshConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TransportConfig {
|
impl TransportConfig {
|
||||||
@ -1052,6 +1180,17 @@ impl TransportConfig {
|
|||||||
.map(|path| path.display().to_string())
|
.map(|path| path.display().to_string())
|
||||||
.unwrap_or_else(|| "<none>".to_string())
|
.unwrap_or_else(|| "<none>".to_string())
|
||||||
),
|
),
|
||||||
|
Self::Ssh(cfg) => format!(
|
||||||
|
"ssh (user={}, subsystem={}, host_key_check={})",
|
||||||
|
cfg.user.as_deref().unwrap_or("<unset>"),
|
||||||
|
cfg.subsystem
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(DEFAULT_SSH_SUBSYSTEM_NAME),
|
||||||
|
cfg.host_key_verification
|
||||||
|
.as_ref()
|
||||||
|
.map(HostKeyVerification::describe)
|
||||||
|
.unwrap_or("<unset>")
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1064,14 +1203,62 @@ struct TlsConfig {
|
|||||||
client_key: Option<PathBuf>,
|
client_key: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_tls_config(transport: &mut TransportConfig) -> io::Result<&mut TlsConfig> {
|
#[derive(Debug, Clone)]
|
||||||
if matches!(transport, TransportConfig::Tcp) {
|
enum HostKeyVerification {
|
||||||
*transport = TransportConfig::Tls(TlsConfig::default());
|
KnownHosts(PathBuf),
|
||||||
}
|
PinnedServerKey(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostKeyVerification {
|
||||||
|
fn describe(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::KnownHosts(_) => "known_hosts",
|
||||||
|
Self::PinnedServerKey(_) => "pinned_server_key",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct SshConfig {
|
||||||
|
user: Option<String>,
|
||||||
|
private_key: Option<PathBuf>,
|
||||||
|
subsystem: Option<String>,
|
||||||
|
known_hosts: Option<PathBuf>,
|
||||||
|
server_key: Option<PathBuf>,
|
||||||
|
host_key_verification: Option<HostKeyVerification>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_tls_config(transport: &mut TransportConfig) -> io::Result<&mut TlsConfig> {
|
||||||
match transport {
|
match transport {
|
||||||
|
TransportConfig::Tcp => {
|
||||||
|
*transport = TransportConfig::Tls(TlsConfig::default());
|
||||||
|
match transport {
|
||||||
|
TransportConfig::Tls(cfg) => Ok(cfg),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
TransportConfig::Tls(cfg) => Ok(cfg),
|
TransportConfig::Tls(cfg) => Ok(cfg),
|
||||||
TransportConfig::Tcp => unreachable!(),
|
TransportConfig::Ssh(_) => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"TLS options cannot be used together with --ssh",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_ssh_config(transport: &mut TransportConfig) -> io::Result<&mut SshConfig> {
|
||||||
|
match transport {
|
||||||
|
TransportConfig::Tcp => {
|
||||||
|
*transport = TransportConfig::Ssh(SshConfig::default());
|
||||||
|
match transport {
|
||||||
|
TransportConfig::Ssh(cfg) => Ok(cfg),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransportConfig::Ssh(cfg) => Ok(cfg),
|
||||||
|
TransportConfig::Tls(_) => Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SSH options cannot be used together with --tls",
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1114,6 +1301,68 @@ fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<Tran
|
|||||||
client_key: cfg.client_key,
|
client_key: cfg.client_key,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
TransportConfig::Ssh(mut cfg) => {
|
||||||
|
let user = cfg.user.take().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SSH mode requires --ssh-user <name>",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if user.trim().is_empty() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh-user must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let private_key = cfg.private_key.take().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SSH mode requires --ssh-key <path>",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if cfg.known_hosts.is_some() && cfg.server_key.is_some() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SSH host key verification must choose one: --ssh-known-hosts or --ssh-server-key",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let host_key_verification = if let Some(path) = cfg.known_hosts.take() {
|
||||||
|
HostKeyVerification::KnownHosts(path)
|
||||||
|
} else if let Some(path) = cfg.server_key.take() {
|
||||||
|
HostKeyVerification::PinnedServerKey(path)
|
||||||
|
} else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"SSH mode requires host key verification: --ssh-known-hosts <path> or --ssh-server-key <path>",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let subsystem = cfg
|
||||||
|
.subsystem
|
||||||
|
.take()
|
||||||
|
.unwrap_or_else(|| DEFAULT_SSH_SUBSYSTEM_NAME.to_string());
|
||||||
|
|
||||||
|
if subsystem.trim().is_empty() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"--ssh-subsystem must not be empty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = parse_host_port(addr)?;
|
||||||
|
|
||||||
|
Ok(TransportConfig::Ssh(SshConfig {
|
||||||
|
user: Some(user),
|
||||||
|
private_key: Some(private_key),
|
||||||
|
subsystem: Some(subsystem),
|
||||||
|
known_hosts: None,
|
||||||
|
server_key: None,
|
||||||
|
host_key_verification: Some(host_key_verification),
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1121,9 +1370,165 @@ async fn connect_stream(config: &Config) -> io::Result<DynStream> {
|
|||||||
match &config.transport {
|
match &config.transport {
|
||||||
TransportConfig::Tcp => Ok(Box::new(TcpStream::connect(&config.addr).await?)),
|
TransportConfig::Tcp => Ok(Box::new(TcpStream::connect(&config.addr).await?)),
|
||||||
TransportConfig::Tls(tls) => connect_tls_stream(&config.addr, tls).await,
|
TransportConfig::Tls(tls) => connect_tls_stream(&config.addr, tls).await,
|
||||||
|
TransportConfig::Ssh(ssh) => connect_ssh_stream(&config.addr, ssh).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct SshClientHandler {
|
||||||
|
host: String,
|
||||||
|
port: u16,
|
||||||
|
host_key_verification: HostKeyVerification,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl client::Handler for SshClientHandler {
|
||||||
|
type Error = russh::Error;
|
||||||
|
|
||||||
|
async fn check_server_key(
|
||||||
|
&mut self,
|
||||||
|
server_public_key: &russh::keys::ssh_key::PublicKey,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
match &self.host_key_verification {
|
||||||
|
HostKeyVerification::KnownHosts(path) => {
|
||||||
|
check_known_hosts_path(&self.host, self.port, server_public_key, path)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
HostKeyVerification::PinnedServerKey(path) => {
|
||||||
|
let expected_key = load_public_key(path)?;
|
||||||
|
Ok(expected_key == *server_public_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SshSessionStream {
|
||||||
|
channel_stream: ChannelStream<SshClientMsg>,
|
||||||
|
_session: client::Handle<SshClientHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncRead for SshSessionStream {
|
||||||
|
fn poll_read(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &mut ReadBuf<'_>,
|
||||||
|
) -> Poll<io::Result<()>> {
|
||||||
|
Pin::new(&mut self.channel_stream).poll_read(cx, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncWrite for SshSessionStream {
|
||||||
|
fn poll_write(
|
||||||
|
mut self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
buf: &[u8],
|
||||||
|
) -> Poll<io::Result<usize>> {
|
||||||
|
Pin::new(&mut self.channel_stream).poll_write(cx, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
Pin::new(&mut self.channel_stream).poll_flush(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||||
|
Pin::new(&mut self.channel_stream).poll_shutdown(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_ssh_stream(addr: &str, ssh: &SshConfig) -> io::Result<DynStream> {
|
||||||
|
let user = ssh
|
||||||
|
.user
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH user"))?;
|
||||||
|
let private_key_path = ssh
|
||||||
|
.private_key
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH private key"))?;
|
||||||
|
let subsystem = ssh
|
||||||
|
.subsystem
|
||||||
|
.as_deref()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing SSH subsystem"))?;
|
||||||
|
let host_key_verification = ssh.host_key_verification.clone().ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"missing SSH host key verification",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (host, port) = parse_host_port(addr)?;
|
||||||
|
let handler = SshClientHandler {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
host_key_verification,
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_config = Arc::new(client::Config::default());
|
||||||
|
let mut session = client::connect(session_config, addr, handler)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionAborted,
|
||||||
|
format!("SSH handshake failed: {}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let private_key = load_secret_key(private_key_path, None).map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!(
|
||||||
|
"failed to load SSH private key {}: {}",
|
||||||
|
private_key_path.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let rsa_hash = session.best_supported_rsa_hash().await.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionAborted,
|
||||||
|
format!("failed to negotiate SSH RSA hash: {}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let auth_result = session
|
||||||
|
.authenticate_publickey(
|
||||||
|
user.to_string(),
|
||||||
|
PrivateKeyWithHashAlg::new(Arc::new(private_key), rsa_hash.flatten()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
format!("SSH publickey authentication failed: {}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !auth_result.success() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::PermissionDenied,
|
||||||
|
"SSH publickey authentication rejected by server",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let channel = session.channel_open_session().await.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionAborted,
|
||||||
|
format!("failed to open SSH session channel: {}", err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
channel
|
||||||
|
.request_subsystem(true, subsystem)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::ConnectionAborted,
|
||||||
|
format!("failed to request SSH subsystem '{}': {}", subsystem, err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let channel_stream = channel.into_stream();
|
||||||
|
|
||||||
|
Ok(Box::new(SshSessionStream {
|
||||||
|
channel_stream,
|
||||||
|
_session: session,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream> {
|
async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream> {
|
||||||
let stream = TcpStream::connect(addr).await?;
|
let stream = TcpStream::connect(addr).await?;
|
||||||
let connector = build_tls_connector(tls)?;
|
let connector = build_tls_connector(tls)?;
|
||||||
@ -1219,6 +1624,44 @@ fn default_server_name_for_addr(addr: &str) -> Option<String> {
|
|||||||
addr.rsplit_once(':').map(|(host, _port)| host.to_string())
|
addr.rsplit_once(':').map(|(host, _port)| host.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_host_port(addr: &str) -> io::Result<(String, u16)> {
|
||||||
|
if let Some(rest) = addr.strip_prefix('[') {
|
||||||
|
let (host, port_part) = rest.split_once("]:").ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid address '{}', expected [host]:port", addr),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let port = port_part.parse::<u16>().map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid port in address '{}': {}", addr, err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
return Ok((host.to_string(), port));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (host, port_part) = addr.rsplit_once(':').ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid address '{}', expected host:port", addr),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if host.is_empty() {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid address '{}', host must not be empty", addr),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let port = port_part.parse::<u16>().map_err(|err| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("invalid port in address '{}': {}", addr, err),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok((host.to_string(), port))
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_u64_arg(value: &str, name: &str) -> io::Result<u64> {
|
fn parse_u64_arg(value: &str, name: &str) -> io::Result<u64> {
|
||||||
let parsed = value.parse::<u64>().map_err(|err| {
|
let parsed = value.parse::<u64>().map_err(|err| {
|
||||||
io::Error::new(
|
io::Error::new(
|
||||||
|
|||||||
121
src/main.rs
121
src/main.rs
@ -4,6 +4,7 @@ use std::sync::{Arc, RwLock};
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
use chrono::{FixedOffset, Utc};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
@ -16,8 +17,10 @@ use rpki::source::pipeline::{PayloadLoadConfig, load_payloads_from_latest_source
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct AppConfig {
|
struct AppConfig {
|
||||||
enable_tls: bool,
|
enable_tls: bool,
|
||||||
|
enable_ssh: bool,
|
||||||
tcp_addr: SocketAddr,
|
tcp_addr: SocketAddr,
|
||||||
tls_addr: SocketAddr,
|
tls_addr: SocketAddr,
|
||||||
|
ssh_addr: SocketAddr,
|
||||||
|
|
||||||
db_path: String,
|
db_path: String,
|
||||||
ccr_dir: String,
|
ccr_dir: String,
|
||||||
@ -25,6 +28,11 @@ struct AppConfig {
|
|||||||
tls_cert_path: String,
|
tls_cert_path: String,
|
||||||
tls_key_path: String,
|
tls_key_path: String,
|
||||||
tls_client_ca_path: String,
|
tls_client_ca_path: String,
|
||||||
|
ssh_host_key_path: String,
|
||||||
|
ssh_authorized_keys_path: String,
|
||||||
|
ssh_username: String,
|
||||||
|
ssh_subsystem_name: String,
|
||||||
|
ssh_password: Option<String>,
|
||||||
|
|
||||||
max_delta: u8,
|
max_delta: u8,
|
||||||
prune_delta_by_snapshot_size: bool,
|
prune_delta_by_snapshot_size: bool,
|
||||||
@ -39,8 +47,10 @@ impl Default for AppConfig {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
enable_tls: false,
|
enable_tls: false,
|
||||||
|
enable_ssh: false,
|
||||||
tcp_addr: "0.0.0.0:323".parse().expect("invalid default tcp_addr"),
|
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"),
|
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(),
|
db_path: "./rtr-db".to_string(),
|
||||||
ccr_dir: "./data".to_string(),
|
ccr_dir: "./data".to_string(),
|
||||||
@ -48,6 +58,11 @@ impl Default for AppConfig {
|
|||||||
tls_cert_path: "./certs/server.crt".to_string(),
|
tls_cert_path: "./certs/server.crt".to_string(),
|
||||||
tls_key_path: "./certs/server.key".to_string(),
|
tls_key_path: "./certs/server.key".to_string(),
|
||||||
tls_client_ca_path: "./certs/client-ca.crt".to_string(),
|
tls_client_ca_path: "./certs/client-ca.crt".to_string(),
|
||||||
|
ssh_host_key_path: "./certs/ssh_host_ed25519_key".to_string(),
|
||||||
|
ssh_authorized_keys_path: "./certs/rtr-authorized_keys".to_string(),
|
||||||
|
ssh_username: "rpki-rtr".to_string(),
|
||||||
|
ssh_subsystem_name: "rpki-rtr".to_string(),
|
||||||
|
ssh_password: None,
|
||||||
|
|
||||||
max_delta: 100,
|
max_delta: 100,
|
||||||
prune_delta_by_snapshot_size: false,
|
prune_delta_by_snapshot_size: false,
|
||||||
@ -74,6 +89,9 @@ impl AppConfig {
|
|||||||
if let Some(value) = env_var("RPKI_RTR_ENABLE_TLS")? {
|
if let Some(value) = env_var("RPKI_RTR_ENABLE_TLS")? {
|
||||||
config.enable_tls = parse_bool(&value, "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")? {
|
if let Some(value) = env_var("RPKI_RTR_TCP_ADDR")? {
|
||||||
config.tcp_addr = value
|
config.tcp_addr = value
|
||||||
.parse()
|
.parse()
|
||||||
@ -84,6 +102,17 @@ impl AppConfig {
|
|||||||
.parse()
|
.parse()
|
||||||
.map_err(|err| anyhow!("invalid RPKI_RTR_TLS_ADDR '{}': {}", value, err))?;
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
// data
|
// data
|
||||||
if let Some(value) = env_var("RPKI_RTR_DB_PATH")? {
|
if let Some(value) = env_var("RPKI_RTR_DB_PATH")? {
|
||||||
@ -109,6 +138,22 @@ impl AppConfig {
|
|||||||
if let Some(value) = env_var("RPKI_RTR_TLS_CLIENT_CA_PATH")? {
|
if let Some(value) = env_var("RPKI_RTR_TLS_CLIENT_CA_PATH")? {
|
||||||
config.tls_client_ca_path = value;
|
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_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")? {
|
if let Some(value) = env_var("RPKI_RTR_MAX_DELTA")? {
|
||||||
let parsed: u8 = value
|
let parsed: u8 = value
|
||||||
.parse()
|
.parse()
|
||||||
@ -135,20 +180,14 @@ impl AppConfig {
|
|||||||
source_refresh_interval_legacy.as_deref(),
|
source_refresh_interval_legacy.as_deref(),
|
||||||
) {
|
) {
|
||||||
(Some(new_value), Some(_)) => {
|
(Some(new_value), Some(_)) => {
|
||||||
let secs = parse_positive_u64(
|
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
|
||||||
new_value,
|
|
||||||
"RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS",
|
|
||||||
)?;
|
|
||||||
config.source_refresh_interval = Duration::from_secs(secs);
|
config.source_refresh_interval = Duration::from_secs(secs);
|
||||||
warn!(
|
warn!(
|
||||||
"both RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS and legacy RPKI_RTR_REFRESH_INTERVAL_SECS are set; using RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS"
|
"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) => {
|
(Some(new_value), None) => {
|
||||||
let secs = parse_positive_u64(
|
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
|
||||||
new_value,
|
|
||||||
"RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS",
|
|
||||||
)?;
|
|
||||||
config.source_refresh_interval = Duration::from_secs(secs);
|
config.source_refresh_interval = Duration::from_secs(secs);
|
||||||
}
|
}
|
||||||
(None, Some(legacy_value)) => {
|
(None, Some(legacy_value)) => {
|
||||||
@ -271,7 +310,22 @@ fn init_shared_cache(config: &AppConfig, store: &RtrStore) -> Result<SharedRtrCa
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService {
|
fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService {
|
||||||
if config.enable_tls {
|
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_password.as_deref(),
|
||||||
|
)
|
||||||
|
} else if config.enable_tls {
|
||||||
info!("starting TCP and TLS RTR servers");
|
info!("starting TCP and TLS RTR servers");
|
||||||
service.spawn_tcp_and_tls_from_pem(
|
service.spawn_tcp_and_tls_from_pem(
|
||||||
config.tcp_addr,
|
config.tcp_addr,
|
||||||
@ -280,6 +334,17 @@ fn start_servers(config: &AppConfig, service: &RtrService) -> RunningRtrService
|
|||||||
&config.tls_key_path,
|
&config.tls_key_path,
|
||||||
&config.tls_client_ca_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_password.as_deref(),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
info!("starting TCP RTR server");
|
info!("starting TCP RTR server");
|
||||||
service.spawn_tcp_only(config.tcp_addr)
|
service.spawn_tcp_only(config.tcp_addr)
|
||||||
@ -373,6 +438,7 @@ fn log_startup_config(config: &AppConfig) {
|
|||||||
info!("db_path={}", config.db_path);
|
info!("db_path={}", config.db_path);
|
||||||
info!("tcp_addr={}", config.tcp_addr);
|
info!("tcp_addr={}", config.tcp_addr);
|
||||||
info!("tls_enabled={}", config.enable_tls);
|
info!("tls_enabled={}", config.enable_tls);
|
||||||
|
info!("ssh_enabled={}", config.enable_ssh);
|
||||||
|
|
||||||
if config.enable_tls {
|
if config.enable_tls {
|
||||||
info!("tls_addr={}", config.tls_addr);
|
info!("tls_addr={}", config.tls_addr);
|
||||||
@ -380,6 +446,17 @@ fn log_startup_config(config: &AppConfig) {
|
|||||||
info!("tls_key_path={}", config.tls_key_path);
|
info!("tls_key_path={}", config.tls_key_path);
|
||||||
info!("tls_client_ca_path={}", config.tls_client_ca_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_password_enabled={}", config.ssh_password.is_some());
|
||||||
|
}
|
||||||
|
|
||||||
info!("ccr_dir={}", config.ccr_dir);
|
info!("ccr_dir={}", config.ccr_dir);
|
||||||
info!(
|
info!(
|
||||||
@ -419,11 +496,33 @@ fn log_startup_config(config: &AppConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init_tracing() {
|
fn init_tracing() {
|
||||||
let _ = tracing_subscriber::fmt()
|
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn"));
|
||||||
|
|
||||||
|
struct ShanghaiTimer;
|
||||||
|
|
||||||
|
impl tracing_subscriber::fmt::time::FormatTime for ShanghaiTimer {
|
||||||
|
fn format_time(
|
||||||
|
&self,
|
||||||
|
w: &mut tracing_subscriber::fmt::format::Writer<'_>,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
let shanghai_offset = FixedOffset::east_opt(8 * 60 * 60)
|
||||||
|
.expect("fixed +08:00 offset should always be valid");
|
||||||
|
let now = Utc::now().with_timezone(&shanghai_offset);
|
||||||
|
write!(w, "{}", now.format("%Y-%m-%d %H:%M:%S%.3f %:z"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = tracing_subscriber::fmt()
|
||||||
|
.with_timer(ShanghaiTimer)
|
||||||
|
.with_env_filter(filter)
|
||||||
.with_target(true)
|
.with_target(true)
|
||||||
.with_thread_ids(true)
|
.with_thread_ids(true)
|
||||||
.with_level(true)
|
.with_level(true)
|
||||||
.try_init();
|
.try_init()
|
||||||
|
{
|
||||||
|
eprintln!("failed to initialize tracing subscriber: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn env_var(name: &str) -> Result<Option<String>> {
|
fn env_var(name: &str) -> Result<Option<String>> {
|
||||||
|
|||||||
9
src/rtr/cache/store.rs
vendored
9
src/rtr/cache/store.rs
vendored
@ -48,7 +48,8 @@ impl RtrCache {
|
|||||||
project_snapshot_for_version(&source_snapshot, version as u8)
|
project_snapshot_for_version(&source_snapshot, version as u8)
|
||||||
});
|
});
|
||||||
let serials = [serial; VERSION_COUNT];
|
let serials = [serial; VERSION_COUNT];
|
||||||
let deltas = std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
let deltas =
|
||||||
|
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
||||||
|
|
||||||
tokio::spawn({
|
tokio::spawn({
|
||||||
let store = store.clone();
|
let store = store.clone();
|
||||||
@ -103,7 +104,8 @@ fn try_restore_from_store(
|
|||||||
let mut snapshots = std::array::from_fn(|_| Snapshot::empty());
|
let mut snapshots = std::array::from_fn(|_| Snapshot::empty());
|
||||||
let mut session_ids = [0u16; VERSION_COUNT];
|
let mut session_ids = [0u16; VERSION_COUNT];
|
||||||
let mut serials = [0u32; VERSION_COUNT];
|
let mut serials = [0u32; VERSION_COUNT];
|
||||||
let mut deltas = std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
let mut deltas =
|
||||||
|
std::array::from_fn(|_| VecDeque::<Arc<Delta>>::with_capacity(max_delta as usize));
|
||||||
|
|
||||||
for version in 0u8..=2 {
|
for version in 0u8..=2 {
|
||||||
let idx = version as usize;
|
let idx = version as usize;
|
||||||
@ -123,7 +125,8 @@ fn try_restore_from_store(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some((min_serial, max_serial)) = store.get_delta_window_for_version(version)? {
|
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 mut loaded =
|
||||||
|
store.load_delta_window_for_version(version, min_serial, max_serial)?;
|
||||||
let max_keep = usize::from(max_delta.max(1));
|
let max_keep = usize::from(max_delta.max(1));
|
||||||
if loaded.len() > max_keep {
|
if loaded.len() > max_keep {
|
||||||
let drop_count = loaded.len() - max_keep;
|
let drop_count = loaded.len() - max_keep;
|
||||||
|
|||||||
@ -1,23 +1,169 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::pin::Pin;
|
||||||
use std::sync::{Arc, atomic::AtomicUsize};
|
use std::sync::{Arc, atomic::AtomicUsize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
|
use russh::server::{self, Msg, Session};
|
||||||
|
use russh::{Channel, ChannelId, Disconnect};
|
||||||
use socket2::{SockRef, TcpKeepalive};
|
use socket2::{SockRef, TcpKeepalive};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::{Semaphore, broadcast, watch};
|
use tokio::sync::{Semaphore, broadcast, watch};
|
||||||
use tracing::{info, warn};
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
use tokio_rustls::TlsAcceptor;
|
|
||||||
|
|
||||||
use crate::rtr::cache::SharedRtrCache;
|
use crate::rtr::cache::SharedRtrCache;
|
||||||
use crate::rtr::server::config::RtrServiceConfig;
|
use crate::rtr::server::config::RtrServiceConfig;
|
||||||
use crate::rtr::server::connection::{
|
use crate::rtr::server::connection::{
|
||||||
ConnectionGuard, handle_tcp_connection, handle_tls_connection, is_expected_disconnect,
|
ConnectionGuard, handle_tcp_connection, handle_tls_connection, is_expected_disconnect,
|
||||||
};
|
};
|
||||||
|
use crate::rtr::server::ssh::RtrSshRuntimeConfig;
|
||||||
use crate::rtr::server::tls::load_rustls_server_config_with_options;
|
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 handle_connection(
|
||||||
|
&self,
|
||||||
|
cache: SharedRtrCache,
|
||||||
|
stream: TcpStream,
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
notify_tx: broadcast::Sender<()>,
|
||||||
|
shutdown_tx: watch::Sender<bool>,
|
||||||
|
) -> TransportFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct TcpTransport;
|
||||||
|
|
||||||
|
impl TransportAcceptor for TcpTransport {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TCP"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_connection(
|
||||||
|
&self,
|
||||||
|
cache: SharedRtrCache,
|
||||||
|
stream: TcpStream,
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
notify_tx: broadcast::Sender<()>,
|
||||||
|
shutdown_tx: watch::Sender<bool>,
|
||||||
|
) -> 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportAcceptor for TlsTransport {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TLS"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_connection(
|
||||||
|
&self,
|
||||||
|
cache: SharedRtrCache,
|
||||||
|
stream: TcpStream,
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
notify_tx: broadcast::Sender<()>,
|
||||||
|
shutdown_tx: watch::Sender<bool>,
|
||||||
|
) -> TransportFuture {
|
||||||
|
let acceptor = self.acceptor.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
handle_tls_connection(
|
||||||
|
cache,
|
||||||
|
stream,
|
||||||
|
peer_addr,
|
||||||
|
acceptor,
|
||||||
|
notify_tx.subscribe(),
|
||||||
|
shutdown_tx.subscribe(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SshTransport {
|
||||||
|
runtime: Arc<RtrSshRuntimeConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransportAcceptor for SshTransport {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SSH"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_connection(
|
||||||
|
&self,
|
||||||
|
cache: SharedRtrCache,
|
||||||
|
stream: TcpStream,
|
||||||
|
peer_addr: SocketAddr,
|
||||||
|
notify_tx: broadcast::Sender<()>,
|
||||||
|
shutdown_tx: watch::Sender<bool>,
|
||||||
|
) -> 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() => {
|
||||||
|
let _ = handle
|
||||||
|
.disconnect(
|
||||||
|
Disconnect::ByApplication,
|
||||||
|
"service shutdown".to_string(),
|
||||||
|
"".to_string(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Ok(()) | Err(_) => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct RtrServer {
|
pub struct RtrServer {
|
||||||
bind_addr: SocketAddr,
|
bind_addr: SocketAddr,
|
||||||
@ -64,104 +210,7 @@ impl RtrServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_tcp(self) -> Result<()> {
|
pub async fn run_tcp(self) -> Result<()> {
|
||||||
let listener = TcpListener::bind(self.bind_addr)
|
self.run_with_transport(TcpTransport).await
|
||||||
.await
|
|
||||||
.with_context(|| format!("failed to bind TCP RTR server on {}", self.bind_addr))?;
|
|
||||||
|
|
||||||
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
|
||||||
|
|
||||||
info!("RTR TCP server listening on {}", self.bind_addr);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
changed = shutdown_rx.changed() => {
|
|
||||||
match changed {
|
|
||||||
Ok(()) => {
|
|
||||||
if *shutdown_rx.borrow() {
|
|
||||||
info!("RTR TCP listener {} shutting down", self.bind_addr);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
info!("RTR TCP listener {} shutdown channel closed", self.bind_addr);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accept_res = listener.accept() => {
|
|
||||||
let (stream, peer_addr) = match accept_res {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("RTR TCP accept failed: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
|
|
||||||
warn!("failed to configure TCP keepalive for {}: {}", peer_addr, err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let permit = match self.connection_limiter.clone().try_acquire_owned() {
|
|
||||||
Ok(permit) => permit,
|
|
||||||
Err(_) => {
|
|
||||||
warn!(
|
|
||||||
"RTR TCP connection rejected for {}: max connections reached ({})",
|
|
||||||
peer_addr,
|
|
||||||
self.config.max_connections
|
|
||||||
);
|
|
||||||
drop(stream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let cache = self.cache.clone();
|
|
||||||
let notify_rx = self.notify_tx.subscribe();
|
|
||||||
let shutdown_rx = self.shutdown_tx.subscribe();
|
|
||||||
let active_connections = self.active_connections.clone();
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"RTR TCP client connected: peer_addr={}, active_connections(before_spawn)={}",
|
|
||||||
peer_addr,
|
|
||||||
self.active_connections()
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let guard = ConnectionGuard::new(active_connections, permit);
|
|
||||||
info!(
|
|
||||||
"RTR TCP connection established: peer_addr={}, active_connections={}",
|
|
||||||
peer_addr,
|
|
||||||
guard.active_count()
|
|
||||||
);
|
|
||||||
if let Err(err) =
|
|
||||||
handle_tcp_connection(cache, stream, peer_addr, notify_rx, shutdown_rx).await
|
|
||||||
{
|
|
||||||
if is_expected_disconnect(&err) {
|
|
||||||
info!(
|
|
||||||
"RTR TCP session closed by peer: peer_addr={}, active_connections={}, err={}",
|
|
||||||
peer_addr,
|
|
||||||
guard.active_count(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
warn!(
|
|
||||||
"RTR TCP session closed with error: peer_addr={}, active_connections={}, err={}",
|
|
||||||
peer_addr,
|
|
||||||
guard.active_count(),
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"RTR TCP session closed cleanly: peer_addr={}, active_connections={}",
|
|
||||||
peer_addr,
|
|
||||||
guard.active_count()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_tls_from_pem(
|
pub async fn run_tls_from_pem(
|
||||||
@ -180,14 +229,37 @@ impl RtrServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_tls(self, tls_config: Arc<ServerConfig>) -> Result<()> {
|
pub async fn run_tls(self, tls_config: Arc<ServerConfig>) -> Result<()> {
|
||||||
let listener = TcpListener::bind(self.bind_addr)
|
let transport = TlsTransport {
|
||||||
.await
|
acceptor: TlsAcceptor::from(tls_config),
|
||||||
.with_context(|| format!("failed to bind TLS RTR server on {}", self.bind_addr))?;
|
};
|
||||||
|
self.run_with_transport(transport).await
|
||||||
|
}
|
||||||
|
|
||||||
let acceptor = TlsAcceptor::from(tls_config);
|
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();
|
let mut shutdown_rx = self.shutdown_tx.subscribe();
|
||||||
|
|
||||||
info!("RTR TLS server listening on {}", self.bind_addr);
|
info!(
|
||||||
|
"RTR {} server listening on {}",
|
||||||
|
transport.name(),
|
||||||
|
self.bind_addr
|
||||||
|
);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@ -195,12 +267,20 @@ impl RtrServer {
|
|||||||
match changed {
|
match changed {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if *shutdown_rx.borrow() {
|
if *shutdown_rx.borrow() {
|
||||||
info!("RTR TLS listener {} shutting down", self.bind_addr);
|
info!(
|
||||||
|
"RTR {} listener {} shutting down",
|
||||||
|
transport.name(),
|
||||||
|
self.bind_addr
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
info!("RTR TLS listener {} shutdown channel closed", self.bind_addr);
|
info!(
|
||||||
|
"RTR {} listener {} shutdown channel closed",
|
||||||
|
transport.name(),
|
||||||
|
self.bind_addr
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,20 +290,26 @@ impl RtrServer {
|
|||||||
let (stream, peer_addr) = match accept_res {
|
let (stream, peer_addr) = match accept_res {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("RTR TLS accept failed: {}", err);
|
warn!("RTR {} accept failed: {}", transport.name(), err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
|
if let Err(err) = apply_keepalive(&stream, self.config.tcp_keepalive) {
|
||||||
warn!("failed to configure TCP keepalive for {}: {}", peer_addr, err);
|
warn!(
|
||||||
|
"failed to configure TCP keepalive for {} peer {}: {}",
|
||||||
|
transport.name(),
|
||||||
|
peer_addr,
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let permit = match self.connection_limiter.clone().try_acquire_owned() {
|
let permit = match self.connection_limiter.clone().try_acquire_owned() {
|
||||||
Ok(permit) => permit,
|
Ok(permit) => permit,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
warn!(
|
warn!(
|
||||||
"RTR TLS connection rejected for {}: max connections reached ({})",
|
"RTR {} connection rejected for {}: max connections reached ({})",
|
||||||
|
transport.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
self.config.max_connections
|
self.config.max_connections
|
||||||
);
|
);
|
||||||
@ -233,13 +319,14 @@ impl RtrServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let cache = self.cache.clone();
|
let cache = self.cache.clone();
|
||||||
let acceptor = acceptor.clone();
|
let notify_tx = self.notify_tx.clone();
|
||||||
let notify_rx = self.notify_tx.subscribe();
|
let shutdown_tx = self.shutdown_tx.clone();
|
||||||
let shutdown_rx = self.shutdown_tx.subscribe();
|
|
||||||
let active_connections = self.active_connections.clone();
|
let active_connections = self.active_connections.clone();
|
||||||
|
let transport_instance = transport.clone();
|
||||||
|
|
||||||
info!(
|
debug!(
|
||||||
"RTR TLS client connected: peer_addr={}, active_connections(before_spawn)={}",
|
"RTR {} client connected: peer_addr={}, active_connections(before_spawn)={}",
|
||||||
|
transport_instance.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
self.active_connections()
|
self.active_connections()
|
||||||
);
|
);
|
||||||
@ -247,38 +334,41 @@ impl RtrServer {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let guard = ConnectionGuard::new(active_connections, permit);
|
let guard = ConnectionGuard::new(active_connections, permit);
|
||||||
info!(
|
info!(
|
||||||
"RTR TLS connection established: peer_addr={}, active_connections={}",
|
"RTR {} connection established: peer_addr={}, active_connections={}",
|
||||||
|
transport_instance.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
guard.active_count()
|
guard.active_count()
|
||||||
);
|
);
|
||||||
if let Err(err) = handle_tls_connection(
|
|
||||||
cache,
|
if let Err(err) = transport_instance
|
||||||
stream,
|
.handle_connection(cache, stream, peer_addr, notify_tx, shutdown_tx)
|
||||||
peer_addr,
|
.await
|
||||||
acceptor,
|
{
|
||||||
notify_rx,
|
let active_after_close = guard.active_count().saturating_sub(1);
|
||||||
shutdown_rx,
|
|
||||||
).await {
|
|
||||||
if is_expected_disconnect(&err) {
|
if is_expected_disconnect(&err) {
|
||||||
info!(
|
info!(
|
||||||
"RTR TLS session closed by peer: peer_addr={}, active_connections={}, err={}",
|
"RTR {} session closed by peer: peer_addr={}, active_connections={}, err={}",
|
||||||
|
transport_instance.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
guard.active_count(),
|
active_after_close,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
warn!(
|
warn!(
|
||||||
"RTR TLS session closed with error: peer_addr={}, active_connections={}, err={}",
|
"RTR {} session closed with error: peer_addr={}, active_connections={}, err={}",
|
||||||
|
transport_instance.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
guard.active_count(),
|
active_after_close,
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let active_after_close = guard.active_count().saturating_sub(1);
|
||||||
info!(
|
info!(
|
||||||
"RTR TLS session closed cleanly: peer_addr={}, active_connections={}",
|
"RTR {} session closed cleanly: peer_addr={}, active_connections={}",
|
||||||
|
transport_instance.name(),
|
||||||
peer_addr,
|
peer_addr,
|
||||||
guard.active_count()
|
active_after_close
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -288,6 +378,215 @@ impl RtrServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct RtrSshHandler {
|
||||||
|
cache: SharedRtrCache,
|
||||||
|
notify_rx: broadcast::Receiver<()>,
|
||||||
|
shutdown_rx: watch::Receiver<bool>,
|
||||||
|
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<bool>,
|
||||||
|
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<()> {
|
fn apply_keepalive(stream: &tokio::net::TcpStream, keepalive: Option<Duration>) -> Result<()> {
|
||||||
let Some(keepalive) = keepalive else {
|
let Some(keepalive) = keepalive else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@ -3,6 +3,7 @@ pub mod connection;
|
|||||||
pub mod listener;
|
pub mod listener;
|
||||||
pub mod notifier;
|
pub mod notifier;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod ssh;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
||||||
pub use config::RtrServiceConfig;
|
pub use config::RtrServiceConfig;
|
||||||
|
|||||||
@ -13,6 +13,7 @@ use crate::rtr::cache::SharedRtrCache;
|
|||||||
use crate::rtr::server::config::RtrServiceConfig;
|
use crate::rtr::server::config::RtrServiceConfig;
|
||||||
use crate::rtr::server::listener::RtrServer;
|
use crate::rtr::server::listener::RtrServer;
|
||||||
use crate::rtr::server::notifier::RtrNotifier;
|
use crate::rtr::server::notifier::RtrNotifier;
|
||||||
|
use crate::rtr::server::ssh::load_rtr_ssh_runtime_config;
|
||||||
|
|
||||||
pub struct RtrService {
|
pub struct RtrService {
|
||||||
cache: SharedRtrCache,
|
cache: SharedRtrCache,
|
||||||
@ -86,6 +87,18 @@ impl RtrService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.active_connections.clone(),
|
||||||
|
self.config.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn_tcp(&self, bind_addr: SocketAddr) -> JoinHandle<()> {
|
pub fn spawn_tcp(&self, bind_addr: SocketAddr) -> JoinHandle<()> {
|
||||||
if self.config.warn_insecure_tcp {
|
if self.config.warn_insecure_tcp {
|
||||||
warn!(
|
warn!(
|
||||||
@ -141,6 +154,111 @@ impl RtrService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
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 {
|
pub fn spawn_tcp_only(&self, tcp_bind_addr: SocketAddr) -> RunningRtrService {
|
||||||
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
|
||||||
|
|
||||||
|
|||||||
97
src/rtr/server/ssh.rs
Normal file
97
src/rtr/server/ssh.rs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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)]
|
||||||
|
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,
|
||||||
|
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 = load_authorized_keys(authorized_keys_path.as_ref())?;
|
||||||
|
let password = password.map(str::trim).filter(|value| !value.is_empty());
|
||||||
|
|
||||||
|
let mut methods = MethodSet::empty();
|
||||||
|
methods.push(MethodKind::PublicKey);
|
||||||
|
if password.is_some() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@ -102,19 +102,43 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header_res = timeout(transport_timeout, Header::read_raw(&mut self.stream)) => {
|
header_res = async {
|
||||||
|
// draft-ietf-sidrops-8210bis-25 Section 6 allows routers to wait up to
|
||||||
|
// Refresh Interval before polling again (recommended default: 3600s).
|
||||||
|
// In an established session, a long quiet period is therefore expected and
|
||||||
|
// must not be treated as a transport stall.
|
||||||
|
//
|
||||||
|
// We only enforce transport timeout before session establishment. Once the
|
||||||
|
// session is established, we wait indefinitely for the next query header.
|
||||||
|
if self.state == SessionState::Established {
|
||||||
|
Header::read_raw(&mut self.stream).await
|
||||||
|
} else {
|
||||||
|
timeout(transport_timeout, Header::read_raw(&mut self.stream))
|
||||||
|
.await
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "transport read timed out"))?
|
||||||
|
}
|
||||||
|
} => {
|
||||||
let raw_header = match header_res {
|
let raw_header = match header_res {
|
||||||
Ok(Ok(raw)) => raw,
|
Ok(raw) => raw,
|
||||||
Ok(Err(_)) => {
|
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
|
||||||
info!("RTR session closed by peer before header read completed: {}", self.session_summary());
|
info!("RTR session closed by peer before header read completed: {}", self.session_summary());
|
||||||
self.state = SessionState::Closed;
|
self.state = SessionState::Closed;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(err) if err.kind() == io::ErrorKind::TimedOut => {
|
||||||
warn!("RTR session transport timeout while waiting for header: {}", self.session_summary());
|
warn!("RTR session transport timeout while waiting for header: {}", self.session_summary());
|
||||||
self.handle_transport_timeout(&[]).await?;
|
self.handle_transport_timeout(&[]).await?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"RTR session failed to read header: err={}, {}",
|
||||||
|
err,
|
||||||
|
self.session_summary()
|
||||||
|
);
|
||||||
|
self.state = SessionState::Closed;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let header = match Header::from_raw(raw_header) {
|
let header = match Header::from_raw(raw_header) {
|
||||||
Ok(h) => h,
|
Ok(h) => h,
|
||||||
@ -546,7 +570,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_reset_query(&mut self, offending_pdu: &[u8]) -> Result<()> {
|
async fn handle_reset_query(&mut self, offending_pdu: &[u8]) -> Result<()> {
|
||||||
info!(
|
debug!(
|
||||||
"RTR session received Reset Query: negotiated_version={:?}, offending_pdu_len={}",
|
"RTR session received Reset Query: negotiated_version={:?}, offending_pdu_len={}",
|
||||||
self.version,
|
self.version,
|
||||||
offending_pdu.len()
|
offending_pdu.len()
|
||||||
@ -568,7 +592,7 @@ where
|
|||||||
if !data_available {
|
if !data_available {
|
||||||
self.send_no_data_available(offending_pdu, "cache data is not currently available")
|
self.send_no_data_available(offending_pdu, "cache data is not currently available")
|
||||||
.await?;
|
.await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session replied No Data Available to Reset Query: {}",
|
"RTR session replied No Data Available to Reset Query: {}",
|
||||||
self.session_summary()
|
self.session_summary()
|
||||||
);
|
);
|
||||||
@ -578,7 +602,7 @@ where
|
|||||||
self.write_cache_response(session_id).await?;
|
self.write_cache_response(session_id).await?;
|
||||||
self.send_payloads(&payloads, true).await?;
|
self.send_payloads(&payloads, true).await?;
|
||||||
self.write_end_of_data(session_id, serial).await?;
|
self.write_end_of_data(session_id, serial).await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session completed Reset Query: response_session_id={}, response_serial={}, payload_count={}, {}",
|
"RTR session completed Reset Query: response_session_id={}, response_serial={}, payload_count={}, {}",
|
||||||
session_id,
|
session_id,
|
||||||
serial,
|
serial,
|
||||||
@ -596,7 +620,7 @@ where
|
|||||||
client_serial: u32,
|
client_serial: u32,
|
||||||
offending_pdu: &[u8],
|
offending_pdu: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!(
|
debug!(
|
||||||
"RTR session received Serial Query: negotiated_version={}, client_session_id={}, client_serial={}, offending_pdu_len={}",
|
"RTR session received Serial Query: negotiated_version={}, client_session_id={}, client_serial={}, offending_pdu_len={}",
|
||||||
version,
|
version,
|
||||||
client_session,
|
client_session,
|
||||||
@ -617,7 +641,7 @@ where
|
|||||||
if !data_available {
|
if !data_available {
|
||||||
self.send_no_data_available(offending_pdu, "cache data is not currently available")
|
self.send_no_data_available(offending_pdu, "cache data is not currently available")
|
||||||
.await?;
|
.await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session replied No Data Available to Serial Query: client_session_id={}, client_serial={}, {}",
|
"RTR session replied No Data Available to Serial Query: client_session_id={}, client_serial={}, {}",
|
||||||
client_session,
|
client_session,
|
||||||
client_serial,
|
client_serial,
|
||||||
@ -650,7 +674,7 @@ where
|
|||||||
match serial_result {
|
match serial_result {
|
||||||
SerialResult::ResetRequired => {
|
SerialResult::ResetRequired => {
|
||||||
self.write_cache_reset().await?;
|
self.write_cache_reset().await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session replied Cache Reset to Serial Query: client_session_id={}, client_serial={}, {}",
|
"RTR session replied Cache Reset to Serial Query: client_session_id={}, client_serial={}, {}",
|
||||||
client_session,
|
client_session,
|
||||||
client_serial,
|
client_serial,
|
||||||
@ -668,13 +692,13 @@ where
|
|||||||
(
|
(
|
||||||
cache.session_id_for_version(version),
|
cache.session_id_for_version(version),
|
||||||
cache.serial_for_version(version),
|
cache.serial_for_version(version),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
self.write_cache_response(current_session).await?;
|
self.write_cache_response(current_session).await?;
|
||||||
self.write_end_of_data(current_session, current_serial)
|
self.write_end_of_data(current_session, current_serial)
|
||||||
.await?;
|
.await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session replied CacheResponse+EndOfData (up-to-date) to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
"RTR session replied CacheResponse+EndOfData (up-to-date) to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
||||||
client_session,
|
client_session,
|
||||||
client_serial,
|
client_serial,
|
||||||
@ -701,7 +725,7 @@ where
|
|||||||
self.send_delta(&delta).await?;
|
self.send_delta(&delta).await?;
|
||||||
self.write_end_of_data(current_session, current_serial)
|
self.write_end_of_data(current_session, current_serial)
|
||||||
.await?;
|
.await?;
|
||||||
info!(
|
debug!(
|
||||||
"RTR session replied delta to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
"RTR session replied delta to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
||||||
client_session,
|
client_session,
|
||||||
client_serial,
|
client_serial,
|
||||||
@ -782,7 +806,10 @@ where
|
|||||||
.cache
|
.cache
|
||||||
.read()
|
.read()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|cache| self.version.map(|version| cache.serial_for_version(version)))
|
.and_then(|cache| {
|
||||||
|
self.version
|
||||||
|
.map(|version| cache.serial_for_version(version))
|
||||||
|
})
|
||||||
.map(|serial| serial.to_string())
|
.map(|serial| serial.to_string())
|
||||||
.unwrap_or_else(|| "<unavailable>".to_string());
|
.unwrap_or_else(|| "<unavailable>".to_string());
|
||||||
let session_id = self
|
let session_id = self
|
||||||
|
|||||||
@ -102,8 +102,13 @@ fn read_slurm_files(slurm_dir: &str) -> Result<Vec<(String, SlurmFile)>> {
|
|||||||
for entry in std::fs::read_dir(slurm_dir)
|
for entry in std::fs::read_dir(slurm_dir)
|
||||||
.map_err(|err| anyhow!("failed to read SLURM directory '{}': {}", slurm_dir, err))?
|
.map_err(|err| anyhow!("failed to read SLURM directory '{}': {}", slurm_dir, err))?
|
||||||
{
|
{
|
||||||
let entry = entry
|
let entry = entry.map_err(|err| {
|
||||||
.map_err(|err| anyhow!("failed to enumerate SLURM directory '{}': {}", slurm_dir, err))?;
|
anyhow!(
|
||||||
|
"failed to enumerate SLURM directory '{}': {}",
|
||||||
|
slurm_dir,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("slurm") {
|
if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("slurm") {
|
||||||
paths.push(path);
|
paths.push(path);
|
||||||
|
|||||||
80
tests/test_rtr_debug_client_ssh_cli.rs
Normal file
80
tests/test_rtr_debug_client_ssh_cli.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn run_client(args: &[&str]) -> std::process::Output {
|
||||||
|
Command::new(env!("CARGO_BIN_EXE_rtr_debug_client"))
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.expect("failed to run rtr_debug_client")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_requires_user() {
|
||||||
|
let output = run_client(&[
|
||||||
|
"--ssh",
|
||||||
|
"--ssh-key",
|
||||||
|
"tests/fixtures/ssh/client.key",
|
||||||
|
"--ssh-server-key",
|
||||||
|
"tests/fixtures/ssh/server.pub",
|
||||||
|
]);
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("SSH mode requires --ssh-user <name>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_requires_key() {
|
||||||
|
let output = run_client(&[
|
||||||
|
"--ssh",
|
||||||
|
"--ssh-user",
|
||||||
|
"rpki-rtr",
|
||||||
|
"--ssh-server-key",
|
||||||
|
"tests/fixtures/ssh/server.pub",
|
||||||
|
]);
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("SSH mode requires --ssh-key <path>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_requires_host_key_verification() {
|
||||||
|
let output = run_client(&[
|
||||||
|
"--ssh",
|
||||||
|
"--ssh-user",
|
||||||
|
"rpki-rtr",
|
||||||
|
"--ssh-key",
|
||||||
|
"tests/fixtures/ssh/client.key",
|
||||||
|
]);
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("SSH mode requires host key verification")
|
||||||
|
&& stderr.contains("--ssh-known-hosts")
|
||||||
|
&& stderr.contains("--ssh-server-key")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_rejects_multiple_host_key_verification_sources() {
|
||||||
|
let output = run_client(&[
|
||||||
|
"--ssh",
|
||||||
|
"--ssh-user",
|
||||||
|
"rpki-rtr",
|
||||||
|
"--ssh-key",
|
||||||
|
"tests/fixtures/ssh/client.key",
|
||||||
|
"--ssh-known-hosts",
|
||||||
|
"tests/fixtures/ssh/known_hosts",
|
||||||
|
"--ssh-server-key",
|
||||||
|
"tests/fixtures/ssh/server.pub",
|
||||||
|
]);
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("must choose one"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ssh_conflicts_with_tls() {
|
||||||
|
let output = run_client(&["--ssh", "--tls"]);
|
||||||
|
assert!(!output.status.success());
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("--tls cannot be used together with --ssh"));
|
||||||
|
}
|
||||||
344
tests/test_server_transports.rs
Normal file
344
tests/test_server_transports.rs
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use rustls::{ClientConfig, RootCertStore};
|
||||||
|
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time::{Instant, sleep};
|
||||||
|
use tokio_rustls::TlsConnector;
|
||||||
|
|
||||||
|
use rpki::rtr::cache::{RtrCacheBuilder, SessionIds, SharedRtrCache};
|
||||||
|
use rpki::rtr::payload::Timing;
|
||||||
|
use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, ResetQuery};
|
||||||
|
use rpki::rtr::server::RtrService;
|
||||||
|
use russh::client;
|
||||||
|
use russh::keys;
|
||||||
|
use russh::keys::ssh_key::LineEnding;
|
||||||
|
|
||||||
|
fn fixture_path(name: &str) -> PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests")
|
||||||
|
.join("fixtures")
|
||||||
|
.join("tls")
|
||||||
|
.join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_pem_certs(path: &Path) -> Vec<CertificateDer<'static>> {
|
||||||
|
let file = fs::File::open(path).expect("open cert file");
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
rustls_pemfile::certs(&mut reader)
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.expect("parse certs")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_pem_key(path: &Path) -> PrivateKeyDer<'static> {
|
||||||
|
let file = fs::File::open(path).expect("open key file");
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
rustls_pemfile::private_key(&mut reader)
|
||||||
|
.expect("read private key")
|
||||||
|
.expect("missing private key")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_cache() -> SharedRtrCache {
|
||||||
|
Arc::new(RwLock::new(
|
||||||
|
RtrCacheBuilder::new()
|
||||||
|
.session_ids(SessionIds::from_array([42, 42, 42]))
|
||||||
|
.serials([100, 100, 100])
|
||||||
|
.timing(Timing::new(600, 600, 7200))
|
||||||
|
.build(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reserve_local_addr() -> SocketAddr {
|
||||||
|
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind temp listener");
|
||||||
|
listener.local_addr().expect("local addr")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn wait_for_port(addr: SocketAddr) {
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
|
loop {
|
||||||
|
if TcpStream::connect(addr).await.is_ok() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"port {} did not open in time",
|
||||||
|
addr
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(20)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_tls_client(addr: SocketAddr) -> tokio_rustls::client::TlsStream<TcpStream> {
|
||||||
|
let mut roots = RootCertStore::empty();
|
||||||
|
for cert in load_pem_certs(&fixture_path("client-ca.crt")) {
|
||||||
|
roots.add(cert).expect("add root cert");
|
||||||
|
}
|
||||||
|
|
||||||
|
let certs = load_pem_certs(&fixture_path("client-good.crt"));
|
||||||
|
let key = load_pem_key(&fixture_path("client-good.key"));
|
||||||
|
let cfg = ClientConfig::builder()
|
||||||
|
.with_root_certificates(roots)
|
||||||
|
.with_client_auth_cert(certs, key)
|
||||||
|
.expect("build tls client auth");
|
||||||
|
let connector = TlsConnector::from(Arc::new(cfg));
|
||||||
|
|
||||||
|
let tcp = TcpStream::connect(addr).await.expect("connect tls tcp");
|
||||||
|
connector
|
||||||
|
.connect(ServerName::IpAddress(addr.ip().into()), tcp)
|
||||||
|
.await
|
||||||
|
.expect("tls connect")
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestSshClientHandler;
|
||||||
|
|
||||||
|
impl client::Handler for TestSshClientHandler {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
async fn check_server_key(
|
||||||
|
&mut self,
|
||||||
|
_server_public_key: &russh::keys::ssh_key::PublicKey,
|
||||||
|
) -> Result<bool, Self::Error> {
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unified_server_tcp_handles_reset_query() {
|
||||||
|
let service = RtrService::new(test_cache());
|
||||||
|
let tcp_addr = reserve_local_addr();
|
||||||
|
|
||||||
|
let running = service.spawn_tcp_only(tcp_addr);
|
||||||
|
wait_for_port(tcp_addr).await;
|
||||||
|
|
||||||
|
let mut client = TcpStream::connect(tcp_addr).await.expect("connect tcp");
|
||||||
|
ResetQuery::new(1)
|
||||||
|
.write(&mut client)
|
||||||
|
.await
|
||||||
|
.expect("send reset");
|
||||||
|
|
||||||
|
let response = CacheResponse::read(&mut client)
|
||||||
|
.await
|
||||||
|
.expect("read cache response");
|
||||||
|
assert_eq!(response.version(), 1);
|
||||||
|
assert_eq!(response.session_id(), 42);
|
||||||
|
|
||||||
|
let eod = EndOfDataV1::read(&mut client).await.expect("read eod");
|
||||||
|
assert_eq!(eod.version(), 1);
|
||||||
|
assert_eq!(eod.session_id(), 42);
|
||||||
|
assert_eq!(eod.serial_number(), 100);
|
||||||
|
|
||||||
|
running.shutdown();
|
||||||
|
running.wait().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unified_server_tls_handles_reset_query() {
|
||||||
|
let service = RtrService::new(test_cache());
|
||||||
|
let tcp_addr = reserve_local_addr();
|
||||||
|
let tls_addr = reserve_local_addr();
|
||||||
|
|
||||||
|
let running = service.spawn_tcp_and_tls_from_pem(
|
||||||
|
tcp_addr,
|
||||||
|
tls_addr,
|
||||||
|
fixture_path("server.crt"),
|
||||||
|
fixture_path("server.key"),
|
||||||
|
fixture_path("client-ca.crt"),
|
||||||
|
);
|
||||||
|
wait_for_port(tls_addr).await;
|
||||||
|
|
||||||
|
let mut client = connect_tls_client(tls_addr).await;
|
||||||
|
ResetQuery::new(1)
|
||||||
|
.write(&mut client)
|
||||||
|
.await
|
||||||
|
.expect("send reset tls");
|
||||||
|
|
||||||
|
let response = CacheResponse::read(&mut client)
|
||||||
|
.await
|
||||||
|
.expect("read tls cache response");
|
||||||
|
assert_eq!(response.version(), 1);
|
||||||
|
assert_eq!(response.session_id(), 42);
|
||||||
|
|
||||||
|
let eod = EndOfDataV1::read(&mut client).await.expect("read tls eod");
|
||||||
|
assert_eq!(eod.version(), 1);
|
||||||
|
assert_eq!(eod.session_id(), 42);
|
||||||
|
assert_eq!(eod.serial_number(), 100);
|
||||||
|
|
||||||
|
running.shutdown();
|
||||||
|
running.wait().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unified_server_ssh_opens_listener_and_emits_banner() {
|
||||||
|
let service = RtrService::new(test_cache());
|
||||||
|
let tcp_addr = reserve_local_addr();
|
||||||
|
let ssh_addr = reserve_local_addr();
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||||
|
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||||
|
|
||||||
|
let host_key =
|
||||||
|
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||||
|
let host_key_pem = host_key
|
||||||
|
.to_openssh(LineEnding::LF)
|
||||||
|
.expect("encode host key");
|
||||||
|
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||||
|
|
||||||
|
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||||
|
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||||
|
|
||||||
|
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||||
|
tcp_addr,
|
||||||
|
ssh_addr,
|
||||||
|
&host_key_path,
|
||||||
|
&authorized_keys_path,
|
||||||
|
"rpki-rtr",
|
||||||
|
"rpki-rtr",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
wait_for_port(ssh_addr).await;
|
||||||
|
|
||||||
|
let stream = TcpStream::connect(ssh_addr).await.expect("connect ssh");
|
||||||
|
let mut reader = tokio::io::BufReader::new(stream);
|
||||||
|
let mut banner = String::new();
|
||||||
|
reader
|
||||||
|
.read_line(&mut banner)
|
||||||
|
.await
|
||||||
|
.expect("read ssh banner");
|
||||||
|
assert!(
|
||||||
|
banner.starts_with("SSH-2.0-"),
|
||||||
|
"unexpected ssh banner: {}",
|
||||||
|
banner
|
||||||
|
);
|
||||||
|
|
||||||
|
running.shutdown();
|
||||||
|
running.wait().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unified_server_ssh_accepts_password_when_configured() {
|
||||||
|
let service = RtrService::new(test_cache());
|
||||||
|
let tcp_addr = reserve_local_addr();
|
||||||
|
let ssh_addr = reserve_local_addr();
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||||
|
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||||
|
|
||||||
|
let host_key =
|
||||||
|
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||||
|
let host_key_pem = host_key
|
||||||
|
.to_openssh(LineEnding::LF)
|
||||||
|
.expect("encode host key");
|
||||||
|
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||||
|
|
||||||
|
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||||
|
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||||
|
|
||||||
|
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||||
|
tcp_addr,
|
||||||
|
ssh_addr,
|
||||||
|
&host_key_path,
|
||||||
|
&authorized_keys_path,
|
||||||
|
"rpki-rtr",
|
||||||
|
"rpki-rtr",
|
||||||
|
Some("test-password"),
|
||||||
|
);
|
||||||
|
wait_for_port(ssh_addr).await;
|
||||||
|
|
||||||
|
let mut session = client::connect(
|
||||||
|
Arc::new(client::Config::default()),
|
||||||
|
ssh_addr,
|
||||||
|
TestSshClientHandler,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("connect ssh client");
|
||||||
|
let auth_result = session
|
||||||
|
.authenticate_password("rpki-rtr", "test-password")
|
||||||
|
.await
|
||||||
|
.expect("password auth result");
|
||||||
|
assert!(auth_result.success(), "password auth should succeed");
|
||||||
|
|
||||||
|
let channel = session
|
||||||
|
.channel_open_session()
|
||||||
|
.await
|
||||||
|
.expect("open session channel");
|
||||||
|
channel
|
||||||
|
.request_subsystem(true, "rpki-rtr")
|
||||||
|
.await
|
||||||
|
.expect("request subsystem");
|
||||||
|
let mut stream = channel.into_stream();
|
||||||
|
|
||||||
|
ResetQuery::new(1)
|
||||||
|
.write(&mut stream)
|
||||||
|
.await
|
||||||
|
.expect("send reset over ssh subsystem");
|
||||||
|
let response = CacheResponse::read(&mut stream)
|
||||||
|
.await
|
||||||
|
.expect("read cache response over ssh subsystem");
|
||||||
|
assert_eq!(response.version(), 1);
|
||||||
|
assert_eq!(response.session_id(), 42);
|
||||||
|
let eod = EndOfDataV1::read(&mut stream)
|
||||||
|
.await
|
||||||
|
.expect("read eod over ssh subsystem");
|
||||||
|
assert_eq!(eod.version(), 1);
|
||||||
|
assert_eq!(eod.session_id(), 42);
|
||||||
|
assert_eq!(eod.serial_number(), 100);
|
||||||
|
|
||||||
|
running.shutdown();
|
||||||
|
running.wait().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn unified_server_ssh_rejects_password_when_not_configured() {
|
||||||
|
let service = RtrService::new(test_cache());
|
||||||
|
let tcp_addr = reserve_local_addr();
|
||||||
|
let ssh_addr = reserve_local_addr();
|
||||||
|
|
||||||
|
let tmp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let host_key_path = tmp.path().join("ssh_host_ed25519_key");
|
||||||
|
let authorized_keys_path = tmp.path().join("authorized_keys");
|
||||||
|
|
||||||
|
let host_key =
|
||||||
|
keys::PrivateKey::random(&mut rand::rng(), keys::Algorithm::Ed25519).expect("gen host key");
|
||||||
|
let host_key_pem = host_key
|
||||||
|
.to_openssh(LineEnding::LF)
|
||||||
|
.expect("encode host key");
|
||||||
|
fs::write(&host_key_path, host_key_pem).expect("write host key");
|
||||||
|
|
||||||
|
let pubkey_line = host_key.public_key().to_openssh().expect("encode pubkey");
|
||||||
|
fs::write(&authorized_keys_path, format!("{pubkey_line}\n")).expect("write authorized_keys");
|
||||||
|
|
||||||
|
let running = service.spawn_tcp_and_ssh_from_openssh(
|
||||||
|
tcp_addr,
|
||||||
|
ssh_addr,
|
||||||
|
&host_key_path,
|
||||||
|
&authorized_keys_path,
|
||||||
|
"rpki-rtr",
|
||||||
|
"rpki-rtr",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
wait_for_port(ssh_addr).await;
|
||||||
|
|
||||||
|
let mut session = client::connect(
|
||||||
|
Arc::new(client::Config::default()),
|
||||||
|
ssh_addr,
|
||||||
|
TestSshClientHandler,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("connect ssh client");
|
||||||
|
let auth_result = session
|
||||||
|
.authenticate_password("rpki-rtr", "test-password")
|
||||||
|
.await
|
||||||
|
.expect("password auth result");
|
||||||
|
assert!(!auth_result.success(), "password auth should be rejected");
|
||||||
|
|
||||||
|
running.shutdown();
|
||||||
|
running.wait().await;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user