use std::sync::Arc; use std::time::{Duration, Instant}; use anyhow::Result; use onis_common::config::OnisConfig; use tracing_subscriber::EnvFilter; mod api; mod checker; mod client; use checker::Checker; use client::AppviewClient; #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .json() .init(); let metrics_handle = onis_common::metrics::init() .map_err(|e| anyhow::anyhow!("failed to install metrics recorder: {e}"))?; metrics::describe_counter!( "verification_checks_total", "Total verification checks" ); metrics::describe_histogram!( "verification_check_duration_seconds", "Verification check duration" ); metrics::describe_gauge!( "verification_zones_verified", "Number of verified zones" ); metrics::describe_gauge!( "verification_zones_unverified", "Number of unverified zones" ); let config = OnisConfig::load()?; let cfg = &config.verify; let nameservers = cfg.parse_nameservers() .map_err(|e| anyhow::anyhow!("invalid nameserver address: {e}"))?; tracing::info!( appview_url = %cfg.appview_url, port = cfg.port, check_interval = cfg.check_interval, recheck_interval = cfg.recheck_interval, expected_ns = ?cfg.expected_ns, "onis-verify starting" ); let client = AppviewClient::new(cfg.appview_url.clone()); let checker = Checker::new(cfg.expected_ns.clone(), nameservers, cfg.dns_port); let state = Arc::new(api::VerifyState { checker, client, metrics_handle, }); let api_bind = format!("{}:{}", cfg.bind, cfg.port); let api_state = state.clone(); let api_handle = tokio::spawn(async move { let app = api::router(api_state); let listener = tokio::net::TcpListener::bind(&api_bind).await.unwrap(); tracing::info!("verify API listening on {api_bind}"); axum::serve(listener, app).await.unwrap(); }); let check_interval = Duration::from_secs(cfg.check_interval); let recheck_interval = cfg.recheck_interval; let scheduler_state = state.clone(); let scheduler_handle = tokio::spawn(async move { run_scheduler(scheduler_state, check_interval, recheck_interval).await; }); tokio::select! { _ = api_handle => tracing::error!("verify API exited unexpectedly"), _ = scheduler_handle => tracing::error!("scheduler exited unexpectedly"), _ = tokio::signal::ctrl_c() => tracing::info!("shutting down"), } Ok(()) } async fn run_scheduler(state: Arc, interval: Duration, recheck_interval: i64) { loop { tokio::time::sleep(interval).await; let now = chrono::Utc::now().timestamp(); let stale_threshold = now - recheck_interval; let zones = match state.client.get_stale_zones(stale_threshold).await { Ok(z) => z, Err(e) => { tracing::error!(error = %e, "failed to fetch stale zones"); continue; } }; if zones.is_empty() { tracing::debug!("no stale zones to check"); continue; } tracing::info!(count = zones.len(), "checking stale zones"); let mut verified_count: u64 = 0; let mut unverified_count: u64 = 0; for zone_entry in &zones { let check_start = Instant::now(); let result = match state.checker.check_zone(&zone_entry.zone, &zone_entry.did).await { Ok(r) => r, Err(e) => { tracing::warn!( zone = %zone_entry.zone, did = %zone_entry.did, error = %e, "verification check failed" ); metrics::counter!("verification_checks_total", "result" => "error") .increment(1); metrics::histogram!("verification_check_duration_seconds") .record(check_start.elapsed().as_secs_f64()); continue; } }; let result_label = if result.verified { "verified" } else { "unverified" }; metrics::counter!("verification_checks_total", "result" => result_label).increment(1); metrics::histogram!("verification_check_duration_seconds") .record(check_start.elapsed().as_secs_f64()); if result.verified { verified_count += 1; } else { unverified_count += 1; } if let Err(e) = state .client .set_verification(&zone_entry.zone, &zone_entry.did, result.verified) .await { tracing::error!( zone = %zone_entry.zone, did = %zone_entry.did, error = %e, "failed to update verification status" ); continue; } if result.verified != zone_entry.verified { tracing::info!( zone = %zone_entry.zone, did = %zone_entry.did, old = zone_entry.verified, new = result.verified, ns = ?result.ns_records, "verification status changed" ); } else { tracing::debug!( zone = %zone_entry.zone, verified = result.verified, "verification status unchanged" ); } } metrics::gauge!("verification_zones_verified").set(verified_count as f64); metrics::gauge!("verification_zones_unverified").set(unverified_count as f64); } }