补充rtr-client,完善tls连接等
This commit is contained in:
@@ -1,63 +1,235 @@
|
||||
# rtr_debug_client
|
||||
|
||||
`rtr_debug_client` 是一个用于调试和联调 RTR(RPKI-to-Router)服务端的小型客户端工具。
|
||||
`rtr_debug_client` 是一个轻量级的 RTR 调试客户端,用于手工联调和协议行为观察。
|
||||
|
||||
它的目标不是做一个完整的生产级 router client,而是提供一个简单、直接、可观察的调试入口,用于:
|
||||
它适合以下场景:
|
||||
- 在开发阶段验证 RTR server 的行为
|
||||
- 发送 `Reset Query` 和 `Serial Query`
|
||||
- 观察服务端返回的各类 PDU
|
||||
- 检查会话状态、`session_id`、`serial` 的变化
|
||||
- 排查 `ErrorReport`、`CacheReset`、`SerialNotify`、`RouterKey`、`ASPA`
|
||||
- 联调纯 TCP 和 TLS 两种 RTR 传输方式
|
||||
|
||||
- 连接 RTR server
|
||||
- 发送 `Reset Query` 或 `Serial Query`
|
||||
- 接收并打印服务端返回的 PDU
|
||||
- 辅助排查协议实现、会话状态、序列号增量、PDU 编码等问题
|
||||
它不是生产级 router client,而是一个便于调试和观察协议细节的小工具。
|
||||
|
||||
---
|
||||
|
||||
## 适用场景
|
||||
|
||||
这个工具适合以下场景:
|
||||
|
||||
- 开发 RTR server 时做本地联调
|
||||
- 验证服务端是否正确返回 `Cache Response`
|
||||
- 检查 `IPv4 Prefix` / `IPv6 Prefix` / `ASPA` / `End of Data` 等 PDU
|
||||
- 验证 `Serial Query` 路径是否正确
|
||||
- 观察异常响应,例如 `Cache Reset` 或 `Error Report`
|
||||
- 后续扩展为支持 TLS、自动断言、会话统计等调试能力
|
||||
|
||||
---
|
||||
|
||||
## 当前能力
|
||||
## 当前支持的能力
|
||||
|
||||
当前版本支持:
|
||||
|
||||
- TCP 连接 RTR server
|
||||
- 纯 TCP 连接
|
||||
- TLS 连接
|
||||
- TLS 服务端证书校验
|
||||
- 可选的 TLS 客户端证书认证
|
||||
- 发送 `Reset Query`
|
||||
- 发送 `Serial Query`
|
||||
- 持续读取服务端返回的 PDU
|
||||
- 解析并打印以下常见 PDU:
|
||||
- `Serial Notify`
|
||||
- `Serial Query`
|
||||
- `Reset Query`
|
||||
- `Cache Response`
|
||||
- `IPv4 Prefix`
|
||||
- `IPv6 Prefix`
|
||||
- `End of Data`
|
||||
- `Cache Reset`
|
||||
- `Error Report`
|
||||
- `ASPA`
|
||||
- 基础长度校验
|
||||
- 最大 PDU 长度限制,防止异常数据导致过大内存分配
|
||||
- 保持长连接持续接收服务端 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` 保持错误后的自动轮询
|
||||
|
||||
---
|
||||
## 构建
|
||||
|
||||
## 目录结构
|
||||
```sh
|
||||
cargo build --bin rtr_debug_client
|
||||
```
|
||||
|
||||
建议目录如下:
|
||||
## 基本用法
|
||||
|
||||
```text
|
||||
src/
|
||||
└── bin/
|
||||
└── rtr_debug_client/
|
||||
├── main.rs
|
||||
├── protocol.rs
|
||||
├── io.rs
|
||||
├── pretty.rs
|
||||
└── README.md
|
||||
基本形式:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id> <serial>] [options]
|
||||
```
|
||||
|
||||
默认值:
|
||||
- `addr`: `127.0.0.1:3323`
|
||||
- `version`: `1`
|
||||
- `mode`: `reset`
|
||||
- `timeout`: `30`
|
||||
- `poll`: `60`
|
||||
|
||||
## TCP 示例
|
||||
|
||||
发送 `Reset Query`:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:3323 1 reset
|
||||
```
|
||||
|
||||
发送 `Serial Query`:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:3323 1 serial 42 100
|
||||
```
|
||||
|
||||
持续观察错误路径:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- 127.0.0.1:3323 1 reset --keep-after-error
|
||||
```
|
||||
|
||||
## TLS 示例
|
||||
|
||||
只做服务端证书校验:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:3324 1 reset \
|
||||
--tls \
|
||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
||||
--server-name localhost
|
||||
```
|
||||
|
||||
双向 TLS 认证:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:3324 1 reset \
|
||||
--tls \
|
||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
||||
--server-name localhost \
|
||||
--client-cert tests/fixtures/tls/client-good.crt \
|
||||
--client-key tests/fixtures/tls/client-good.key
|
||||
```
|
||||
|
||||
双向 TLS + 错误后继续自动轮询:
|
||||
|
||||
```sh
|
||||
cargo run --bin rtr_debug_client -- \
|
||||
127.0.0.1:3324 1 reset \
|
||||
--tls \
|
||||
--ca-cert tests/fixtures/tls/client-ca.crt \
|
||||
--server-name localhost \
|
||||
--client-cert tests/fixtures/tls/client-good.crt \
|
||||
--client-key tests/fixtures/tls/client-good.key \
|
||||
--keep-after-error
|
||||
```
|
||||
|
||||
说明:
|
||||
- 开启 `--tls` 时必须提供 `--ca-cert`
|
||||
- 如果目标地址本身不适合直接作为 TLS 名称,显式提供 `--server-name`
|
||||
- 客户端认证必须同时提供 `--client-cert` 和 `--client-key`
|
||||
|
||||
## 命令行参数
|
||||
|
||||
- `--tls`
|
||||
使用 TLS 而不是纯 TCP。
|
||||
|
||||
- `--ca-cert <path>`
|
||||
用于校验服务端证书的 CA 证书文件,PEM 格式。
|
||||
|
||||
- `--server-name <name>`
|
||||
TLS 握手时用于校验证书的服务端名称。
|
||||
|
||||
- `--client-cert <path>`
|
||||
双向 TLS 时使用的客户端证书,PEM 格式。
|
||||
|
||||
- `--client-key <path>`
|
||||
与 `--client-cert` 配套的客户端私钥,PEM 格式。
|
||||
|
||||
- `--timeout <secs>`
|
||||
等待下一个 PDU 的读取超时时间,单位秒。
|
||||
|
||||
- `--poll <secs>`
|
||||
在尚未拿到 `EndOfData` timing hint 前,默认使用的自动轮询间隔。
|
||||
|
||||
- `--keep-after-error`
|
||||
收到 `ErrorReport` 后不暂停自动轮询。
|
||||
|
||||
## 运行中可用命令
|
||||
|
||||
程序启动后,可以在控制台输入以下命令:
|
||||
|
||||
- `help`
|
||||
显示帮助。
|
||||
|
||||
- `state`
|
||||
打印当前客户端状态。
|
||||
|
||||
- `reset`
|
||||
发送 `Reset Query`。
|
||||
|
||||
- `serial`
|
||||
使用当前 `session_id` 和 `serial` 发送 `Serial Query`。
|
||||
|
||||
- `serial <sid> <serial>`
|
||||
使用显式参数发送 `Serial Query`。
|
||||
|
||||
- `timeout`
|
||||
查看当前读取超时设置。
|
||||
|
||||
- `timeout <secs>`
|
||||
修改读取超时。
|
||||
|
||||
- `poll`
|
||||
查看当前自动轮询间隔、轮询来源以及暂停状态。
|
||||
|
||||
- `poll <secs>`
|
||||
手工覆盖当前轮询间隔。
|
||||
|
||||
- `poll pause`
|
||||
暂停自动轮询。
|
||||
|
||||
- `poll resume`
|
||||
恢复自动轮询。
|
||||
|
||||
- `keep-after-error`
|
||||
查看当前是否启用了错误后持续轮询。
|
||||
|
||||
- `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,就能快速知道是哪一个请求触发了错误。
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::env;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
|
||||
use tokio::io::{self as tokio_io, AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader, WriteHalf};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration, Instant};
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
mod wire;
|
||||
mod pretty;
|
||||
@@ -19,15 +23,23 @@ use crate::protocol::{PduHeader, PduType, QueryMode};
|
||||
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
|
||||
const DEFAULT_POLL_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||
|
||||
type DynStream = Box<dyn AsyncStream>;
|
||||
type ClientWriter = WriteHalf<DynStream>;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let config = Config::from_args()?;
|
||||
|
||||
println!("== RTR debug client ==");
|
||||
println!("target : {}", config.addr);
|
||||
println!("transport: {}", config.transport.describe());
|
||||
println!("version : {}", config.version);
|
||||
println!("timeout : {}s", config.read_timeout_secs);
|
||||
println!("poll : {}s (default before EndOfData refresh is known)", config.default_poll_secs);
|
||||
println!("keep-after-error: {}", config.keep_after_error);
|
||||
match &config.mode {
|
||||
QueryMode::Reset => {
|
||||
println!("mode : reset");
|
||||
@@ -41,14 +53,15 @@ async fn main() -> io::Result<()> {
|
||||
println!();
|
||||
print_help();
|
||||
|
||||
let stream = TcpStream::connect(&config.addr).await?;
|
||||
let stream = connect_stream(&config).await?;
|
||||
println!("connected to {}", config.addr);
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
let (mut reader, mut writer) = tokio_io::split(stream);
|
||||
let mut state = ClientState::new(
|
||||
config.version,
|
||||
config.read_timeout_secs,
|
||||
config.default_poll_secs,
|
||||
config.keep_after_error,
|
||||
);
|
||||
|
||||
match config.mode {
|
||||
@@ -71,7 +84,7 @@ async fn main() -> io::Result<()> {
|
||||
let mut stdin_lines = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
let poll_sleep = tokio::time::sleep_until(state.next_poll_deadline);
|
||||
let poll_sleep = tokio::time::sleep_until(state.poll_deadline());
|
||||
tokio::pin!(poll_sleep);
|
||||
|
||||
tokio::select! {
|
||||
@@ -131,7 +144,7 @@ async fn main() -> io::Result<()> {
|
||||
}
|
||||
|
||||
async fn handle_incoming_pdu(
|
||||
writer: &mut OwnedWriteHalf,
|
||||
writer: &mut ClientWriter,
|
||||
state: &mut ClientState,
|
||||
header: &PduHeader,
|
||||
body: &[u8],
|
||||
@@ -139,9 +152,10 @@ async fn handle_incoming_pdu(
|
||||
match header.pdu_type() {
|
||||
PduType::CacheResponse => {
|
||||
state.current_session_id = Some(header.session_id());
|
||||
state.last_error_code = None;
|
||||
}
|
||||
|
||||
PduType::Ipv4Prefix | PduType::Ipv6Prefix | PduType::Aspa => {
|
||||
PduType::Ipv4Prefix | PduType::Ipv6Prefix | PduType::RouterKey | PduType::Aspa => {
|
||||
if state.current_session_id.is_none() {
|
||||
state.current_session_id = Some(header.session_id());
|
||||
}
|
||||
@@ -161,6 +175,7 @@ async fn handle_incoming_pdu(
|
||||
state.refresh = eod.refresh;
|
||||
state.retry = eod.retry;
|
||||
state.expire = eod.expire;
|
||||
state.last_error_code = None;
|
||||
|
||||
println!(
|
||||
"updated client state: session_id={}, serial={}",
|
||||
@@ -198,6 +213,11 @@ async fn handle_incoming_pdu(
|
||||
let notify_serial = parse_serial_notify_serial(body);
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"[notify] received Serial Notify: session_id={}, notify_serial={:?}",
|
||||
notify_session_id,
|
||||
notify_serial
|
||||
);
|
||||
|
||||
match (state.session_id, state.serial, notify_serial) {
|
||||
(Some(current_session_id), Some(current_serial), Some(_new_serial))
|
||||
@@ -233,6 +253,7 @@ async fn handle_incoming_pdu(
|
||||
println!("received Cache Reset, send Reset Query");
|
||||
state.current_session_id = None;
|
||||
state.serial = None;
|
||||
state.last_error_code = None;
|
||||
send_reset_query(writer, state.version).await?;
|
||||
state.schedule_next_poll();
|
||||
println!();
|
||||
@@ -240,9 +261,20 @@ async fn handle_incoming_pdu(
|
||||
|
||||
PduType::ErrorReport => {
|
||||
println!();
|
||||
println!("received Error Report, keep connection open for debugging.");
|
||||
println!("received Error Report, pause auto polling for debugging.");
|
||||
state.last_error_code = Some(header.error_code());
|
||||
if let Some(retry) = state.retry {
|
||||
println!("will keep auto polling; server retry hint currently stored: {}s", retry);
|
||||
println!("server retry hint currently stored: {}s", retry);
|
||||
if state.should_prefer_retry_poll() {
|
||||
println!("when resumed, auto polling will use retry instead of refresh.");
|
||||
}
|
||||
}
|
||||
if state.keep_after_error {
|
||||
println!("keep-after-error is enabled, auto polling will continue.");
|
||||
state.schedule_next_poll();
|
||||
} else {
|
||||
println!("use `reset`, `serial`, or `poll resume` to continue manually.");
|
||||
state.pause_auto_poll();
|
||||
}
|
||||
println!();
|
||||
}
|
||||
@@ -256,7 +288,7 @@ async fn handle_incoming_pdu(
|
||||
}
|
||||
|
||||
async fn handle_poll_tick(
|
||||
writer: &mut OwnedWriteHalf,
|
||||
writer: &mut ClientWriter,
|
||||
state: &mut ClientState,
|
||||
) -> io::Result<()> {
|
||||
println!();
|
||||
@@ -285,7 +317,7 @@ async fn handle_poll_tick(
|
||||
|
||||
async fn handle_console_command(
|
||||
line: &str,
|
||||
writer: &mut OwnedWriteHalf,
|
||||
writer: &mut ClientWriter,
|
||||
state: &mut ClientState,
|
||||
) -> io::Result<bool> {
|
||||
let line = line.trim();
|
||||
@@ -382,8 +414,24 @@ async fn handle_console_command(
|
||||
"current effective poll interval: {}s",
|
||||
state.effective_poll_secs()
|
||||
);
|
||||
println!("poll interval source : {}", state.poll_interval_source());
|
||||
println!("stored refresh hint : {:?}", state.refresh);
|
||||
println!("default poll interval : {}s", state.default_poll_secs);
|
||||
println!("last_error_code : {:?}", state.last_error_code);
|
||||
println!("auto polling paused : {}", state.poll_paused);
|
||||
}
|
||||
|
||||
["poll", "pause"] => {
|
||||
state.pause_auto_poll();
|
||||
println!("auto polling paused");
|
||||
}
|
||||
|
||||
["poll", "resume"] => {
|
||||
state.resume_auto_poll();
|
||||
println!(
|
||||
"auto polling resumed, next poll scheduled after {}s",
|
||||
state.effective_poll_secs()
|
||||
);
|
||||
}
|
||||
|
||||
["poll", secs] => {
|
||||
@@ -428,6 +476,9 @@ fn print_help() {
|
||||
println!(" timeout <secs> update read timeout seconds");
|
||||
println!(" poll show current poll interval");
|
||||
println!(" poll <secs> override poll interval seconds");
|
||||
println!(" poll pause pause auto polling");
|
||||
println!(" poll resume resume auto polling");
|
||||
println!(" keep-after-error show current keep-after-error setting");
|
||||
println!(" quit exit client");
|
||||
println!();
|
||||
}
|
||||
@@ -444,6 +495,10 @@ fn print_state(state: &ClientState) {
|
||||
println!(" read_timeout_secs : {}", state.read_timeout_secs);
|
||||
println!(" default_poll_secs : {}", state.default_poll_secs);
|
||||
println!(" effective_poll_secs: {}", state.effective_poll_secs());
|
||||
println!(" poll_source : {}", state.poll_interval_source());
|
||||
println!(" last_error_code : {:?}", state.last_error_code);
|
||||
println!(" keep_after_error : {}", state.keep_after_error);
|
||||
println!(" poll_paused : {}", state.poll_paused);
|
||||
println!();
|
||||
}
|
||||
|
||||
@@ -457,14 +512,22 @@ struct ClientState {
|
||||
refresh: Option<u32>,
|
||||
retry: Option<u32>,
|
||||
expire: Option<u32>,
|
||||
last_error_code: Option<u16>,
|
||||
keep_after_error: bool,
|
||||
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
next_poll_deadline: Instant,
|
||||
poll_paused: bool,
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
fn new(version: u8, read_timeout_secs: u64, default_poll_secs: u64) -> Self {
|
||||
fn new(
|
||||
version: u8,
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
keep_after_error: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
session_id: None,
|
||||
@@ -473,20 +536,60 @@ impl ClientState {
|
||||
refresh: None,
|
||||
retry: None,
|
||||
expire: None,
|
||||
last_error_code: None,
|
||||
keep_after_error,
|
||||
read_timeout_secs,
|
||||
default_poll_secs,
|
||||
next_poll_deadline: Instant::now() + Duration::from_secs(default_poll_secs),
|
||||
poll_paused: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_poll_secs(&self) -> u64 {
|
||||
self.refresh.map(|v| v as u64).unwrap_or(self.default_poll_secs)
|
||||
if self.should_prefer_retry_poll() {
|
||||
self.retry
|
||||
.map(|v| v as u64)
|
||||
.unwrap_or_else(|| self.refresh.map(|v| v as u64).unwrap_or(self.default_poll_secs))
|
||||
} else {
|
||||
self.refresh.map(|v| v as u64).unwrap_or(self.default_poll_secs)
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_next_poll(&mut self) {
|
||||
self.next_poll_deadline =
|
||||
Instant::now() + Duration::from_secs(self.effective_poll_secs());
|
||||
}
|
||||
|
||||
fn pause_auto_poll(&mut self) {
|
||||
self.poll_paused = true;
|
||||
}
|
||||
|
||||
fn resume_auto_poll(&mut self) {
|
||||
self.poll_paused = false;
|
||||
self.schedule_next_poll();
|
||||
}
|
||||
|
||||
fn poll_deadline(&self) -> Instant {
|
||||
if self.poll_paused {
|
||||
Instant::now() + Duration::from_secs(365 * 24 * 60 * 60)
|
||||
} else {
|
||||
self.next_poll_deadline
|
||||
}
|
||||
}
|
||||
|
||||
fn should_prefer_retry_poll(&self) -> bool {
|
||||
matches!(self.last_error_code, Some(2 | 10))
|
||||
}
|
||||
|
||||
fn poll_interval_source(&self) -> &'static str {
|
||||
if self.should_prefer_retry_poll() && self.retry.is_some() {
|
||||
"retry"
|
||||
} else if self.refresh.is_some() {
|
||||
"refresh"
|
||||
} else {
|
||||
"default"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -496,17 +599,80 @@ struct Config {
|
||||
mode: QueryMode,
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
transport: TransportConfig,
|
||||
keep_after_error: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn from_args() -> io::Result<Self> {
|
||||
let mut args = env::args().skip(1);
|
||||
let mut positional = Vec::new();
|
||||
let mut transport = TransportConfig::Tcp;
|
||||
let mut read_timeout_secs = DEFAULT_READ_TIMEOUT_SECS;
|
||||
let mut default_poll_secs = DEFAULT_POLL_INTERVAL_SECS;
|
||||
let mut keep_after_error = false;
|
||||
|
||||
let addr = args
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--tls" => {
|
||||
transport = TransportConfig::Tls(TlsConfig::default());
|
||||
}
|
||||
"--ca-cert" => {
|
||||
let path = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--ca-cert requires a path")
|
||||
})?;
|
||||
ensure_tls_config(&mut transport)?.ca_cert = Some(PathBuf::from(path));
|
||||
}
|
||||
"--client-cert" => {
|
||||
let path = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--client-cert requires a path")
|
||||
})?;
|
||||
ensure_tls_config(&mut transport)?.client_cert = Some(PathBuf::from(path));
|
||||
}
|
||||
"--client-key" => {
|
||||
let path = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--client-key requires a path")
|
||||
})?;
|
||||
ensure_tls_config(&mut transport)?.client_key = Some(PathBuf::from(path));
|
||||
}
|
||||
"--server-name" => {
|
||||
let name = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--server-name requires a value")
|
||||
})?;
|
||||
ensure_tls_config(&mut transport)?.server_name = Some(name);
|
||||
}
|
||||
"--timeout" => {
|
||||
let secs = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--timeout requires seconds")
|
||||
})?;
|
||||
read_timeout_secs = parse_u64_arg(&secs, "--timeout")?;
|
||||
}
|
||||
"--poll" => {
|
||||
let secs = args.next().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, "--poll requires seconds")
|
||||
})?;
|
||||
default_poll_secs = parse_u64_arg(&secs, "--poll")?;
|
||||
}
|
||||
"--keep-after-error" => {
|
||||
keep_after_error = true;
|
||||
}
|
||||
_ if arg.starts_with("--") => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown option '{}'", arg),
|
||||
));
|
||||
}
|
||||
_ => positional.push(arg),
|
||||
}
|
||||
}
|
||||
|
||||
let mut positional = positional.into_iter();
|
||||
|
||||
let addr = positional
|
||||
.next()
|
||||
.unwrap_or_else(|| "127.0.0.1:3323".to_string());
|
||||
|
||||
let version = args
|
||||
let version = positional
|
||||
.next()
|
||||
.map(|s| {
|
||||
s.parse::<u8>().map_err(|e| {
|
||||
@@ -526,10 +692,10 @@ impl Config {
|
||||
));
|
||||
}
|
||||
|
||||
let mode = match args.next().as_deref() {
|
||||
let mode = match positional.next().as_deref() {
|
||||
None | Some("reset") => QueryMode::Reset,
|
||||
Some("serial") => {
|
||||
let session_id = args
|
||||
let session_id = positional
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
@@ -545,7 +711,7 @@ impl Config {
|
||||
)
|
||||
})?;
|
||||
|
||||
let serial = args
|
||||
let serial = positional
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
@@ -571,12 +737,212 @@ impl Config {
|
||||
}
|
||||
};
|
||||
|
||||
let transport = finalize_transport(transport, &addr)?;
|
||||
|
||||
Ok(Self {
|
||||
addr,
|
||||
version,
|
||||
mode,
|
||||
read_timeout_secs: DEFAULT_READ_TIMEOUT_SECS,
|
||||
default_poll_secs: DEFAULT_POLL_INTERVAL_SECS,
|
||||
read_timeout_secs,
|
||||
default_poll_secs,
|
||||
transport,
|
||||
keep_after_error,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TransportConfig {
|
||||
Tcp,
|
||||
Tls(TlsConfig),
|
||||
}
|
||||
|
||||
impl TransportConfig {
|
||||
fn describe(&self) -> String {
|
||||
match self {
|
||||
Self::Tcp => "tcp".to_string(),
|
||||
Self::Tls(cfg) => format!(
|
||||
"tls (server_name={}, ca_cert={}, client_cert={})",
|
||||
cfg.server_name.as_deref().unwrap_or("<unset>"),
|
||||
cfg.ca_cert
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "<unset>".to_string()),
|
||||
cfg.client_cert
|
||||
.as_ref()
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "<none>".to_string())
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct TlsConfig {
|
||||
server_name: Option<String>,
|
||||
ca_cert: Option<PathBuf>,
|
||||
client_cert: Option<PathBuf>,
|
||||
client_key: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn ensure_tls_config(transport: &mut TransportConfig) -> io::Result<&mut TlsConfig> {
|
||||
if matches!(transport, TransportConfig::Tcp) {
|
||||
*transport = TransportConfig::Tls(TlsConfig::default());
|
||||
}
|
||||
|
||||
match transport {
|
||||
TransportConfig::Tls(cfg) => Ok(cfg),
|
||||
TransportConfig::Tcp => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_transport(transport: TransportConfig, addr: &str) -> io::Result<TransportConfig> {
|
||||
match transport {
|
||||
TransportConfig::Tcp => Ok(TransportConfig::Tcp),
|
||||
TransportConfig::Tls(mut cfg) => {
|
||||
let ca_cert = cfg.ca_cert.take().ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"TLS mode requires --ca-cert <path>",
|
||||
)
|
||||
})?;
|
||||
|
||||
match (&cfg.client_cert, &cfg.client_key) {
|
||||
(Some(_), Some(_)) | (None, None) => {}
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"TLS client authentication requires both --client-cert and --client-key",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let server_name = cfg
|
||||
.server_name
|
||||
.take()
|
||||
.or_else(|| default_server_name_for_addr(addr))
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"TLS mode requires --server-name or an address with a parsable host",
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(TransportConfig::Tls(TlsConfig {
|
||||
server_name: Some(server_name),
|
||||
ca_cert: Some(ca_cert),
|
||||
client_cert: cfg.client_cert,
|
||||
client_key: cfg.client_key,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_stream(config: &Config) -> io::Result<DynStream> {
|
||||
match &config.transport {
|
||||
TransportConfig::Tcp => Ok(Box::new(TcpStream::connect(&config.addr).await?)),
|
||||
TransportConfig::Tls(tls) => connect_tls_stream(&config.addr, tls).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream> {
|
||||
let stream = TcpStream::connect(addr).await?;
|
||||
let connector = build_tls_connector(tls)?;
|
||||
let server_name_str = tls
|
||||
.server_name
|
||||
.as_ref()
|
||||
.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| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid TLS server name '{}': {}", server_name_str, err),
|
||||
)
|
||||
})?;
|
||||
let tls_stream = connector.connect(server_name, stream).await.map_err(|err| {
|
||||
io::Error::new(io::ErrorKind::ConnectionAborted, format!("TLS handshake failed: {}", err))
|
||||
})?;
|
||||
Ok(Box::new(tls_stream))
|
||||
}
|
||||
|
||||
fn build_tls_connector(tls: &TlsConfig) -> io::Result<TlsConnector> {
|
||||
let ca_cert_path = tls
|
||||
.ca_cert
|
||||
.as_ref()
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "missing TLS CA cert"))?;
|
||||
let ca_certs = load_certs(ca_cert_path)?;
|
||||
let mut roots = RootCertStore::empty();
|
||||
let (added, _ignored) = roots.add_parsable_certificates(ca_certs);
|
||||
if added == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("no valid CA certificates found in {}", ca_cert_path.display()),
|
||||
));
|
||||
}
|
||||
|
||||
let builder = RustlsClientConfig::builder().with_root_certificates(roots);
|
||||
let client_config = match (&tls.client_cert, &tls.client_key) {
|
||||
(Some(cert_path), Some(key_path)) => {
|
||||
let certs = load_certs(cert_path)?;
|
||||
let key = load_private_key(key_path)?;
|
||||
builder.with_client_auth_cert(certs, key).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid TLS client certificate/key: {}", err),
|
||||
)
|
||||
})?
|
||||
}
|
||||
(None, None) => builder.with_no_client_auth(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
Ok(TlsConnector::from(Arc::new(client_config)))
|
||||
}
|
||||
|
||||
fn load_certs(path: &Path) -> io::Result<Vec<CertificateDer<'static>>> {
|
||||
let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
|
||||
let certs = rustls_pemfile::certs(&mut reader)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
|
||||
if certs.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("no certificates found in {}", path.display()),
|
||||
));
|
||||
}
|
||||
Ok(certs)
|
||||
}
|
||||
|
||||
fn load_private_key(path: &Path) -> io::Result<PrivateKeyDer<'static>> {
|
||||
let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
|
||||
rustls_pemfile::private_key(&mut reader)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("no private key found in {}", path.display()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn default_server_name_for_addr(addr: &str) -> Option<String> {
|
||||
if let Some(rest) = addr.strip_prefix('[') {
|
||||
return rest.split(']').next().map(str::to_string);
|
||||
}
|
||||
addr.rsplit_once(':').map(|(host, _port)| host.to_string())
|
||||
}
|
||||
|
||||
fn parse_u64_arg(value: &str, name: &str) -> io::Result<u64> {
|
||||
let parsed = value.parse::<u64>().map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid value for {}: {}", name, err),
|
||||
)
|
||||
})?;
|
||||
if parsed == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{} must be > 0", name),
|
||||
));
|
||||
}
|
||||
Ok(parsed)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ pub fn print_pdu(header: &PduHeader, body: &[u8]) {
|
||||
PduType::Ipv6Prefix => {
|
||||
print_ipv6_prefix(header, body);
|
||||
}
|
||||
PduType::RouterKey => {
|
||||
print_router_key(header, body);
|
||||
}
|
||||
PduType::EndOfData => {
|
||||
print_end_of_data(header, body);
|
||||
}
|
||||
@@ -128,7 +131,11 @@ fn print_end_of_data(header: &PduHeader, body: &[u8]) {
|
||||
}
|
||||
|
||||
fn print_error_report(header: &PduHeader, body: &[u8]) {
|
||||
println!("error_code : {}", header.error_code());
|
||||
println!(
|
||||
"error_code : {} ({})",
|
||||
header.error_code(),
|
||||
error_code_name(header.error_code())
|
||||
);
|
||||
|
||||
if body.len() < 8 {
|
||||
println!("invalid ErrorReport body length: {}", body.len());
|
||||
@@ -165,8 +172,27 @@ fn print_error_report(header: &PduHeader, body: &[u8]) {
|
||||
let text = String::from_utf8_lossy(text_bytes);
|
||||
|
||||
println!("encap_len : {}", encapsulated_len);
|
||||
if let Some(encap_header) = parse_encapsulated_header(encapsulated) {
|
||||
println!("encap_pdu_type : {}", encap_header.pdu_type());
|
||||
println!("encap_version : {}", encap_header.version);
|
||||
println!("encap_length : {}", encap_header.length);
|
||||
match encap_header.pdu_type() {
|
||||
PduType::ErrorReport => {
|
||||
println!("encap_field1 : error_code={}", encap_header.error_code());
|
||||
}
|
||||
PduType::Unknown(_) => {
|
||||
println!("encap_field1 : {}", encap_header.field1);
|
||||
}
|
||||
_ => {
|
||||
println!("encap_field1 : session_id={}", encap_header.session_id());
|
||||
}
|
||||
}
|
||||
} else if encapsulated_len > 0 {
|
||||
println!("encap_header : <truncated or unavailable>");
|
||||
}
|
||||
println!("encap_pdu : {}", hex_bytes(encapsulated));
|
||||
println!("text_len : {}", text_len);
|
||||
println!("text_utf8 : {}", std::str::from_utf8(text_bytes).is_ok());
|
||||
println!("text : {}", text);
|
||||
}
|
||||
|
||||
@@ -194,7 +220,6 @@ fn print_serial_query(header: &PduHeader, body: &[u8]) {
|
||||
println!("serial : {}", serial);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn print_router_key(header: &PduHeader, body: &[u8]) {
|
||||
println!("session_id : {}", header.session_id());
|
||||
|
||||
@@ -218,6 +243,34 @@ fn print_router_key(header: &PduHeader, body: &[u8]) {
|
||||
println!("spki : {}", hex_bytes(spki));
|
||||
}
|
||||
|
||||
fn error_code_name(code: u16) -> &'static str {
|
||||
match code {
|
||||
0 => "Corrupt Data",
|
||||
1 => "Internal Error",
|
||||
2 => "No Data Available",
|
||||
3 => "Invalid Request",
|
||||
4 => "Unsupported Protocol Version",
|
||||
5 => "Unsupported PDU Type",
|
||||
6 => "Withdrawal of Unknown Record",
|
||||
7 => "Duplicate Announcement Received",
|
||||
8 => "Unexpected Protocol Version",
|
||||
9 => "ASPA Provider List Error",
|
||||
10 => "Transport Failure",
|
||||
11 => "Ordering Error",
|
||||
_ => "Unknown Error Code",
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_encapsulated_header(encapsulated: &[u8]) -> Option<PduHeader> {
|
||||
if encapsulated.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut header = [0u8; 8];
|
||||
header.copy_from_slice(&encapsulated[..8]);
|
||||
Some(PduHeader::from_bytes(header))
|
||||
}
|
||||
|
||||
fn print_aspa(header: &PduHeader, body: &[u8]) {
|
||||
println!("session_id : {}", header.session_id());
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ pub enum PduType {
|
||||
CacheResponse,
|
||||
Ipv4Prefix,
|
||||
Ipv6Prefix,
|
||||
RouterKey,
|
||||
EndOfData,
|
||||
CacheReset,
|
||||
ErrorReport,
|
||||
@@ -41,6 +42,7 @@ impl PduType {
|
||||
Self::CacheResponse => 3,
|
||||
Self::Ipv4Prefix => 4,
|
||||
Self::Ipv6Prefix => 6,
|
||||
Self::RouterKey => 9,
|
||||
Self::EndOfData => 7,
|
||||
Self::CacheReset => 8,
|
||||
Self::ErrorReport => 10,
|
||||
@@ -57,6 +59,7 @@ impl PduType {
|
||||
Self::CacheResponse => "Cache Response",
|
||||
Self::Ipv4Prefix => "IPv4 Prefix",
|
||||
Self::Ipv6Prefix => "IPv6 Prefix",
|
||||
Self::RouterKey => "Router Key",
|
||||
Self::EndOfData => "End of Data",
|
||||
Self::CacheReset => "Cache Reset",
|
||||
Self::ErrorReport => "Error Report",
|
||||
@@ -75,6 +78,7 @@ impl From<u8> for PduType {
|
||||
3 => Self::CacheResponse,
|
||||
4 => Self::Ipv4Prefix,
|
||||
6 => Self::Ipv6Prefix,
|
||||
9 => Self::RouterKey,
|
||||
7 => Self::EndOfData,
|
||||
8 => Self::CacheReset,
|
||||
10 => Self::ErrorReport,
|
||||
@@ -152,4 +156,4 @@ pub fn hex_bytes(data: &[u8]) -> String {
|
||||
let _ = write!(out, "{:02x}", b);
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user