优化rtr

This commit is contained in:
xiuting.xu
2026-04-08 14:03:23 +08:00
parent b483c2af8b
commit 23cdad095d
16 changed files with 1835 additions and 1606 deletions

290
src/bin/ccr_fixture_gen.rs Normal file
View File

@@ -0,0 +1,290 @@
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use sha2::{Digest, Sha256};
const CONTENT_TYPE_OID: &str = "1.2.840.113549.1.9.16.1.54";
const SHA256_OID: &str = "2.16.840.1.101.3.4.2.1";
#[derive(Clone)]
struct Vrp {
addr: IpAddr,
prefix_len: u8,
max_len: u8,
asn: u32,
}
#[derive(Clone)]
struct Vap {
customer_asn: u32,
providers: Vec<u32>,
}
fn main() -> Result<()> {
let out_dir = parse_out_dir_arg();
fs::create_dir_all(&out_dir)
.with_context(|| format!("failed to create output dir {}", out_dir.display()))?;
write_snapshot(
&out_dir.join("20260403T000001Z-mini-a.ccr"),
"20260403000001Z",
vec![
Vrp {
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
prefix_len: 24,
max_len: 24,
asn: 65001,
},
Vrp {
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
prefix_len: 48,
max_len: 48,
asn: 65002,
},
],
vec![Vap {
customer_asn: 65010,
providers: vec![65011, 65012],
}],
)?;
write_snapshot(
&out_dir.join("20260403T000101Z-mini-b.ccr"),
"20260403000101Z",
vec![
Vrp {
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 0)),
prefix_len: 24,
max_len: 24,
asn: 65001,
},
Vrp {
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
prefix_len: 24,
max_len: 24,
asn: 65003,
},
Vrp {
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 1, 0, 0, 0, 0, 0)),
prefix_len: 48,
max_len: 48,
asn: 65002,
},
],
vec![Vap {
customer_asn: 65010,
providers: vec![65011, 65012, 65013],
}],
)?;
write_snapshot(
&out_dir.join("20260403T000201Z-mini-c.ccr"),
"20260403000201Z",
vec![
Vrp {
addr: IpAddr::V4(Ipv4Addr::new(10, 0, 1, 0)),
prefix_len: 24,
max_len: 24,
asn: 65003,
},
Vrp {
addr: IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 2, 0, 0, 0, 0, 0)),
prefix_len: 48,
max_len: 48,
asn: 65004,
},
],
vec![
Vap {
customer_asn: 65010,
providers: vec![65012, 65013],
},
Vap {
customer_asn: 65020,
providers: vec![65021],
},
],
)?;
println!("generated CCR fixtures under {}", out_dir.display());
Ok(())
}
fn parse_out_dir_arg() -> PathBuf {
let mut args = std::env::args().skip(1);
let mut out_dir = PathBuf::from("data");
while let Some(arg) = args.next() {
if arg == "--out-dir" {
if let Some(v) = args.next() {
out_dir = PathBuf::from(v);
}
}
}
out_dir
}
fn write_snapshot(path: &Path, produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Result<()> {
let bytes = encode_ccr_snapshot(produced_at, vrps, vaps);
fs::write(path, bytes).with_context(|| format!("failed to write {}", path.display()))?;
println!("wrote {}", path.display());
Ok(())
}
fn encode_ccr_snapshot(produced_at: &str, vrps: Vec<Vrp>, vaps: Vec<Vap>) -> Vec<u8> {
let vrp_sets = vrps
.into_iter()
.map(encode_roa_payload_set)
.collect::<Vec<_>>();
let vap_sets = vaps
.into_iter()
.map(encode_aspa_payload_set)
.collect::<Vec<_>>();
let vrp_set_seq = der_sequence(vrp_sets);
let vap_set_seq = der_sequence(vap_sets);
let vrp_hash = Sha256::digest(&vrp_set_seq).to_vec();
let vap_hash = Sha256::digest(&vap_set_seq).to_vec();
// draft-ietf-sidrops-rpki-ccr-02:
// ROAPayloadState/ASPAPayloadState include payload-set sequence + hash.
let vrp_state = der_sequence(vec![vrp_set_seq, der_octet_string(vrp_hash)]);
let vap_state = der_sequence(vec![vap_set_seq, der_octet_string(vap_hash)]);
// AlgorithmIdentifier for SHA-256.
let hash_alg = der_sequence(vec![der_oid(SHA256_OID), der_null()]);
let payload = der_sequence(vec![
der_integer(0),
hash_alg,
der_generalized_time(produced_at),
der_ctx(2, vrp_state),
der_ctx(3, vap_state),
]);
der_sequence(vec![der_oid(CONTENT_TYPE_OID), der_ctx(0, payload)])
}
fn encode_roa_payload_set(v: Vrp) -> Vec<u8> {
let (afi, addr_bytes) = match v.addr {
IpAddr::V4(ip) => ([0u8, 1u8].to_vec(), ip.octets().to_vec()),
IpAddr::V6(ip) => ([0u8, 2u8].to_vec(), ip.octets().to_vec()),
};
let bit_string = prefix_to_bit_string(&addr_bytes, v.prefix_len);
let roa_ip = der_sequence(vec![
der_bit_string(0, bit_string),
der_integer(u32::from(v.max_len)),
]);
let family = der_sequence(vec![der_octet_string(afi), der_sequence(vec![roa_ip])]);
der_sequence(vec![der_integer(v.asn), der_sequence(vec![family])])
}
fn encode_aspa_payload_set(v: Vap) -> Vec<u8> {
let providers = v.providers.into_iter().map(der_integer).collect::<Vec<_>>();
der_sequence(vec![der_integer(v.customer_asn), der_sequence(providers)])
}
fn prefix_to_bit_string(addr: &[u8], prefix_len: u8) -> Vec<u8> {
let byte_len = usize::from(prefix_len).div_ceil(8);
let mut out = addr[..byte_len].to_vec();
let rem = prefix_len % 8;
if rem != 0 {
let mask = 0xFFu8 << (8 - rem);
let last = out.len() - 1;
out[last] &= mask;
}
out
}
fn der_sequence(items: Vec<Vec<u8>>) -> Vec<u8> {
let content = items.concat();
der_tlv(0x30, content)
}
fn der_integer(v: u32) -> Vec<u8> {
if v == 0 {
return der_tlv(0x02, vec![0]);
}
let mut bytes = v.to_be_bytes().to_vec();
while bytes.len() > 1 && bytes[0] == 0 {
bytes.remove(0);
}
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0);
}
der_tlv(0x02, bytes)
}
fn der_oid(oid: &str) -> Vec<u8> {
let parts = oid
.split('.')
.map(|s| s.parse::<u32>().unwrap())
.collect::<Vec<_>>();
assert!(parts.len() >= 2);
let mut out = Vec::new();
out.push((parts[0] * 40 + parts[1]) as u8);
for &part in &parts[2..] {
out.extend(base128(part));
}
der_tlv(0x06, out)
}
fn base128(mut n: u32) -> Vec<u8> {
let mut buf = vec![(n & 0x7F) as u8];
n >>= 7;
while n > 0 {
buf.push(((n & 0x7F) as u8) | 0x80);
n >>= 7;
}
buf.reverse();
buf
}
fn der_octet_string(bytes: Vec<u8>) -> Vec<u8> {
der_tlv(0x04, bytes)
}
fn der_null() -> Vec<u8> {
der_tlv(0x05, Vec::new())
}
fn der_bit_string(unused_bits: u8, bytes: Vec<u8>) -> Vec<u8> {
let mut content = Vec::with_capacity(1 + bytes.len());
content.push(unused_bits);
content.extend(bytes);
der_tlv(0x03, content)
}
fn der_generalized_time(v: &str) -> Vec<u8> {
der_tlv(0x18, v.as_bytes().to_vec())
}
fn der_ctx(tag_no: u8, encoded_inner_der: Vec<u8>) -> Vec<u8> {
der_tlv(0xA0 + tag_no, encoded_inner_der)
}
fn der_tlv(tag: u8, content: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(2 + content.len());
out.push(tag);
out.extend(der_len(content.len()));
out.extend(content);
out
}
fn der_len(len: usize) -> Vec<u8> {
if len < 128 {
return vec![len as u8];
}
let mut bytes = Vec::new();
let mut n = len;
while n > 0 {
bytes.push((n & 0xFF) as u8);
n >>= 8;
}
bytes.reverse();
let mut out = vec![0x80 | (bytes.len() as u8)];
out.extend(bytes);
out
}

