RTR server以及client的开发、test的补充
This commit is contained in:
63
src/bin/rtr_debug_client/README.md
Normal file
63
src/bin/rtr_debug_client/README.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# rtr_debug_client
|
||||
|
||||
`rtr_debug_client` 是一个用于调试和联调 RTR(RPKI-to-Router)服务端的小型客户端工具。
|
||||
|
||||
它的目标不是做一个完整的生产级 router client,而是提供一个简单、直接、可观察的调试入口,用于:
|
||||
|
||||
- 连接 RTR server
|
||||
- 发送 `Reset Query` 或 `Serial Query`
|
||||
- 接收并打印服务端返回的 PDU
|
||||
- 辅助排查协议实现、会话状态、序列号增量、PDU 编码等问题
|
||||
|
||||
---
|
||||
|
||||
## 适用场景
|
||||
|
||||
这个工具适合以下场景:
|
||||
|
||||
- 开发 RTR server 时做本地联调
|
||||
- 验证服务端是否正确返回 `Cache Response`
|
||||
- 检查 `IPv4 Prefix` / `IPv6 Prefix` / `ASPA` / `End of Data` 等 PDU
|
||||
- 验证 `Serial Query` 路径是否正确
|
||||
- 观察异常响应,例如 `Cache Reset` 或 `Error Report`
|
||||
- 后续扩展为支持 TLS、自动断言、会话统计等调试能力
|
||||
|
||||
---
|
||||
|
||||
## 当前能力
|
||||
|
||||
当前版本支持:
|
||||
|
||||
- TCP 连接 RTR server
|
||||
- 发送 `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 长度限制,防止异常数据导致过大内存分配
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
建议目录如下:
|
||||
|
||||
```text
|
||||
src/
|
||||
└── bin/
|
||||
└── rtr_debug_client/
|
||||
├── main.rs
|
||||
├── protocol.rs
|
||||
├── io.rs
|
||||
├── pretty.rs
|
||||
└── README.md
|
||||
582
src/bin/rtr_debug_client/main.rs
Normal file
582
src/bin/rtr_debug_client/main.rs
Normal file
@@ -0,0 +1,582 @@
|
||||
use std::env;
|
||||
use std::io;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::net::tcp::{OwnedReadHalf, OwnedWriteHalf};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration, Instant};
|
||||
|
||||
mod wire;
|
||||
mod pretty;
|
||||
mod protocol;
|
||||
|
||||
use crate::wire::{read_pdu, send_reset_query, send_serial_query};
|
||||
use crate::pretty::{
|
||||
parse_end_of_data_info, parse_serial_notify_serial, print_pdu,
|
||||
};
|
||||
use crate::protocol::{PduHeader, PduType, QueryMode};
|
||||
|
||||
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
|
||||
const DEFAULT_POLL_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let config = Config::from_args()?;
|
||||
|
||||
println!("== RTR debug client ==");
|
||||
println!("target : {}", config.addr);
|
||||
println!("version : {}", config.version);
|
||||
println!("timeout : {}s", config.read_timeout_secs);
|
||||
println!("poll : {}s (default before EndOfData refresh is known)", config.default_poll_secs);
|
||||
match &config.mode {
|
||||
QueryMode::Reset => {
|
||||
println!("mode : reset");
|
||||
}
|
||||
QueryMode::Serial { session_id, serial } => {
|
||||
println!("mode : serial");
|
||||
println!("session : {}", session_id);
|
||||
println!("serial : {}", serial);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
print_help();
|
||||
|
||||
let stream = TcpStream::connect(&config.addr).await?;
|
||||
println!("connected to {}", config.addr);
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
let mut state = ClientState::new(
|
||||
config.version,
|
||||
config.read_timeout_secs,
|
||||
config.default_poll_secs,
|
||||
);
|
||||
|
||||
match config.mode {
|
||||
QueryMode::Reset => {
|
||||
send_reset_query(&mut writer, config.version).await?;
|
||||
println!("sent Reset Query");
|
||||
}
|
||||
QueryMode::Serial { session_id, serial } => {
|
||||
state.session_id = Some(session_id);
|
||||
state.serial = Some(serial);
|
||||
send_serial_query(&mut writer, config.version, session_id, serial).await?;
|
||||
println!("sent Serial Query");
|
||||
}
|
||||
}
|
||||
|
||||
state.schedule_next_poll();
|
||||
println!();
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut stdin_lines = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
let poll_sleep = tokio::time::sleep_until(state.next_poll_deadline);
|
||||
tokio::pin!(poll_sleep);
|
||||
|
||||
tokio::select! {
|
||||
line = stdin_lines.next_line() => {
|
||||
match line {
|
||||
Ok(Some(line)) => {
|
||||
let should_quit = handle_console_command(
|
||||
&line,
|
||||
&mut writer,
|
||||
&mut state,
|
||||
).await?;
|
||||
|
||||
if should_quit {
|
||||
println!("quit requested, closing client.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("stdin closed, continue network loop.");
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("read stdin failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = &mut poll_sleep => {
|
||||
handle_poll_tick(&mut writer, &mut state).await?;
|
||||
state.schedule_next_poll();
|
||||
}
|
||||
|
||||
read_result = timeout(
|
||||
Duration::from_secs(state.read_timeout_secs),
|
||||
read_pdu(&mut reader)
|
||||
) => {
|
||||
match read_result {
|
||||
Ok(Ok(pdu)) => {
|
||||
print_pdu(&pdu.header, &pdu.body);
|
||||
handle_incoming_pdu(&mut writer, &mut state, &pdu.header, &pdu.body).await?;
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
eprintln!("read PDU failed: {}", err);
|
||||
return Err(err);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(
|
||||
"[timeout] no PDU received in {}s, connection kept open.",
|
||||
state.read_timeout_secs
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_incoming_pdu(
|
||||
writer: &mut OwnedWriteHalf,
|
||||
state: &mut ClientState,
|
||||
header: &PduHeader,
|
||||
body: &[u8],
|
||||
) -> io::Result<()> {
|
||||
match header.pdu_type() {
|
||||
PduType::CacheResponse => {
|
||||
state.current_session_id = Some(header.session_id());
|
||||
}
|
||||
|
||||
PduType::Ipv4Prefix | PduType::Ipv6Prefix | PduType::Aspa => {
|
||||
if state.current_session_id.is_none() {
|
||||
state.current_session_id = Some(header.session_id());
|
||||
}
|
||||
}
|
||||
|
||||
PduType::EndOfData => {
|
||||
let session_id = header.session_id();
|
||||
let eod = parse_end_of_data_info(body);
|
||||
|
||||
state.session_id = Some(session_id);
|
||||
state.current_session_id = Some(session_id);
|
||||
|
||||
println!();
|
||||
|
||||
if let Some(eod) = eod {
|
||||
state.serial = Some(eod.serial);
|
||||
state.refresh = eod.refresh;
|
||||
state.retry = eod.retry;
|
||||
state.expire = eod.expire;
|
||||
|
||||
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();
|
||||
println!(
|
||||
"next auto poll scheduled after {}s",
|
||||
state.effective_poll_secs()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"updated client state: session_id={}, serial=<unknown>",
|
||||
session_id
|
||||
);
|
||||
}
|
||||
|
||||
println!("received EndOfData, keep connection open.");
|
||||
println!();
|
||||
}
|
||||
|
||||
PduType::SerialNotify => {
|
||||
let notify_session_id = header.session_id();
|
||||
let notify_serial = parse_serial_notify_serial(body);
|
||||
|
||||
println!();
|
||||
|
||||
match (state.session_id, state.serial, notify_serial) {
|
||||
(Some(current_session_id), Some(current_serial), Some(_new_serial))
|
||||
if current_session_id == notify_session_id =>
|
||||
{
|
||||
println!(
|
||||
"received Serial Notify for current session {}, send Serial Query with serial {}",
|
||||
current_session_id, current_serial
|
||||
);
|
||||
send_serial_query(
|
||||
writer,
|
||||
state.version,
|
||||
current_session_id,
|
||||
current_serial,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
_ => {
|
||||
println!(
|
||||
"received Serial Notify but local session/serial state is not usable, send Reset Query"
|
||||
);
|
||||
send_reset_query(writer, state.version).await?;
|
||||
}
|
||||
}
|
||||
|
||||
state.schedule_next_poll();
|
||||
println!();
|
||||
}
|
||||
|
||||
PduType::CacheReset => {
|
||||
println!();
|
||||
println!("received Cache Reset, send Reset Query");
|
||||
state.current_session_id = None;
|
||||
state.serial = None;
|
||||
send_reset_query(writer, state.version).await?;
|
||||
state.schedule_next_poll();
|
||||
println!();
|
||||
}
|
||||
|
||||
PduType::ErrorReport => {
|
||||
println!();
|
||||
println!("received Error Report, keep connection open for debugging.");
|
||||
if let Some(retry) = state.retry {
|
||||
println!("will keep auto polling; server retry hint currently stored: {}s", retry);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
PduType::SerialQuery | PduType::ResetQuery | PduType::Unknown(_) => {
|
||||
// only print, no extra action
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_poll_tick(
|
||||
writer: &mut OwnedWriteHalf,
|
||||
state: &mut ClientState,
|
||||
) -> io::Result<()> {
|
||||
println!();
|
||||
println!(
|
||||
"[auto-poll] timer fired (interval={}s)",
|
||||
state.effective_poll_secs()
|
||||
);
|
||||
|
||||
match (state.session_id, state.serial) {
|
||||
(Some(session_id), Some(serial)) => {
|
||||
println!(
|
||||
"[auto-poll] send Serial Query with session_id={}, serial={}",
|
||||
session_id, serial
|
||||
);
|
||||
send_serial_query(writer, state.version, session_id, serial).await?;
|
||||
}
|
||||
_ => {
|
||||
println!("[auto-poll] local state incomplete, send Reset Query");
|
||||
send_reset_query(writer, state.version).await?;
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_console_command(
|
||||
line: &str,
|
||||
writer: &mut OwnedWriteHalf,
|
||||
state: &mut ClientState,
|
||||
) -> io::Result<bool> {
|
||||
let line = line.trim();
|
||||
|
||||
if line.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
|
||||
match parts.as_slice() {
|
||||
["help"] => {
|
||||
print_help();
|
||||
}
|
||||
|
||||
["state"] => {
|
||||
print_state(state);
|
||||
}
|
||||
|
||||
["reset"] => {
|
||||
println!("manual command: send Reset Query");
|
||||
send_reset_query(writer, state.version).await?;
|
||||
state.schedule_next_poll();
|
||||
}
|
||||
|
||||
["serial"] => {
|
||||
match (state.session_id, state.serial) {
|
||||
(Some(session_id), Some(serial)) => {
|
||||
println!(
|
||||
"manual command: send Serial Query with current state: session_id={}, serial={}",
|
||||
session_id, serial
|
||||
);
|
||||
send_serial_query(writer, state.version, session_id, serial).await?;
|
||||
state.schedule_next_poll();
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
"manual command failed: current session_id/serial not available, use `reset` or `serial <session_id> <serial>`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
["serial", session_id, serial] => {
|
||||
let session_id = match session_id.parse::<u16>() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
println!("invalid session_id: {}", err);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
let serial = match serial.parse::<u32>() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
println!("invalid serial: {}", err);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"manual command: send Serial Query with explicit args: session_id={}, serial={}",
|
||||
session_id, serial
|
||||
);
|
||||
state.session_id = Some(session_id);
|
||||
state.serial = Some(serial);
|
||||
send_serial_query(writer, state.version, session_id, serial).await?;
|
||||
state.schedule_next_poll();
|
||||
}
|
||||
|
||||
["timeout"] => {
|
||||
println!("current read timeout: {}s", state.read_timeout_secs);
|
||||
}
|
||||
|
||||
["timeout", secs] => {
|
||||
let secs = match secs.parse::<u64>() {
|
||||
Ok(v) if v > 0 => v,
|
||||
Ok(_) => {
|
||||
println!("timeout must be > 0");
|
||||
return Ok(false);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("invalid timeout seconds: {}", err);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
state.read_timeout_secs = secs;
|
||||
println!("updated read timeout to {}s", state.read_timeout_secs);
|
||||
}
|
||||
|
||||
["poll"] => {
|
||||
println!(
|
||||
"current effective poll interval: {}s",
|
||||
state.effective_poll_secs()
|
||||
);
|
||||
println!("stored refresh hint : {:?}", state.refresh);
|
||||
println!("default poll interval : {}s", state.default_poll_secs);
|
||||
}
|
||||
|
||||
["poll", secs] => {
|
||||
let secs = match secs.parse::<u64>() {
|
||||
Ok(v) if v > 0 => v,
|
||||
Ok(_) => {
|
||||
println!("poll interval must be > 0");
|
||||
return Ok(false);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("invalid poll seconds: {}", err);
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
state.refresh = Some(secs as u32);
|
||||
state.schedule_next_poll();
|
||||
println!("updated poll interval to {}s", secs);
|
||||
}
|
||||
|
||||
["quit"] | ["exit"] => {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
_ => {
|
||||
println!("unknown command: {}", line);
|
||||
print_help();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("available commands:");
|
||||
println!(" help show this help");
|
||||
println!(" state print current client state");
|
||||
println!(" reset send Reset Query");
|
||||
println!(" serial send Serial Query with current session_id/serial");
|
||||
println!(" serial <sid> <serial> send Serial Query with explicit values");
|
||||
println!(" timeout show current read timeout");
|
||||
println!(" timeout <secs> update read timeout seconds");
|
||||
println!(" poll show current poll interval");
|
||||
println!(" poll <secs> override poll interval seconds");
|
||||
println!(" quit exit client");
|
||||
println!();
|
||||
}
|
||||
|
||||
fn print_state(state: &ClientState) {
|
||||
println!("client state:");
|
||||
println!(" version : {}", state.version);
|
||||
println!(" session_id : {:?}", state.session_id);
|
||||
println!(" serial : {:?}", state.serial);
|
||||
println!(" current_session_id : {:?}", state.current_session_id);
|
||||
println!(" refresh : {:?}", state.refresh);
|
||||
println!(" retry : {:?}", state.retry);
|
||||
println!(" expire : {:?}", state.expire);
|
||||
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!();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientState {
|
||||
version: u8,
|
||||
session_id: Option<u16>,
|
||||
serial: Option<u32>,
|
||||
current_session_id: Option<u16>,
|
||||
|
||||
refresh: Option<u32>,
|
||||
retry: Option<u32>,
|
||||
expire: Option<u32>,
|
||||
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
next_poll_deadline: Instant,
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
fn new(version: u8, read_timeout_secs: u64, default_poll_secs: u64) -> Self {
|
||||
Self {
|
||||
version,
|
||||
session_id: None,
|
||||
serial: None,
|
||||
current_session_id: None,
|
||||
refresh: None,
|
||||
retry: None,
|
||||
expire: None,
|
||||
read_timeout_secs,
|
||||
default_poll_secs,
|
||||
next_poll_deadline: Instant::now() + Duration::from_secs(default_poll_secs),
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_poll_secs(&self) -> u64 {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Config {
|
||||
addr: String,
|
||||
version: u8,
|
||||
mode: QueryMode,
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
fn from_args() -> io::Result<Self> {
|
||||
let mut args = env::args().skip(1);
|
||||
|
||||
let addr = args
|
||||
.next()
|
||||
.unwrap_or_else(|| "127.0.0.1:3323".to_string());
|
||||
|
||||
let version = args
|
||||
.next()
|
||||
.map(|s| {
|
||||
s.parse::<u8>().map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid version '{}': {}", s, e),
|
||||
)
|
||||
})
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or(1);
|
||||
|
||||
if version > 2 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unsupported RTR version {}, expected 0..=2", version),
|
||||
));
|
||||
}
|
||||
|
||||
let mode = match args.next().as_deref() {
|
||||
None | Some("reset") => QueryMode::Reset,
|
||||
Some("serial") => {
|
||||
let session_id = args
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"serial mode requires session_id and serial",
|
||||
)
|
||||
})?
|
||||
.parse::<u16>()
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid session_id: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let serial = args
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"serial mode requires serial",
|
||||
)
|
||||
})?
|
||||
.parse::<u32>()
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid serial: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
QueryMode::Serial { session_id, serial }
|
||||
}
|
||||
Some(other) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid mode '{}', expected 'reset' or 'serial'", other),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
addr,
|
||||
version,
|
||||
mode,
|
||||
read_timeout_secs: DEFAULT_READ_TIMEOUT_SECS,
|
||||
default_poll_secs: DEFAULT_POLL_INTERVAL_SECS,
|
||||
})
|
||||
}
|
||||
}
|
||||
303
src/bin/rtr_debug_client/pretty.rs
Normal file
303
src/bin/rtr_debug_client/pretty.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use crate::protocol::{
|
||||
flag_meaning, hex_bytes, PduHeader, PduType, ASPA_FIXED_BODY_LEN,
|
||||
END_OF_DATA_V0_BODY_LEN, END_OF_DATA_V1_BODY_LEN, IPV4_PREFIX_BODY_LEN,
|
||||
IPV6_PREFIX_BODY_LEN, ROUTER_KEY_FIXED_BODY_LEN,
|
||||
};
|
||||
|
||||
pub fn print_pdu(header: &PduHeader, body: &[u8]) {
|
||||
println!("--------------------------------------------------");
|
||||
println!("PDU: {}", header.pdu_type());
|
||||
println!("version : {}", header.version);
|
||||
println!("length : {}", header.length);
|
||||
|
||||
match header.pdu_type() {
|
||||
PduType::CacheResponse => {
|
||||
println!("session_id : {}", header.session_id());
|
||||
}
|
||||
PduType::CacheReset => {
|
||||
println!("cache reset");
|
||||
}
|
||||
PduType::Ipv4Prefix => {
|
||||
print_ipv4_prefix(header, body);
|
||||
}
|
||||
PduType::Ipv6Prefix => {
|
||||
print_ipv6_prefix(header, body);
|
||||
}
|
||||
PduType::EndOfData => {
|
||||
print_end_of_data(header, body);
|
||||
}
|
||||
PduType::ErrorReport => {
|
||||
print_error_report(header, body);
|
||||
}
|
||||
PduType::SerialNotify => {
|
||||
print_serial_notify(header, body);
|
||||
}
|
||||
PduType::SerialQuery => {
|
||||
print_serial_query(header, body);
|
||||
}
|
||||
PduType::Aspa => {
|
||||
print_aspa(header, body);
|
||||
}
|
||||
PduType::ResetQuery => {
|
||||
println!("reset query");
|
||||
}
|
||||
PduType::Unknown(_) => {
|
||||
println!("field1 : {}", header.field1);
|
||||
println!("body : {}", hex_bytes(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_ipv4_prefix(header: &PduHeader, body: &[u8]) {
|
||||
if body.len() != IPV4_PREFIX_BODY_LEN {
|
||||
println!("invalid IPv4 Prefix body length: {}", body.len());
|
||||
println!("raw body: {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = body[0];
|
||||
let prefix_len = body[1];
|
||||
let max_len = body[2];
|
||||
let zero = body[3];
|
||||
let prefix = Ipv4Addr::new(body[4], body[5], body[6], body[7]);
|
||||
let asn = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
||||
|
||||
println!("session_id : {}", header.session_id());
|
||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
||||
println!("prefix_len : {}", prefix_len);
|
||||
println!("max_len : {}", max_len);
|
||||
println!("zero : {}", zero);
|
||||
println!("prefix : {}", prefix);
|
||||
println!("asn : {}", asn);
|
||||
}
|
||||
|
||||
fn print_ipv6_prefix(header: &PduHeader, body: &[u8]) {
|
||||
if body.len() != IPV6_PREFIX_BODY_LEN {
|
||||
println!("invalid IPv6 Prefix body length: {}", body.len());
|
||||
println!("raw body: {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = body[0];
|
||||
let prefix_len = body[1];
|
||||
let max_len = body[2];
|
||||
let zero = body[3];
|
||||
|
||||
let mut addr = [0u8; 16];
|
||||
addr.copy_from_slice(&body[4..20]);
|
||||
let prefix = Ipv6Addr::from(addr);
|
||||
|
||||
let asn = u32::from_be_bytes([body[20], body[21], body[22], body[23]]);
|
||||
|
||||
println!("session_id : {}", header.session_id());
|
||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
||||
println!("prefix_len : {}", prefix_len);
|
||||
println!("max_len : {}", max_len);
|
||||
println!("zero : {}", zero);
|
||||
println!("prefix : {}", prefix);
|
||||
println!("asn : {}", asn);
|
||||
}
|
||||
|
||||
fn print_end_of_data(header: &PduHeader, body: &[u8]) {
|
||||
println!("session_id : {}", header.session_id());
|
||||
|
||||
match body.len() {
|
||||
END_OF_DATA_V0_BODY_LEN => {
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
println!("serial : {}", serial);
|
||||
println!("variant : v0");
|
||||
}
|
||||
END_OF_DATA_V1_BODY_LEN => {
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
||||
println!("serial : {}", serial);
|
||||
println!("refresh : {}", refresh);
|
||||
println!("retry : {}", retry);
|
||||
println!("expire : {}", expire);
|
||||
println!("variant : v1/v2");
|
||||
}
|
||||
_ => {
|
||||
println!("invalid EndOfData body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_error_report(header: &PduHeader, body: &[u8]) {
|
||||
println!("error_code : {}", header.error_code());
|
||||
|
||||
if body.len() < 8 {
|
||||
println!("invalid ErrorReport body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let encapsulated_len =
|
||||
u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
|
||||
|
||||
if body.len() < 4 + encapsulated_len + 4 {
|
||||
println!("invalid ErrorReport: truncated encapsulated PDU");
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let encapsulated = &body[4..4 + encapsulated_len];
|
||||
|
||||
let text_len_offset = 4 + encapsulated_len;
|
||||
let text_len = u32::from_be_bytes([
|
||||
body[text_len_offset],
|
||||
body[text_len_offset + 1],
|
||||
body[text_len_offset + 2],
|
||||
body[text_len_offset + 3],
|
||||
]) as usize;
|
||||
|
||||
if body.len() < text_len_offset + 4 + text_len {
|
||||
println!("invalid ErrorReport: truncated text");
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let text_bytes = &body[text_len_offset + 4..text_len_offset + 4 + text_len];
|
||||
let text = String::from_utf8_lossy(text_bytes);
|
||||
|
||||
println!("encap_len : {}", encapsulated_len);
|
||||
println!("encap_pdu : {}", hex_bytes(encapsulated));
|
||||
println!("text_len : {}", text_len);
|
||||
println!("text : {}", text);
|
||||
}
|
||||
|
||||
fn print_serial_notify(header: &PduHeader, body: &[u8]) {
|
||||
if body.len() != 4 {
|
||||
println!("invalid Serial Notify body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
println!("session_id : {}", header.session_id());
|
||||
println!("serial : {}", serial);
|
||||
}
|
||||
|
||||
fn print_serial_query(header: &PduHeader, body: &[u8]) {
|
||||
if body.len() != 4 {
|
||||
println!("invalid Serial Query body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
println!("session_id : {}", header.session_id());
|
||||
println!("serial : {}", serial);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn print_router_key(header: &PduHeader, body: &[u8]) {
|
||||
println!("session_id : {}", header.session_id());
|
||||
|
||||
if body.len() < ROUTER_KEY_FIXED_BODY_LEN {
|
||||
println!("invalid Router Key body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = body[0];
|
||||
let zero = body[1];
|
||||
let ski = &body[2..22];
|
||||
let asn = u32::from_be_bytes([body[22], body[23], body[24], body[25]]);
|
||||
let spki = &body[26..];
|
||||
|
||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
||||
println!("zero : {}", zero);
|
||||
println!("ski : {}", hex_bytes(ski));
|
||||
println!("asn : {}", asn);
|
||||
println!("spki_len : {}", spki.len());
|
||||
println!("spki : {}", hex_bytes(spki));
|
||||
}
|
||||
|
||||
fn print_aspa(header: &PduHeader, body: &[u8]) {
|
||||
println!("session_id : {}", header.session_id());
|
||||
|
||||
if body.len() < ASPA_FIXED_BODY_LEN {
|
||||
println!("invalid ASPA body length: {}", body.len());
|
||||
println!("raw body : {}", hex_bytes(body));
|
||||
return;
|
||||
}
|
||||
|
||||
let flags = body[0];
|
||||
let zero1 = body[1];
|
||||
let zero2 = body[2];
|
||||
let zero3 = body[3];
|
||||
let customer_asn = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
||||
|
||||
println!("flags : 0x{:02x} ({})", flags, flag_meaning(flags));
|
||||
println!("reserved : [{}, {}, {}]", zero1, zero2, zero3);
|
||||
println!("customer_as : {}", customer_asn);
|
||||
|
||||
let providers_raw = &body[8..];
|
||||
if providers_raw.len() % 4 != 0 {
|
||||
println!("invalid ASPA providers length: {}", providers_raw.len());
|
||||
println!("providers : {}", hex_bytes(providers_raw));
|
||||
return;
|
||||
}
|
||||
|
||||
let mut providers = Vec::new();
|
||||
for chunk in providers_raw.chunks_exact(4) {
|
||||
providers.push(u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
||||
}
|
||||
|
||||
println!("providers : {:?}", providers);
|
||||
}
|
||||
|
||||
pub fn parse_serial_notify_serial(body: &[u8]) -> Option<u32> {
|
||||
if body.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]]))
|
||||
}
|
||||
|
||||
pub fn parse_end_of_data_serial(body: &[u8]) -> Option<u32> {
|
||||
match body.len() {
|
||||
4 | 16 => Some(u32::from_be_bytes([body[0], body[1], body[2], body[3]])),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EndOfDataInfo {
|
||||
pub serial: u32,
|
||||
pub refresh: Option<u32>,
|
||||
pub retry: Option<u32>,
|
||||
pub expire: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn parse_end_of_data_info(body: &[u8]) -> Option<EndOfDataInfo> {
|
||||
match body.len() {
|
||||
4 => {
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
Some(EndOfDataInfo {
|
||||
serial,
|
||||
refresh: None,
|
||||
retry: None,
|
||||
expire: None,
|
||||
})
|
||||
}
|
||||
16 => {
|
||||
let serial = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
|
||||
let refresh = u32::from_be_bytes([body[4], body[5], body[6], body[7]]);
|
||||
let retry = u32::from_be_bytes([body[8], body[9], body[10], body[11]]);
|
||||
let expire = u32::from_be_bytes([body[12], body[13], body[14], body[15]]);
|
||||
Some(EndOfDataInfo {
|
||||
serial,
|
||||
refresh: Some(refresh),
|
||||
retry: Some(retry),
|
||||
expire: Some(expire),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
155
src/bin/rtr_debug_client/protocol.rs
Normal file
155
src/bin/rtr_debug_client/protocol.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::fmt;
|
||||
|
||||
pub const HEADER_LEN: usize = 8;
|
||||
pub const SERIAL_QUERY_LEN: usize = 12;
|
||||
pub const MAX_PDU_LEN: u32 = 1024 * 1024; // 1 MiB
|
||||
|
||||
pub const IPV4_PREFIX_BODY_LEN: usize = 12;
|
||||
pub const IPV6_PREFIX_BODY_LEN: usize = 24;
|
||||
pub const END_OF_DATA_V0_BODY_LEN: usize = 4;
|
||||
pub const END_OF_DATA_V1_BODY_LEN: usize = 16;
|
||||
pub const ROUTER_KEY_FIXED_BODY_LEN: usize = 26;
|
||||
pub const ASPA_FIXED_BODY_LEN: usize = 8;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum QueryMode {
|
||||
Reset,
|
||||
Serial { session_id: u16, serial: u32 },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PduType {
|
||||
SerialNotify,
|
||||
SerialQuery,
|
||||
ResetQuery,
|
||||
CacheResponse,
|
||||
Ipv4Prefix,
|
||||
Ipv6Prefix,
|
||||
EndOfData,
|
||||
CacheReset,
|
||||
ErrorReport,
|
||||
Aspa,
|
||||
Unknown(u8),
|
||||
}
|
||||
|
||||
impl PduType {
|
||||
pub fn code(self) -> u8 {
|
||||
match self {
|
||||
Self::SerialNotify => 0,
|
||||
Self::SerialQuery => 1,
|
||||
Self::ResetQuery => 2,
|
||||
Self::CacheResponse => 3,
|
||||
Self::Ipv4Prefix => 4,
|
||||
Self::Ipv6Prefix => 6,
|
||||
Self::EndOfData => 7,
|
||||
Self::CacheReset => 8,
|
||||
Self::ErrorReport => 10,
|
||||
Self::Aspa => 11,
|
||||
Self::Unknown(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::SerialNotify => "Serial Notify",
|
||||
Self::SerialQuery => "Serial Query",
|
||||
Self::ResetQuery => "Reset Query",
|
||||
Self::CacheResponse => "Cache Response",
|
||||
Self::Ipv4Prefix => "IPv4 Prefix",
|
||||
Self::Ipv6Prefix => "IPv6 Prefix",
|
||||
Self::EndOfData => "End of Data",
|
||||
Self::CacheReset => "Cache Reset",
|
||||
Self::ErrorReport => "Error Report",
|
||||
Self::Aspa => "ASPA",
|
||||
Self::Unknown(_) => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PduType {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => Self::SerialNotify,
|
||||
1 => Self::SerialQuery,
|
||||
2 => Self::ResetQuery,
|
||||
3 => Self::CacheResponse,
|
||||
4 => Self::Ipv4Prefix,
|
||||
6 => Self::Ipv6Prefix,
|
||||
7 => Self::EndOfData,
|
||||
8 => Self::CacheReset,
|
||||
10 => Self::ErrorReport,
|
||||
11 => Self::Aspa,
|
||||
x => Self::Unknown(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PduType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Unknown(v) => write!(f, "{} ({})", self.name(), v),
|
||||
_ => write!(f, "{} ({})", self.name(), self.code()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PduHeader {
|
||||
pub version: u8,
|
||||
pub pdu_type_raw: u8,
|
||||
pub field1: u16,
|
||||
pub length: u32,
|
||||
}
|
||||
|
||||
impl PduHeader {
|
||||
pub fn from_bytes(buf: [u8; HEADER_LEN]) -> Self {
|
||||
Self {
|
||||
version: buf[0],
|
||||
pdu_type_raw: buf[1],
|
||||
field1: u16::from_be_bytes([buf[2], buf[3]]),
|
||||
length: u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pdu_type(&self) -> PduType {
|
||||
self.pdu_type_raw.into()
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> u16 {
|
||||
self.field1
|
||||
}
|
||||
|
||||
pub fn error_code(&self) -> u16 {
|
||||
self.field1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RawPdu {
|
||||
pub header: PduHeader,
|
||||
pub body: Vec<u8>,
|
||||
}
|
||||
|
||||
pub fn flag_meaning(flags: u8) -> &'static str {
|
||||
if flags & 0x01 == 0x01 {
|
||||
"announcement"
|
||||
} else {
|
||||
"withdrawal"
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hex_bytes(data: &[u8]) -> String {
|
||||
if data.is_empty() {
|
||||
return "<empty>".to_string();
|
||||
}
|
||||
|
||||
let mut out = String::with_capacity(data.len() * 3 - 1);
|
||||
for (idx, b) in data.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
out.push(' ');
|
||||
}
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(out, "{:02x}", b);
|
||||
}
|
||||
out
|
||||
}
|
||||
81
src/bin/rtr_debug_client/wire.rs
Normal file
81
src/bin/rtr_debug_client/wire.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use std::io;
|
||||
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
use crate::protocol::{
|
||||
PduHeader, PduType, RawPdu, HEADER_LEN, MAX_PDU_LEN, SERIAL_QUERY_LEN,
|
||||
};
|
||||
|
||||
pub async fn send_reset_query<S>(stream: &mut S, version: u8) -> io::Result<()>
|
||||
where
|
||||
S: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut buf = [0u8; HEADER_LEN];
|
||||
buf[0] = version;
|
||||
buf[1] = PduType::ResetQuery.code();
|
||||
buf[2..4].copy_from_slice(&0u16.to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&(HEADER_LEN as u32).to_be_bytes());
|
||||
stream.write_all(&buf).await?;
|
||||
stream.flush().await
|
||||
}
|
||||
|
||||
pub async fn send_serial_query<S>(
|
||||
stream: &mut S,
|
||||
version: u8,
|
||||
session_id: u16,
|
||||
serial: u32,
|
||||
) -> io::Result<()>
|
||||
where
|
||||
S: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut buf = [0u8; SERIAL_QUERY_LEN];
|
||||
buf[0] = version;
|
||||
buf[1] = PduType::SerialQuery.code();
|
||||
buf[2..4].copy_from_slice(&session_id.to_be_bytes());
|
||||
buf[4..8].copy_from_slice(&(SERIAL_QUERY_LEN as u32).to_be_bytes());
|
||||
buf[8..12].copy_from_slice(&serial.to_be_bytes());
|
||||
stream.write_all(&buf).await?;
|
||||
stream.flush().await
|
||||
}
|
||||
|
||||
pub async fn read_header<S>(stream: &mut S) -> io::Result<PduHeader>
|
||||
where
|
||||
S: AsyncRead + Unpin,
|
||||
{
|
||||
let mut buf = [0u8; HEADER_LEN];
|
||||
stream.read_exact(&mut buf).await?;
|
||||
Ok(PduHeader::from_bytes(buf))
|
||||
}
|
||||
|
||||
pub async fn read_pdu<S>(stream: &mut S) -> io::Result<RawPdu>
|
||||
where
|
||||
S: AsyncRead + Unpin,
|
||||
{
|
||||
let header = read_header(stream).await?;
|
||||
|
||||
if header.length < HEADER_LEN as u32 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"invalid PDU length {} < {}",
|
||||
header.length, HEADER_LEN
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if header.length > MAX_PDU_LEN {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"PDU length {} exceeds max allowed {}",
|
||||
header.length, MAX_PDU_LEN
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let body_len = header.length as usize - HEADER_LEN;
|
||||
let mut body = vec![0u8; body_len];
|
||||
stream.read_exact(&mut body).await?;
|
||||
|
||||
Ok(RawPdu { header, body })
|
||||
}
|
||||
Reference in New Issue
Block a user