补充rtr-client,完善tls连接等

This commit is contained in:
xiuting.xu
2026-03-25 10:08:40 +08:00
parent 9cbea4e2d0
commit 03c0ab0ec7
45 changed files with 7521 additions and 1835 deletions

View File

@@ -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,就能快速知道是哪一个请求触发了错误。

View File

@@ -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)
}

View File

@@ -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());

View File

@@ -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
}
}