616 lines
22 KiB
Rust
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
|
|
)
|
|
})
|
|
}
|