View File

@@ -94,7 +94,7 @@ async fn main() -> io::Result<()> {
Ok(Some(line)) => {
match handle_console_command(
&line,
&mut writer,
Some(&mut writer),
&mut state,
).await {
Ok(should_quit) => {
@@ -169,7 +169,47 @@ async fn main() -> io::Result<()> {
let delay = state.reconnect_delay_secs();
state.current_session_id = None;
println!("[reconnect] transport disconnected, retry after {}s", delay);
tokio::time::sleep(Duration::from_secs(delay)).await;
let reconnect_sleep = tokio::time::sleep(Duration::from_secs(delay));
tokio::pin!(reconnect_sleep);
let mut reconnect_now = false;
loop {
tokio::select! {
_ = &mut reconnect_sleep => break,
line = stdin_lines.next_line() => {
match line {
Ok(Some(line)) => {
match handle_console_command(&line, None, &mut state).await {
Ok(should_quit) => {
if should_quit {
println!("quit requested, closing client.");
return Ok(());
}
}
Ok(false) => {
if state.take_reconnect_now() {
reconnect_now = true;
break;
}
}
Err(err) => return Err(err),
}
}
Ok(None) => {
println!("stdin closed, continue reconnect loop.");
}
Err(err) => {
eprintln!("read stdin failed: {}", err);
}
}
}
}
}
if reconnect_now {
println!("[reconnect] user requested immediate reconnect");
}
}
}
}
@@ -179,6 +219,16 @@ async fn send_resume_query(
state: &mut ClientState,
mode: &QueryMode,
) -> io::Result<()> {
if state.force_reset_on_reconnect {
state.force_reset_on_reconnect = false;
state.session_id = None;
state.serial = None;
state.current_session_id = None;
send_reset_query(writer, state.version).await?;
println!("reconnected, send Reset Query (forced)");
return Ok(());
}
match (state.session_id, state.serial) {
(Some(session_id), Some(serial)) => {
println!(
@@ -380,7 +430,7 @@ async fn handle_poll_tick(writer: &mut ClientWriter, state: &mut ClientState) ->
async fn handle_console_command(
line: &str,
writer: &mut ClientWriter,
mut writer: Option<&mut ClientWriter>,
state: &mut ClientState,
) -> io::Result<bool> {
let line = line.trim();
@@ -400,10 +450,35 @@ async fn handle_console_command(
print_state(state);
}
["version"] => {
println!("current RTR version: {}", state.version);
}
["version", version] => {
let version = match version.parse::<u8>() {
Ok(v) => v,
Err(err) => {
println!("invalid version: {}", err);
return Ok(false);
}
};
state.version = version;
println!("updated RTR version to {}", state.version);
}
["reset"] => {
println!("manual command: send Reset Query");
send_reset_query(writer, state.version).await?;
state.schedule_next_poll();
if let Some(writer) = writer.as_mut() {
send_reset_query(writer, state.version).await?;
state.schedule_next_poll();
} else {
state.force_reset_on_reconnect = true;
state.request_reconnect_now();
state.session_id = None;
state.serial = None;
state.current_session_id = None;
println!("not connected, queued Reset Query for next reconnect");
}
}
["serial"] => match (state.session_id, state.serial) {
@@ -412,8 +487,12 @@ async fn handle_console_command(
"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();
if let Some(writer) = writer.as_mut() {
send_serial_query(writer, state.version, session_id, serial).await?;
state.schedule_next_poll();
} else {
println!("not connected, will send Serial Query on reconnect");
}
}
_ => {
println!(
@@ -445,8 +524,13 @@ async fn handle_console_command(
);
state.session_id = Some(session_id);
state.serial = Some(serial);
send_serial_query(writer, state.version, session_id, serial).await?;
state.schedule_next_poll();
if let Some(writer) = writer.as_mut() {
send_serial_query(writer, state.version, session_id, serial).await?;
state.schedule_next_poll();
} else {
state.force_reset_on_reconnect = false;
println!("not connected, queued Serial Query for next reconnect");
}
}
["timeout"] => {
@@ -533,6 +617,8 @@ fn print_help() {
println!("available commands:");
println!(" help show this help");
println!(" state print current client state");
println!(" version show current RTR version");
println!(" version <n> update RTR version");
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");
@@ -583,6 +669,8 @@ struct ClientState {
default_poll_secs: u64,
next_poll_deadline: Instant,
poll_paused: bool,
force_reset_on_reconnect: bool,
reconnect_now: bool,
}
impl ClientState {
@@ -606,6 +694,8 @@ impl ClientState {
default_poll_secs,
next_poll_deadline: Instant::now() + Duration::from_secs(default_poll_secs),
poll_paused: false,
force_reset_on_reconnect: false,
reconnect_now: false,
}
}
@@ -667,6 +757,19 @@ impl ClientState {
self.default_poll_secs
}
}
fn request_reconnect_now(&mut self) {
self.reconnect_now = true;
}
fn take_reconnect_now(&mut self) -> bool {
if self.reconnect_now {
self.reconnect_now = false;
true
} else {
false
}
}
}
#[derive(Debug)]
@@ -767,12 +870,7 @@ impl Config {
.transpose()?
.unwrap_or(1);
if version > 2 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("unsupported RTR version {}, expected 0..=2", version),
));
}
// Allow any version here; server will validate and respond.
let mode = match positional.next().as_deref() {
None | Some("reset") => QueryMode::Reset,