diff --git a/data/20260324T000037Z-sng1.ccr b/data/20260324T000037Z-sng1.ccr deleted file mode 100644 index 128af6d..0000000 Binary files a/data/20260324T000037Z-sng1.ccr and /dev/null differ diff --git a/deploy/bird/docker-compose.yml b/deploy/bird/docker-compose.yml index ae8b3eb..cbf0a59 100644 --- a/deploy/bird/docker-compose.yml +++ b/deploy/bird/docker-compose.yml @@ -32,3 +32,4 @@ services: SHOW_ROA6: "1" volumes: - ./bird.conf:/config/bird.conf:ro + - ../../logs/bird:/app/logs diff --git a/deploy/bird/entrypoint.sh b/deploy/bird/entrypoint.sh index 4bfa720..b0b8045 100644 --- a/deploy/bird/entrypoint.sh +++ b/deploy/bird/entrypoint.sh @@ -27,6 +27,14 @@ SHOW_ROA6="${SHOW_ROA6:-1}" SSH_HOST_PUBKEY_PATH="${SSH_HOST_PUBKEY_PATH:-/config/ssh/ssh_host_rsa_key.pub}" SSH_KNOWN_HOSTS_PATH="${SSH_KNOWN_HOSTS_PATH:-/run/bird/known_hosts}" +LOG_DIR="${LOG_DIR:-/app/logs}" +LOG_NAME="${LOG_NAME:-${HOSTNAME:-bird-rpki-client}}" +STDOUT_LOG="${LOG_DIR}/${LOG_NAME}.stdout.log" +STDERR_LOG="${LOG_DIR}/${LOG_NAME}.stderr.log" + +mkdir -p "$LOG_DIR" +exec >>"$STDOUT_LOG" 2>>"$STDERR_LOG" + ensure_ssh_known_hosts() { if [ -s "$SSH_KNOWN_HOSTS_PATH" ]; then return diff --git a/deploy/server/docker-compose.ssh.yml b/deploy/server/docker-compose.ssh.yml index e047705..cb17b40 100644 --- a/deploy/server/docker-compose.ssh.yml +++ b/deploy/server/docker-compose.ssh.yml @@ -27,6 +27,7 @@ services: RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" + RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RUST_LOG: "info" volumes: - ../../data:/app/data:ro diff --git a/deploy/server/docker-compose.tcp.yml b/deploy/server/docker-compose.tcp.yml index 004fb00..05be793 100644 --- a/deploy/server/docker-compose.tcp.yml +++ b/deploy/server/docker-compose.tcp.yml @@ -18,8 +18,9 @@ services: RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_STRICT_CCR_VALIDATION: "false" - RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" + RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "60" RPKI_RTR_MAX_CONNECTIONS: "100000" + RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RUST_LOG: "info" volumes: - ../../data:/app/data:ro diff --git a/deploy/server/docker-compose.tls.yml b/deploy/server/docker-compose.tls.yml index ad75e50..59382a9 100644 --- a/deploy/server/docker-compose.tls.yml +++ b/deploy/server/docker-compose.tls.yml @@ -19,11 +19,13 @@ services: RPKI_RTR_TLS_CERT_PATH: "/app/certs/server-dns.crt" RPKI_RTR_TLS_KEY_PATH: "/app/certs/server-dns.key" RPKI_RTR_TLS_CLIENT_CA_PATH: "/app/certs/client-ca.crt" + RPKI_RTR_ENFORCE_TLS_CLIENT_SAN_IP_MATCH: "false" RPKI_RTR_DB_PATH: "/app/rtr-db" RPKI_RTR_CCR_DIR: "/app/data" RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" + RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RUST_LOG: "info" volumes: - ../../data:/app/data:ro diff --git a/deploy/server/docker-compose.yml b/deploy/server/docker-compose.yml index 46a47eb..bccb99f 100644 --- a/deploy/server/docker-compose.yml +++ b/deploy/server/docker-compose.yml @@ -22,6 +22,7 @@ services: RPKI_RTR_SLURM_DIR: "/app/slurm" RPKI_RTR_STRICT_CCR_VALIDATION: "false" RPKI_RTR_SOURCE_REFRESH_INTERVAL_SECS: "300" + RPKI_RTR_MAX_CONCURRENT_HANDSHAKES: "128" RUST_LOG: "info" # SSH mode example: # RPKI_RTR_ENABLE_SSH: "true" diff --git a/src/main.rs b/src/main.rs index 78507fa..49baddb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -73,10 +73,12 @@ impl Default for AppConfig { 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, }, } } @@ -218,6 +220,15 @@ impl AppConfig { .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) @@ -241,6 +252,29 @@ impl AppConfig { 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) } @@ -489,6 +523,10 @@ fn log_startup_config(config: &AppConfig) { info!("rtr_timing_retry_secs={}", config.timing.retry); info!("rtr_timing_expire_secs={}", config.timing.expire); 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 @@ -509,6 +547,10 @@ fn log_startup_config(config: &AppConfig) { "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 + ); } fn init_tracing() { diff --git a/src/rtr/loader.rs b/src/rtr/loader.rs index 06349a6..70807eb 100644 --- a/src/rtr/loader.rs +++ b/src/rtr/loader.rs @@ -239,8 +239,12 @@ fn validate_aspa(customer_asn: u32, provider_asns: &[u32]) -> Result<()> { return Err(anyhow!("provider list must not be empty")); } - if provider_asns.iter().any(|asn| *asn == 0) { - return Err(anyhow!("provider list must not contain AS0")); + if provider_asns.iter().any(|asn| *asn == 0) + && !(provider_asns.len() == 1 && provider_asns[0] == 0) + { + return Err(anyhow!( + "provider list containing AS0 must be exactly [0]" + )); } Ok(()) diff --git a/src/rtr/payload.rs b/src/rtr/payload.rs index a5aebbb..b673b5e 100644 --- a/src/rtr/payload.rs +++ b/src/rtr/payload.rs @@ -163,10 +163,12 @@ impl Aspa { )); } - if self.provider_asns.iter().any(|asn| asn.into_u32() == 0) { + if self.provider_asns.iter().any(|asn| asn.into_u32() == 0) + && !(self.provider_asns.len() == 1 && self.provider_asns[0].into_u32() == 0) + { return Err(io::Error::new( io::ErrorKind::InvalidData, - "ASPA provider list must not contain AS0", + "ASPA provider list containing AS0 must be exactly [0]", )); } diff --git a/src/rtr/pdu.rs b/src/rtr/pdu.rs index 0dd742b..e8ef094 100644 --- a/src/rtr/pdu.rs +++ b/src/rtr/pdu.rs @@ -1317,10 +1317,12 @@ impl Aspa { "ASPA withdrawal must not contain provider ASNs", )); } - if self.provider_asns.iter().any(|asn| *asn == 0) { + if self.provider_asns.iter().any(|asn| *asn == 0) + && !(self.provider_asns.len() == 1 && self.provider_asns[0] == 0) + { return Err(io::Error::new( io::ErrorKind::InvalidData, - "ASPA provider list must not contain AS0", + "ASPA provider list containing AS0 must be exactly [0]", )); } if self.provider_asns.windows(2).any(|pair| pair[0] >= pair[1]) { diff --git a/src/rtr/server/config.rs b/src/rtr/server/config.rs index 6920eb3..8404f56 100644 --- a/src/rtr/server/config.rs +++ b/src/rtr/server/config.rs @@ -3,20 +3,24 @@ use std::time::Duration; #[derive(Debug, Clone)] pub struct RtrServiceConfig { pub max_connections: usize, + pub max_concurrent_handshakes: usize, pub notify_queue_size: usize, pub tcp_keepalive: Option, pub warn_insecure_tcp: bool, pub require_tls_server_dns_name_san: bool, + pub enforce_tls_client_san_ip_match: bool, } impl Default for RtrServiceConfig { fn default() -> Self { Self { max_connections: 1024, + 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, } } } diff --git a/src/rtr/server/connection.rs b/src/rtr/server/connection.rs index 7dbed16..9cdd115 100644 --- a/src/rtr/server/connection.rs +++ b/src/rtr/server/connection.rs @@ -60,8 +60,10 @@ pub async fn handle_tls_connection( stream: TcpStream, peer_addr: SocketAddr, acceptor: TlsAcceptor, + enforce_client_san_ip_match: bool, notify_rx: broadcast::Receiver<()>, shutdown_rx: watch::Receiver, + handshake_permit: Option, ) -> Result<()> { info!("RTR TLS handshake started for {}", peer_addr); let tls_stream = acceptor @@ -69,13 +71,24 @@ pub async fn handle_tls_connection( .await .with_context(|| format!("TLS handshake failed for {}", peer_addr))?; info!("RTR TLS handshake completed for {}", peer_addr); - verify_peer_certificate_ip(&tls_stream, peer_addr.ip()).with_context(|| { - format!( - "TLS client certificate SAN IP validation failed for {}", - peer_addr - ) - })?; - info!("RTR TLS client certificate validated for {}", peer_addr); + match verify_peer_certificate_ip(&tls_stream, peer_addr.ip()) { + Ok(()) => info!("RTR TLS client certificate SAN IP validated for {}", peer_addr), + Err(err) => { + if enforce_client_san_ip_match { + return Err(err).with_context(|| { + format!( + "TLS client certificate SAN IP validation failed for {}", + peer_addr + ) + }); + } + warn!( + "RTR TLS client certificate SAN IP validation failed but allowed by configuration for {}: {}", + peer_addr, err + ); + } + } + drop(handshake_permit); let session = RtrSession::new(cache, tls_stream, notify_rx, shutdown_rx); session.run().await?; diff --git a/src/rtr/server/listener.rs b/src/rtr/server/listener.rs index b1f470d..ccfb808 100644 --- a/src/rtr/server/listener.rs +++ b/src/rtr/server/listener.rs @@ -11,7 +11,7 @@ use russh::server::{self, Msg, Session}; use russh::{Channel, ChannelId, Disconnect}; use socket2::{SockRef, TcpKeepalive}; use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::{Semaphore, broadcast, watch}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, watch}; use tokio_rustls::TlsAcceptor; use tracing::{debug, info, warn}; @@ -30,6 +30,9 @@ type TransportFuture = Pin> + Send>>; pub trait TransportAcceptor: Clone + Send + Sync + 'static { fn name(&self) -> &'static str; + fn requires_handshake_limit(&self) -> bool { + false + } fn handle_connection( &self, @@ -38,6 +41,7 @@ pub trait TransportAcceptor: Clone + Send + Sync + 'static { peer_addr: SocketAddr, notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, + handshake_permit: Option, ) -> TransportFuture; } @@ -56,6 +60,7 @@ impl TransportAcceptor for TcpTransport { peer_addr: SocketAddr, notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, + _handshake_permit: Option, ) -> TransportFuture { Box::pin(async move { handle_tcp_connection( @@ -73,12 +78,16 @@ impl TransportAcceptor for TcpTransport { #[derive(Clone)] struct TlsTransport { acceptor: TlsAcceptor, + enforce_client_san_ip_match: bool, } impl TransportAcceptor for TlsTransport { fn name(&self) -> &'static str { "TLS" } + fn requires_handshake_limit(&self) -> bool { + true + } fn handle_connection( &self, @@ -87,16 +96,20 @@ impl TransportAcceptor for TlsTransport { peer_addr: SocketAddr, notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, + handshake_permit: Option, ) -> TransportFuture { let acceptor = self.acceptor.clone(); + let enforce_client_san_ip_match = self.enforce_client_san_ip_match; Box::pin(async move { handle_tls_connection( cache, stream, peer_addr, acceptor, + enforce_client_san_ip_match, notify_tx.subscribe(), shutdown_tx.subscribe(), + handshake_permit, ) .await }) @@ -120,6 +133,7 @@ impl TransportAcceptor for SshTransport { peer_addr: SocketAddr, notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, + _handshake_permit: Option, ) -> TransportFuture { let runtime = self.runtime.clone(); Box::pin(async move { @@ -171,6 +185,7 @@ pub struct RtrServer { notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, connection_limiter: Arc, + handshake_limiter: Arc, active_connections: Arc, config: RtrServiceConfig, } @@ -182,6 +197,7 @@ impl RtrServer { notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, connection_limiter: Arc, + handshake_limiter: Arc, active_connections: Arc, config: RtrServiceConfig, ) -> Self { @@ -191,6 +207,7 @@ impl RtrServer { notify_tx, shutdown_tx, connection_limiter, + handshake_limiter, active_connections, config, } @@ -231,6 +248,7 @@ impl RtrServer { pub async fn run_tls(self, tls_config: Arc) -> Result<()> { let transport = TlsTransport { acceptor: TlsAcceptor::from(tls_config), + enforce_client_san_ip_match: self.config.enforce_tls_client_san_ip_match, }; self.run_with_transport(transport).await } @@ -317,6 +335,24 @@ impl RtrServer { continue; } }; + let handshake_permit = if transport.requires_handshake_limit() { + match self.handshake_limiter.clone().try_acquire_owned() { + Ok(permit) => Some(permit), + Err(_) => { + warn!( + "RTR {} connection rejected for {}: max concurrent handshakes reached ({})", + transport.name(), + peer_addr, + self.config.max_concurrent_handshakes + ); + drop(stream); + drop(permit); + continue; + } + } + } else { + None + }; let cache = self.cache.clone(); let notify_tx = self.notify_tx.clone(); @@ -341,7 +377,14 @@ impl RtrServer { ); if let Err(err) = transport_instance - .handle_connection(cache, stream, peer_addr, notify_tx, shutdown_tx) + .handle_connection( + cache, + stream, + peer_addr, + notify_tx, + shutdown_tx, + handshake_permit, + ) .await { let active_after_close = guard.active_count().saturating_sub(1); diff --git a/src/rtr/server/service.rs b/src/rtr/server/service.rs index b4f248d..53a6a85 100644 --- a/src/rtr/server/service.rs +++ b/src/rtr/server/service.rs @@ -20,6 +20,7 @@ pub struct RtrService { notify_tx: broadcast::Sender<()>, shutdown_tx: watch::Sender, connection_limiter: Arc, + handshake_limiter: Arc, active_connections: Arc, config: RtrServiceConfig, } @@ -38,6 +39,7 @@ impl RtrService { notify_tx, shutdown_tx, connection_limiter: Arc::new(Semaphore::new(config.max_connections)), + handshake_limiter: Arc::new(Semaphore::new(config.max_concurrent_handshakes)), active_connections: Arc::new(AtomicUsize::new(0)), config, } @@ -70,6 +72,7 @@ impl RtrService { self.notify_tx.clone(), self.shutdown_tx.clone(), self.connection_limiter.clone(), + self.handshake_limiter.clone(), self.active_connections.clone(), self.config.clone(), ) @@ -82,6 +85,7 @@ impl RtrService { self.notify_tx.clone(), self.shutdown_tx.clone(), self.connection_limiter.clone(), + self.handshake_limiter.clone(), self.active_connections.clone(), self.config.clone(), ) @@ -94,6 +98,7 @@ impl RtrService { self.notify_tx.clone(), self.shutdown_tx.clone(), self.connection_limiter.clone(), + self.handshake_limiter.clone(), self.active_connections.clone(), self.config.clone(), ) diff --git a/tests/test_session.rs b/tests/test_session.rs index 368a762..bfc0c37 100644 --- a/tests/test_session.rs +++ b/tests/test_session.rs @@ -157,7 +157,9 @@ async fn start_tls_session_server_with_cert( return; }; - let _ = handle_tls_connection(cache, stream, peer_addr, acceptor, notify_rx, shutdown_rx).await; + let _ = + handle_tls_connection(cache, stream, peer_addr, acceptor, notify_rx, shutdown_rx, None) + .await; }); (addr, shutdown_tx, handle)