add dockerfile
This commit is contained in:
parent
23cdad095d
commit
c1d3112a45
Binary file not shown.
@ -1,7 +1,17 @@
|
||||
FROM rust:1.86-bookworm AS builder
|
||||
FROM rust:1.89-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
cmake \
|
||||
pkg-config \
|
||||
clang \
|
||||
libclang-dev \
|
||||
libssl-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
|
||||
24
deploy/Dockerfile.client
Normal file
24
deploy/Dockerfile.client
Normal file
@ -0,0 +1,24 @@
|
||||
FROM rust:1.89-bookworm AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends clang libclang-dev pkg-config \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN cargo build --release --bin rtr_debug_client
|
||||
|
||||
FROM debian:bookworm-slim AS runtime
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /build/target/release/rtr_debug_client /usr/local/bin/rtr_debug_client
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/rtr_debug_client"]
|
||||
10
deploy/docker-compose.client.yml
Normal file
10
deploy/docker-compose.client.yml
Normal file
@ -0,0 +1,10 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-debug-client:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deploy/Dockerfile.client
|
||||
image: rpki-rtr-debug-client:latest
|
||||
stdin_open: true
|
||||
tty: true
|
||||
32
deploy/docker-compose.clients.yml
Normal file
32
deploy/docker-compose.clients.yml
Normal file
@ -0,0 +1,32 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
rtr-client-1:
|
||||
image: rpki-rtr-debug-client:latest
|
||||
network_mode: host
|
||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||
restart: unless-stopped
|
||||
|
||||
rtr-client-2:
|
||||
image: rpki-rtr-debug-client:latest
|
||||
network_mode: host
|
||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||
restart: unless-stopped
|
||||
|
||||
rtr-client-3:
|
||||
image: rpki-rtr-debug-client:latest
|
||||
network_mode: host
|
||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||
restart: unless-stopped
|
||||
|
||||
rtr-client-4:
|
||||
image: rpki-rtr-debug-client:latest
|
||||
network_mode: host
|
||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||
restart: unless-stopped
|
||||
|
||||
rtr-client-5:
|
||||
image: rpki-rtr-debug-client:latest
|
||||
network_mode: host
|
||||
command: ["127.0.0.1:323", "2", "reset", "--keep-after-error", "--summary-only"]
|
||||
restart: unless-stopped
|
||||
@ -1,4 +1,5 @@
|
||||
use std::env;
|
||||
use std::future::pending;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
@ -27,6 +28,12 @@ impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
||||
type DynStream = Box<dyn AsyncStream>;
|
||||
type ClientWriter = WriteHalf<DynStream>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OutputMode {
|
||||
Verbose,
|
||||
SummaryOnly,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
let config = Config::from_args()?;
|
||||
@ -41,6 +48,7 @@ async fn main() -> io::Result<()> {
|
||||
config.default_poll_secs
|
||||
);
|
||||
println!("keep-after-error: {}", config.keep_after_error);
|
||||
println!("output : {}", config.output_mode.describe());
|
||||
match &config.mode {
|
||||
QueryMode::Reset => {
|
||||
println!("mode : reset");
|
||||
@ -59,10 +67,12 @@ async fn main() -> io::Result<()> {
|
||||
config.read_timeout_secs,
|
||||
config.default_poll_secs,
|
||||
config.keep_after_error,
|
||||
config.output_mode,
|
||||
);
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut stdin_lines = BufReader::new(stdin).lines();
|
||||
let mut stdin_closed = false;
|
||||
|
||||
loop {
|
||||
let stream = loop {
|
||||
@ -89,7 +99,13 @@ async fn main() -> io::Result<()> {
|
||||
tokio::pin!(poll_sleep);
|
||||
|
||||
tokio::select! {
|
||||
line = stdin_lines.next_line() => {
|
||||
line = async {
|
||||
if stdin_closed {
|
||||
pending::<io::Result<Option<String>>>().await
|
||||
} else {
|
||||
stdin_lines.next_line().await
|
||||
}
|
||||
} => {
|
||||
match line {
|
||||
Ok(Some(line)) => {
|
||||
match handle_console_command(
|
||||
@ -111,7 +127,8 @@ async fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("stdin closed, continue network loop.");
|
||||
stdin_closed = true;
|
||||
println!("stdin closed, disable console input.");
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("read stdin failed: {}", err);
|
||||
@ -136,8 +153,11 @@ async fn main() -> io::Result<()> {
|
||||
) => {
|
||||
match read_result {
|
||||
Ok(Ok(pdu)) => {
|
||||
print_raw_pdu(&pdu.header, &pdu.body);
|
||||
print_pdu(&pdu.header, &pdu.body);
|
||||
state.observe_pdu(&pdu.header);
|
||||
if should_print_pdu(state.output_mode, &pdu.header) {
|
||||
print_raw_pdu(&pdu.header, &pdu.body);
|
||||
print_pdu(&pdu.header, &pdu.body);
|
||||
}
|
||||
match handle_incoming_pdu(&mut writer, &mut state, &pdu.header, &pdu.body).await {
|
||||
Ok(()) => {}
|
||||
Err(err) if should_reconnect(&err) => {
|
||||
@ -177,7 +197,13 @@ async fn main() -> io::Result<()> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut reconnect_sleep => break,
|
||||
line = stdin_lines.next_line() => {
|
||||
line = async {
|
||||
if stdin_closed {
|
||||
pending::<io::Result<Option<String>>>().await
|
||||
} else {
|
||||
stdin_lines.next_line().await
|
||||
}
|
||||
} => {
|
||||
match line {
|
||||
Ok(Some(line)) => {
|
||||
match handle_console_command(&line, None, &mut state).await {
|
||||
@ -197,7 +223,8 @@ async fn main() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("stdin closed, continue reconnect loop.");
|
||||
stdin_closed = true;
|
||||
println!("stdin closed, disable console input.");
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("read stdin failed: {}", err);
|
||||
@ -326,6 +353,16 @@ async fn handle_incoming_pdu(
|
||||
);
|
||||
}
|
||||
|
||||
if state.output_mode == OutputMode::SummaryOnly
|
||||
&& state.skipped_payload_pdu_count_in_round > 0
|
||||
{
|
||||
println!(
|
||||
"summary : skipped {} payload PDUs in this response",
|
||||
state.skipped_payload_pdu_count_in_round
|
||||
);
|
||||
state.skipped_payload_pdu_count_in_round = 0;
|
||||
}
|
||||
|
||||
println!("received EndOfData, keep connection open.");
|
||||
println!();
|
||||
}
|
||||
@ -370,6 +407,7 @@ async fn handle_incoming_pdu(
|
||||
state.current_session_id = None;
|
||||
state.serial = None;
|
||||
state.last_error_code = None;
|
||||
state.skipped_payload_pdu_count_in_round = 0;
|
||||
send_reset_query(writer, state.version).await?;
|
||||
state.schedule_next_poll();
|
||||
println!();
|
||||
@ -604,6 +642,21 @@ async fn handle_console_command(
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
["output"] => {
|
||||
println!("current output mode: {}", state.output_mode.describe());
|
||||
println!("skipped payload PDUs: {}", state.skipped_payload_pdu_count);
|
||||
}
|
||||
|
||||
["output", "verbose"] => {
|
||||
state.output_mode = OutputMode::Verbose;
|
||||
println!("updated output mode to verbose");
|
||||
}
|
||||
|
||||
["output", "summary"] => {
|
||||
state.output_mode = OutputMode::SummaryOnly;
|
||||
println!("updated output mode to summary");
|
||||
}
|
||||
|
||||
_ => {
|
||||
println!("unknown command: {}", line);
|
||||
print_help();
|
||||
@ -629,6 +682,9 @@ fn print_help() {
|
||||
println!(" poll pause pause auto polling");
|
||||
println!(" poll resume resume auto polling");
|
||||
println!(" keep-after-error show current keep-after-error setting");
|
||||
println!(" output show current output mode");
|
||||
println!(" output verbose print all PDUs");
|
||||
println!(" output summary suppress payload PDU details");
|
||||
println!(" quit exit client");
|
||||
println!();
|
||||
}
|
||||
@ -648,6 +704,8 @@ fn print_state(state: &ClientState) {
|
||||
println!(" poll_source : {}", state.poll_interval_source());
|
||||
println!(" last_error_code : {:?}", state.last_error_code);
|
||||
println!(" keep_after_error : {}", state.keep_after_error);
|
||||
println!(" output_mode : {}", state.output_mode.describe());
|
||||
println!(" skipped_payloads : {}", state.skipped_payload_pdu_count);
|
||||
println!(" poll_paused : {}", state.poll_paused);
|
||||
println!();
|
||||
}
|
||||
@ -664,6 +722,9 @@ struct ClientState {
|
||||
expire: Option<u32>,
|
||||
last_error_code: Option<u16>,
|
||||
keep_after_error: bool,
|
||||
output_mode: OutputMode,
|
||||
skipped_payload_pdu_count: u64,
|
||||
skipped_payload_pdu_count_in_round: u64,
|
||||
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
@ -679,6 +740,7 @@ impl ClientState {
|
||||
read_timeout_secs: u64,
|
||||
default_poll_secs: u64,
|
||||
keep_after_error: bool,
|
||||
output_mode: OutputMode,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
@ -690,6 +752,9 @@ impl ClientState {
|
||||
expire: None,
|
||||
last_error_code: None,
|
||||
keep_after_error,
|
||||
output_mode,
|
||||
skipped_payload_pdu_count: 0,
|
||||
skipped_payload_pdu_count_in_round: 0,
|
||||
read_timeout_secs,
|
||||
default_poll_secs,
|
||||
next_poll_deadline: Instant::now() + Duration::from_secs(default_poll_secs),
|
||||
@ -770,6 +835,14 @@ impl ClientState {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_pdu(&mut self, header: &PduHeader) {
|
||||
if self.output_mode == OutputMode::SummaryOnly && is_payload_pdu(header) {
|
||||
self.skipped_payload_pdu_count = self.skipped_payload_pdu_count.saturating_add(1);
|
||||
self.skipped_payload_pdu_count_in_round =
|
||||
self.skipped_payload_pdu_count_in_round.saturating_add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -781,6 +854,7 @@ struct Config {
|
||||
default_poll_secs: u64,
|
||||
transport: TransportConfig,
|
||||
keep_after_error: bool,
|
||||
output_mode: OutputMode,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@ -791,6 +865,7 @@ impl Config {
|
||||
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 mut output_mode = OutputMode::Verbose;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
@ -841,6 +916,9 @@ impl Config {
|
||||
"--keep-after-error" => {
|
||||
keep_after_error = true;
|
||||
}
|
||||
"--summary-only" => {
|
||||
output_mode = OutputMode::SummaryOnly;
|
||||
}
|
||||
_ if arg.starts_with("--") => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
@ -924,10 +1002,34 @@ impl Config {
|
||||
default_poll_secs,
|
||||
transport,
|
||||
keep_after_error,
|
||||
output_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl OutputMode {
|
||||
fn describe(self) -> &'static str {
|
||||
match self {
|
||||
Self::Verbose => "verbose",
|
||||
Self::SummaryOnly => "summary",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_payload_pdu(header: &PduHeader) -> bool {
|
||||
matches!(
|
||||
header.pdu_type(),
|
||||
PduType::Ipv4Prefix | PduType::Ipv6Prefix | PduType::RouterKey | PduType::Aspa
|
||||
)
|
||||
}
|
||||
|
||||
fn should_print_pdu(output_mode: OutputMode, header: &PduHeader) -> bool {
|
||||
match output_mode {
|
||||
OutputMode::Verbose => true,
|
||||
OutputMode::SummaryOnly => !is_payload_pdu(header),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TransportConfig {
|
||||
Tcp,
|
||||
|
||||
@ -668,13 +668,14 @@ where
|
||||
(
|
||||
cache.session_id_for_version(version),
|
||||
cache.serial_for_version(version),
|
||||
)
|
||||
)
|
||||
};
|
||||
|
||||
self.write_cache_response(current_session).await?;
|
||||
self.write_end_of_data(current_session, current_serial)
|
||||
.await?;
|
||||
info!(
|
||||
"RTR session replied EndOfData (up-to-date) to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
||||
"RTR session replied CacheResponse+EndOfData (up-to-date) to Serial Query: client_session_id={}, client_serial={}, response_session_id={}, response_serial={}, {}",
|
||||
client_session,
|
||||
client_serial,
|
||||
current_session,
|
||||
|
||||
@ -268,6 +268,12 @@ pub struct AspaAssertion {
|
||||
|
||||
impl AspaAssertion {
|
||||
fn validate(&self) -> Result<(), SlurmError> {
|
||||
if self.provider_asns.contains(&self.customer_asn) {
|
||||
return Err(SlurmError::Invalid(
|
||||
"aspaAssertion providerAsns must not contain customerAsn".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let providers = self
|
||||
.provider_asns
|
||||
.iter()
|
||||
|
||||
@ -289,8 +289,9 @@ impl<'de> Deserialize<'de> for AspaAssertion {
|
||||
}
|
||||
|
||||
fn decode_ski(input: &str) -> Result<Ski, SlurmError> {
|
||||
let bytes = hex::decode(input)
|
||||
.map_err(|err| SlurmError::Invalid(format!("invalid SKI '{}': {}", input, err)))?;
|
||||
let bytes = STANDARD_NO_PAD
|
||||
.decode(input)
|
||||
.map_err(|err| SlurmError::Invalid(format!("invalid SKI base64 '{}': {}", input, err)))?;
|
||||
if bytes.len() != 20 {
|
||||
return Err(SlurmError::Invalid(format!(
|
||||
"SKI must be exactly 20 bytes, got {}",
|
||||
|
||||
@ -20,9 +20,13 @@ fn sample_ski() -> [u8; 20] {
|
||||
]
|
||||
}
|
||||
|
||||
fn sample_ski_b64() -> String {
|
||||
STANDARD_NO_PAD.encode(sample_ski())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_rfc8416_v1_slurm() {
|
||||
let ski_hex = hex::encode(sample_ski());
|
||||
let ski_b64 = sample_ski_b64();
|
||||
let router_public_key = STANDARD_NO_PAD.encode(sample_spki());
|
||||
let json = format!(
|
||||
r#"{{
|
||||
@ -32,7 +36,7 @@ fn parses_rfc8416_v1_slurm() {
|
||||
{{ "prefix": "192.0.2.0/24", "asn": 64496, "comment": "drop roa" }}
|
||||
],
|
||||
"bgpsecFilters": [
|
||||
{{ "asn": 64497, "SKI": "{ski_hex}" }}
|
||||
{{ "asn": 64497, "SKI": "{ski_b64}" }}
|
||||
]
|
||||
}},
|
||||
"locallyAddedAssertions": {{
|
||||
@ -40,7 +44,7 @@ fn parses_rfc8416_v1_slurm() {
|
||||
{{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }}
|
||||
],
|
||||
"bgpsecAssertions": [
|
||||
{{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{router_public_key}" }}
|
||||
{{ "asn": 64501, "SKI": "{ski_b64}", "routerPublicKey": "{router_public_key}" }}
|
||||
]
|
||||
}}
|
||||
}}"#
|
||||
@ -145,7 +149,7 @@ fn applies_filters_before_assertions_and_excludes_duplicates() {
|
||||
let ski = Ski::from_bytes(sample_ski());
|
||||
let spki = sample_spki();
|
||||
let spki_b64 = STANDARD_NO_PAD.encode(&spki);
|
||||
let ski_hex = hex::encode(sample_ski());
|
||||
let ski_b64 = sample_ski_b64();
|
||||
let json = format!(
|
||||
r#"{{
|
||||
"slurmVersion": 2,
|
||||
@ -154,7 +158,7 @@ fn applies_filters_before_assertions_and_excludes_duplicates() {
|
||||
{{ "prefix": "192.0.2.0/24", "asn": 64496 }}
|
||||
],
|
||||
"bgpsecFilters": [
|
||||
{{ "SKI": "{ski_hex}" }}
|
||||
{{ "SKI": "{ski_b64}" }}
|
||||
],
|
||||
"aspaFilters": [
|
||||
{{ "customerAsn": 64496 }}
|
||||
@ -166,7 +170,7 @@ fn applies_filters_before_assertions_and_excludes_duplicates() {
|
||||
{{ "prefix": "198.51.100.0/24", "asn": 64500, "maxPrefixLength": 24 }}
|
||||
],
|
||||
"bgpsecAssertions": [
|
||||
{{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{spki_b64}" }}
|
||||
{{ "asn": 64501, "SKI": "{ski_b64}", "routerPublicKey": "{spki_b64}" }}
|
||||
],
|
||||
"aspaAssertions": [
|
||||
{{ "customerAsn": 64510, "providerAsns": [64511, 64512] }}
|
||||
@ -216,6 +220,55 @@ fn applies_filters_before_assertions_and_excludes_duplicates() {
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_hex_encoded_ski_and_aspa_customer_in_providers() {
|
||||
let ski_hex = hex::encode(sample_ski());
|
||||
let router_public_key = STANDARD_NO_PAD.encode(sample_spki());
|
||||
let invalid_ski = format!(
|
||||
r#"{{
|
||||
"slurmVersion": 1,
|
||||
"validationOutputFilters": {{
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": [
|
||||
{{ "SKI": "{ski_hex}" }}
|
||||
]
|
||||
}},
|
||||
"locallyAddedAssertions": {{
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": [
|
||||
{{ "asn": 64501, "SKI": "{ski_hex}", "routerPublicKey": "{router_public_key}" }}
|
||||
]
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
let ski_err = SlurmFile::from_slice(invalid_ski.as_bytes()).unwrap_err();
|
||||
let ski_err_text = ski_err.to_string();
|
||||
assert!(
|
||||
ski_err_text.contains("invalid SKI base64")
|
||||
|| ski_err_text.contains("SKI must be exactly 20 bytes")
|
||||
);
|
||||
|
||||
let invalid_aspa = r#"{
|
||||
"slurmVersion": 2,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": [],
|
||||
"aspaFilters": []
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": [],
|
||||
"aspaAssertions": [
|
||||
{ "customerAsn": 64500, "providerAsns": [64500, 64501] }
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
let aspa_err = SlurmFile::from_slice(invalid_aspa.as_bytes()).unwrap_err();
|
||||
assert!(aspa_err
|
||||
.to_string()
|
||||
.contains("providerAsns must not contain customerAsn"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_multiple_slurm_files_without_conflict() {
|
||||
let a = r#"{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user