//! NATS connection layer. //! //! Connection parameters follow the production-proven Vigilance profile: //! infinite reconnects with capped exponential backoff, 30s pings to detect //! zombie TCP in ~60s, and a deep client-side send queue so telemetry buffers //! through broker outages instead of erroring. use anyhow::{Context, Result}; use std::time::Duration; use crate::config::Settings; pub async fn connect(cfg: &Settings) -> Result { let (url, force_tls) = normalize_url(&cfg.nats_url); let mut opts = async_nats::ConnectOptions::new() .name("corrosion-host-agent") .retry_on_initial_connect() .max_reconnects(None) .ping_interval(Duration::from_secs(30)) .client_capacity(8192) .reconnect_delay_callback(|attempts| { Duration::from_millis(std::cmp::min(attempts as u64 * 100, 8_000)) }) .event_callback(|event| async move { match event { async_nats::Event::Disconnected => tracing::warn!("nats disconnected"), async_nats::Event::Connected => tracing::info!("nats connected"), other => tracing::debug!("nats event: {other}"), } }); if force_tls { opts = opts.require_tls(true); } // Per-license auth: the broker maps user=license_id, password=derived // token to permissions scoped to corrosion.{license_id}.>. Falls back to // token-only or anonymous so the agent still works against a broker that // hasn't enforced auth yet (transition period). if let Some(password) = &cfg.nats_password { let user = cfg.nats_user.clone().unwrap_or_else(|| cfg.license_id.clone()); opts = opts.user_and_password(user, password.clone()); } else if let Some(token) = &cfg.nats_token { opts = opts.token(token.clone()); } let client = opts .connect(&url) .await .with_context(|| format!("connecting to NATS at {url}"))?; Ok(client) } /// Accept `tls://` / `nats+tls://` URL schemes by translating to `nats://` + /// an explicit TLS requirement. fn normalize_url(raw: &str) -> (String, bool) { if let Some(rest) = raw.strip_prefix("tls://") { (format!("nats://{rest}"), true) } else if let Some(rest) = raw.strip_prefix("nats+tls://") { (format!("nats://{rest}"), true) } else { (raw.to_string(), false) } }