RTR server以及client的开发、test的补充

This commit is contained in:
xiuting.xu
2026-03-17 16:36:42 +08:00
parent 251dea8e5e
commit 9cbea4e2d0
31 changed files with 4970 additions and 260 deletions

View 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

View 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,
})
}
}

View 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,
}
}

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

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