rpki/src/rtr/config.rs
2026-07-09 17:14:21 +08:00

616 lines
22 KiB
Rust

use std::env;
use std::net::SocketAddr;
use std::time::Duration;
use anyhow::{Result, anyhow};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use tracing::{info, warn};
use crate::rtr::payload::Timing;
use crate::rtr::server::RtrServiceConfig;
use crate::rtr::server::ssh::SshAuthMode;
#[derive(Debug, Clone)]
pub struct AppConfig {
pub enable_tls: bool,
pub enable_ssh: bool,
pub tcp_addr: SocketAddr,
pub tls_addr: SocketAddr,
pub ssh_addr: SocketAddr,
pub db_path: String,
pub ccr_dir: String,
pub slurm_dir: Option<String>,
pub tls_cert_path: String,
pub tls_key_path: String,
pub tls_client_ca_path: String,
pub ssh_host_key_path: String,
pub ssh_authorized_keys_path: String,
pub ssh_username: String,
pub ssh_subsystem_name: String,
pub ssh_auth_mode: SshAuthMode,
pub ssh_password: Option<String>,
pub max_delta: u8,
pub prune_delta_by_snapshot_size: bool,
pub source_refresh_interval: Duration,
pub report_dir: String,
pub runtime_report_interval: Duration,
pub report_history_limit: usize,
pub timezone: Tz,
pub timing: Timing,
pub admin_addr: Option<SocketAddr>,
pub admin_token: Option<String>,
pub service_config: RtrServiceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeConfig {
pub max_delta: u8,
pub prune_delta_by_snapshot_size: bool,
pub source_refresh_interval_seconds: u64,
pub runtime_report_interval_seconds: u64,
pub report_history_limit: usize,
pub timezone: String,
pub timing: RuntimeTimingConfig,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub struct RuntimeTimingConfig {
pub refresh: u32,
pub retry: u32,
pub expire: u32,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct RuntimeConfigPatch {
pub max_delta: Option<u8>,
pub prune_delta_by_snapshot_size: Option<bool>,
pub source_refresh_interval_seconds: Option<u64>,
pub runtime_report_interval_seconds: Option<u64>,
pub report_history_limit: Option<usize>,
pub timezone: Option<String>,
pub timing: Option<RuntimeTimingConfigPatch>,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub struct RuntimeTimingConfigPatch {
pub refresh: Option<u32>,
pub retry: Option<u32>,
pub expire: Option<u32>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
enable_tls: false,
enable_ssh: false,
tcp_addr: "0.0.0.0:323".parse().expect("invalid default tcp_addr"),
tls_addr: "0.0.0.0:324".parse().expect("invalid default tls_addr"),
ssh_addr: "0.0.0.0:22".parse().expect("invalid default ssh_addr"),
db_path: "./rtr-db".to_string(),
ccr_dir: "./data".to_string(),
slurm_dir: None,
tls_cert_path: "./certs/tls/server-dns.crt".to_string(),
tls_key_path: "./certs/tls/server-dns.key".to_string(),
tls_client_ca_path: "./certs/tls/client-ca.crt".to_string(),
ssh_host_key_path: "./certs/ssh/ssh_host_rsa_key".to_string(),
ssh_authorized_keys_path: "./certs/ssh/rtr-authorized_keys".to_string(),
ssh_username: "rpki-rtr".to_string(),
ssh_subsystem_name: "rpki-rtr".to_string(),
ssh_auth_mode: SshAuthMode::Key,
ssh_password: None,
max_delta: 100,
prune_delta_by_snapshot_size: false,
source_refresh_interval: Duration::from_secs(300),
report_dir: "./report".to_string(),
runtime_report_interval: Duration::from_secs(300),
report_history_limit: 10,
timezone: default_timezone(),
timing: Timing::default(),
admin_addr: None,
admin_token: None,
service_config: RtrServiceConfig {
max_connections: 512,
max_concurrent_handshakes: 128,
notify_queue_size: 1024,
tcp_keepalive: Some(Duration::from_secs(60)),
warn_insecure_tcp: true,
require_tls_server_dns_name_san: false,
enforce_tls_client_san_ip_match: true,
},
}
}
}
impl AppConfig {
pub fn from_env() -> Result<Self> {
let mut config = Self::default();
if let Some(value) = env_var("RPKI_RTR_ENABLE_TLS")? {
config.enable_tls = parse_bool(&value, "RPKI_RTR_ENABLE_TLS")?;
}
if let Some(value) = env_var("RPKI_RTR_ENABLE_SSH")? {
config.enable_ssh = parse_bool(&value, "RPKI_RTR_ENABLE_SSH")?;
}
if let Some(value) = env_var("RPKI_RTR_TCP_ADDR")? {
config.tcp_addr = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_TCP_ADDR '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_TLS_ADDR")? {
config.tls_addr = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_TLS_ADDR '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_SSH_ADDR")? {
config.ssh_addr = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_SSH_ADDR '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_SSH_PORT")? {
let port: u16 = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_SSH_PORT '{}': {}", value, err))?;
config.ssh_addr.set_port(port);
}
if let Some(value) = env_var("RPKI_RTR_DB_PATH")? {
config.db_path = value;
}
if let Some(value) = env_var("RPKI_RTR_CCR_DIR")? {
config.ccr_dir = value;
}
if let Some(value) = env_var("RPKI_RTR_SLURM_DIR")? {
let value = value.trim();
config.slurm_dir = if value.is_empty() {
None
} else {
Some(value.to_string())
};
}
if let Some(value) = env_var("RPKI_RTR_TLS_CERT_PATH")? {
config.tls_cert_path = value;
}
if let Some(value) = env_var("RPKI_RTR_TLS_KEY_PATH")? {
config.tls_key_path = value;
}
if let Some(value) = env_var("RPKI_RTR_TLS_CLIENT_CA_PATH")? {
config.tls_client_ca_path = value;
}
if let Some(value) = env_var("RPKI_RTR_SSH_HOST_KEY_PATH")? {
config.ssh_host_key_path = value;
}
if let Some(value) = env_var("RPKI_RTR_SSH_AUTHORIZED_KEYS_PATH")? {
config.ssh_authorized_keys_path = value;
}
if let Some(value) = env_var("RPKI_RTR_SSH_USERNAME")? {
config.ssh_username = value;
}
if let Some(value) = env_var("RPKI_RTR_SSH_SUBSYSTEM_NAME")? {
config.ssh_subsystem_name = value;
}
if let Some(value) = env_var("RPKI_RTR_SSH_AUTH_MODE")? {
config.ssh_auth_mode = SshAuthMode::parse(&value).ok_or_else(|| {
anyhow!(
"invalid RPKI_RTR_SSH_AUTH_MODE '{}': expected key|password|both",
value
)
})?;
}
if let Some(value) = env_var("RPKI_RTR_SSH_PASSWORD")? {
let value = value.trim().to_string();
config.ssh_password = if value.is_empty() { None } else { Some(value) };
}
if let Some(value) = env_var("RPKI_RTR_MAX_DELTA")? {
let parsed: u8 = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_DELTA '{}': {}", value, err))?;
if parsed == 0 {
return Err(anyhow!(
"invalid RPKI_RTR_MAX_DELTA '{}': must be >= 1",
value
));
}
config.max_delta = parsed;
}
if let Some(value) = env_var("RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")? {
config.prune_delta_by_snapshot_size =
parse_bool(&value, "RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")?;
}
if let Some(value) = env_var("RPKI_RTR_REPORT_DIR")? {
config.report_dir = value;
}
if let Some(value) = env_var("RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS")? {
let secs = parse_positive_u64(&value, "RPKI_RTR_RUNTIME_REPORT_INTERVAL_SECS")?;
config.runtime_report_interval = Duration::from_secs(secs);
}
if let Some(value) = env_var("RPKI_RTR_REPORT_HISTORY_LIMIT")? {
config.report_history_limit =
parse_positive_usize(&value, "RPKI_RTR_REPORT_HISTORY_LIMIT")?;
}
if let Some(value) = env_var("RPKI_RTR_TIMEZONE")? {
config.timezone = parse_timezone(&value, "RPKI_RTR_TIMEZONE")?;
}
if let Some(value) = env_var("RPKI_RTR_ADMIN_ADDR")? {
let value = value.trim();
if !value.is_empty() {
config.admin_addr =
Some(value.parse().map_err(|err| {
anyhow!("invalid RPKI_RTR_ADMIN_ADDR '{}': {}", value, err)
})?);
}
}
if let Some(value) = env_var("RPKI_RTR_ADMIN_TOKEN")? {
let value = value.trim().to_string();
config.admin_token = if value.is_empty() { None } else { Some(value) };
}
let source_refresh_interval_new = env_var("RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
let source_refresh_interval_legacy = env_var("RPKI_RTR_REFRESH_INTERVAL_SECS")?;
match (
source_refresh_interval_new.as_deref(),
source_refresh_interval_legacy.as_deref(),
) {
(Some(new_value), Some(_)) => {
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
config.source_refresh_interval = Duration::from_secs(secs);
warn!(
"both RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS and legacy RPKI_RTR_REFRESH_INTERVAL_SECS are set; using RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS"
);
}
(Some(new_value), None) => {
let secs = parse_positive_u64(new_value, "RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS")?;
config.source_refresh_interval = Duration::from_secs(secs);
}
(None, Some(legacy_value)) => {
let secs = parse_positive_u64(legacy_value, "RPKI_RTR_REFRESH_INTERVAL_SECS")?;
config.source_refresh_interval = Duration::from_secs(secs);
warn!(
"RPKI_RTR_REFRESH_INTERVAL_SECS is deprecated; use RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS"
);
}
(None, None) => {}
}
if let Some(value) = env_var("RPKI_RTR_TIMING_REFRESH_SECS")? {
config.timing.refresh = parse_positive_u32(&value, "RPKI_RTR_TIMING_REFRESH_SECS")?;
}
if let Some(value) = env_var("RPKI_RTR_TIMING_RETRY_SECS")? {
config.timing.retry = parse_positive_u32(&value, "RPKI_RTR_TIMING_RETRY_SECS")?;
}
if let Some(value) = env_var("RPKI_RTR_TIMING_EXPIRE_SECS")? {
config.timing.expire = parse_positive_u32(&value, "RPKI_RTR_TIMING_EXPIRE_SECS")?;
}
config
.timing
.validate()
.map_err(|err| anyhow!("invalid RTR timing configuration: {}", err))?;
if let Some(value) = env_var("RPKI_RTR_MAX_CONNECTIONS")? {
config.service_config.max_connections = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_CONNECTIONS '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_MAX_CONCURRENT_HANDSHAKES")? {
config.service_config.max_concurrent_handshakes = value.parse().map_err(|err| {
anyhow!(
"invalid RPKI_RTR_MAX_CONCURRENT_HANDSHAKES '{}': {}",
value,
err
)
})?;
}
if let Some(value) = env_var("RPKI_RTR_NOTIFY_QUEUE_SIZE")? {
config.service_config.notify_queue_size = value.parse().map_err(|err| {
anyhow!("invalid RPKI_RTR_NOTIFY_QUEUE_SIZE '{}': {}", value, err)
})?;
}
if let Some(value) = env_var("RPKI_RTR_TCP_KEEPALIVE_SECS")? {
let secs: u64 = value.parse().map_err(|err| {
anyhow!("invalid RPKI_RTR_TCP_KEEPALIVE_SECS '{}': {}", value, err)
})?;
config.service_config.tcp_keepalive = if secs == 0 {
None
} else {
Some(Duration::from_secs(secs))
};
}
if let Some(value) = env_var("RPKI_RTR_WARN_INSECURE_TCP")? {
config.service_config.warn_insecure_tcp =
parse_bool(&value, "RPKI_RTR_WARN_INSECURE_TCP")?;
}
if let Some(value) = env_var("RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN")? {
config.service_config.require_tls_server_dns_name_san =
parse_bool(&value, "RPKI_RTR_REQUIRE_TLS_SERVER_DNS_NAME_SAN")?;
}
if let Some(value) = env_var("RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH")? {
config.service_config.enforce_tls_client_san_ip_match =
parse_bool(&value, "RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH")?;
}
if config.service_config.max_connections == 0 {
return Err(anyhow!(
"invalid RPKI_RTR_MAX_CONNECTIONS '{}': must be >= 1",
config.service_config.max_connections
));
}
if config.service_config.max_concurrent_handshakes == 0 {
return Err(anyhow!(
"invalid RPKI_RTR_MAX_CONCURRENT_HANDSHAKES '{}': must be >= 1",
config.service_config.max_concurrent_handshakes
));
}
if config.service_config.max_concurrent_handshakes > config.service_config.max_connections {
return Err(anyhow!(
"invalid handshake/connection limits: RPKI_RTR_MAX_CONCURRENT_HANDSHAKES ({}) must be <= RPKI_RTR_MAX_CONNECTIONS ({})",
config.service_config.max_concurrent_handshakes,
config.service_config.max_connections
));
}
Ok(config)
}
pub fn runtime_config(&self) -> RuntimeConfig {
RuntimeConfig {
max_delta: self.max_delta,
prune_delta_by_snapshot_size: self.prune_delta_by_snapshot_size,
source_refresh_interval_seconds: self.source_refresh_interval.as_secs(),
runtime_report_interval_seconds: self.runtime_report_interval.as_secs(),
report_history_limit: self.report_history_limit,
timezone: format_timezone(self.timezone),
timing: RuntimeTimingConfig::from(self.timing),
}
}
}
impl RuntimeConfig {
pub fn apply_patch(&self, patch: RuntimeConfigPatch) -> Result<Self> {
let mut next = self.clone();
if let Some(max_delta) = patch.max_delta {
if max_delta == 0 {
return Err(anyhow!("invalid max_delta '{}': must be >= 1", max_delta));
}
next.max_delta = max_delta;
}
if let Some(value) = patch.prune_delta_by_snapshot_size {
next.prune_delta_by_snapshot_size = value;
}
if let Some(value) = patch.source_refresh_interval_seconds {
if value == 0 {
return Err(anyhow!(
"invalid source_refresh_interval_seconds '{}': must be >= 1",
value
));
}
next.source_refresh_interval_seconds = value;
}
if let Some(value) = patch.runtime_report_interval_seconds {
if value == 0 {
return Err(anyhow!(
"invalid runtime_report_interval_seconds '{}': must be >= 1",
value
));
}
next.runtime_report_interval_seconds = value;
}
if let Some(value) = patch.report_history_limit {
if value == 0 {
return Err(anyhow!(
"invalid report_history_limit '{}': must be >= 1",
value
));
}
next.report_history_limit = value;
}
if let Some(value) = patch.timezone {
next.timezone = format_timezone(parse_timezone(&value, "timezone")?);
}
if let Some(timing) = patch.timing {
let mut next_timing = Timing::from(next.timing);
if let Some(value) = timing.refresh {
next_timing.refresh = value;
}
if let Some(value) = timing.retry {
next_timing.retry = value;
}
if let Some(value) = timing.expire {
next_timing.expire = value;
}
next_timing
.validate()
.map_err(|err| anyhow!("invalid timing: {}", err))?;
next.timing = RuntimeTimingConfig::from(next_timing);
}
Ok(next)
}
pub fn timezone(&self) -> Result<Tz> {
parse_timezone(&self.timezone, "timezone")
}
pub fn timing(&self) -> Timing {
Timing::from(self.timing)
}
}
impl From<Timing> for RuntimeTimingConfig {
fn from(timing: Timing) -> Self {
Self {
refresh: timing.refresh,
retry: timing.retry,
expire: timing.expire,
}
}
}
impl From<RuntimeTimingConfig> for Timing {
fn from(timing: RuntimeTimingConfig) -> Self {
Self::new(timing.refresh, timing.retry, timing.expire)
}
}
pub fn log_startup_config(config: &AppConfig) {
info!("starting RTR service");
info!("db_path={}", config.db_path);
info!("tcp_addr={}", config.tcp_addr);
info!("tls_enabled={}", config.enable_tls);
info!("ssh_enabled={}", config.enable_ssh);
if config.enable_tls {
info!("tls_addr={}", config.tls_addr);
info!("tls_cert_path={}", config.tls_cert_path);
info!("tls_key_path={}", config.tls_key_path);
info!("tls_client_ca_path={}", config.tls_client_ca_path);
}
if config.enable_ssh {
info!("ssh_addr={}", config.ssh_addr);
info!("ssh_host_key_path={}", config.ssh_host_key_path);
info!(
"ssh_authorized_keys_path={}",
config.ssh_authorized_keys_path
);
info!("ssh_username={}", config.ssh_username);
info!("ssh_subsystem_name={}", config.ssh_subsystem_name);
info!("ssh_auth_mode={}", config.ssh_auth_mode.as_str());
info!("ssh_password_enabled={}", config.ssh_password.is_some());
}
info!("ccr_dir={}", config.ccr_dir);
info!(
"slurm_dir={}",
config.slurm_dir.as_deref().unwrap_or("disabled")
);
info!("max_delta={}", config.max_delta);
info!("strict_ccr_validation=true");
info!(
"source_refresh_interval_secs={}",
config.source_refresh_interval.as_secs()
);
info!("report_dir={}", config.report_dir);
info!(
"runtime_report_interval_secs={}",
config.runtime_report_interval.as_secs()
);
info!("report_history_limit={}", config.report_history_limit);
info!("timezone={}", format_timezone(config.timezone));
info!("rtr_timing_refresh_secs={}", config.timing.refresh);
info!("rtr_timing_retry_secs={}", config.timing.retry);
info!("rtr_timing_expire_secs={}", config.timing.expire);
info!(
"admin_addr={}",
config
.admin_addr
.map(|addr| addr.to_string())
.unwrap_or_else(|| "disabled".to_string())
);
info!("admin_token_enabled={}", config.admin_token.is_some());
info!("max_connections={}", config.service_config.max_connections);
info!(
"max_concurrent_handshakes={}",
config.service_config.max_concurrent_handshakes
);
info!(
"notify_queue_size={}",
config.service_config.notify_queue_size
);
info!(
"tcp_keepalive_secs={}",
config
.service_config
.tcp_keepalive
.map(|duration| duration.as_secs().to_string())
.unwrap_or_else(|| "disabled".to_string())
);
info!(
"warn_insecure_tcp={}",
config.service_config.warn_insecure_tcp
);
info!(
"require_tls_server_dns_name_san={}",
config.service_config.require_tls_server_dns_name_san
);
info!(
"enforce_tls_client_san_ip_match={}",
config.service_config.enforce_tls_client_san_ip_match
);
}
pub fn default_timezone() -> Tz {
chrono_tz::Asia::Shanghai
}
pub fn format_timezone(timezone: Tz) -> String {
timezone.name().to_string()
}
fn env_var(name: &str) -> Result<Option<String>> {
match env::var(name) {
Ok(value) => Ok(Some(value)),
Err(env::VarError::NotPresent) => Ok(None),
Err(err) => Err(anyhow!("failed to read {}: {}", name, err)),
}
}
fn parse_bool(value: &str, name: &str) -> Result<bool> {
match value.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "on" => Ok(true),
"0" | "false" | "no" | "off" => Ok(false),
_ => Err(anyhow!("invalid {} '{}': expected boolean", name, value)),
}
}
fn parse_positive_u64(value: &str, name: &str) -> Result<u64> {
let parsed = value
.parse::<u64>()
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
if parsed == 0 {
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
}
Ok(parsed)
}
fn parse_positive_usize(value: &str, name: &str) -> Result<usize> {
let parsed = value
.parse::<usize>()
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
if parsed == 0 {
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
}
Ok(parsed)
}
fn parse_positive_u32(value: &str, name: &str) -> Result<u32> {
let parsed = value
.parse::<u32>()
.map_err(|err| anyhow!("invalid {} '{}': {}", name, value, err))?;
if parsed == 0 {
return Err(anyhow!("invalid {} '{}': must be >= 1", name, value));
}
Ok(parsed)
}
pub fn parse_timezone(value: &str, name: &str) -> Result<Tz> {
let value = value.trim();
let normalized = match value.to_ascii_lowercase().as_str() {
"shanghai" | "beijing" | "peking" => "Asia/Shanghai",
"utc" | "z" => "UTC",
_ => value,
};
normalized.parse::<Tz>().map_err(|err| {
anyhow!(
"invalid {} '{}': expected IANA timezone like Asia/Shanghai, Europe/London, America/New_York, or UTC: {}",
name,
value,
err
)
})
}