//! Wiring only: the one place that knows the concrete DNS provider, prober and sinks. mod config; mod dns; mod event; mod monitor; mod notify; mod probe; use std::{env, io::IsTerminal, path::PathBuf, sync::Arc, time::Duration}; use anyhow::{Context, Result}; use clap::Parser; use tokio::{ signal::unix::{SignalKind, signal}, time::MissedTickBehavior, }; use tracing::info; use tracing_subscriber::EnvFilter; use dns::cloudflare; use monitor::GroupMonitor; use notify::{Hub, log::LogSink, telegram::TelegramSink}; use probe::IcmpProber; /// Keep Cloudflare names' A records pointed at whichever member hosts answer ping. #[derive(Parser)] struct Args { #[arg(long, default_value = "/etc/dns-monitor/config.toml")] config: PathBuf, /// Run a single check round and exit. #[arg(long)] once: bool, /// Log the DNS changes that would be made without making them; sends no notifications. #[arg(long)] dry_run: bool, /// Send a test message to the configured Telegram chat and exit. #[arg(long)] test_notify: bool, } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"))) .with_ansi(std::io::stdout().is_terminal()) .init(); let args = Args::parse(); let cfg = config::load(&args.config)?; // Built even for --dry-run, so deploy.sh catches a missing token before restarting. let telegram = match &cfg.telegram { Some(t) => { let token = env::var("TELEGRAM_BOT_TOKEN").context("config has [telegram] but TELEGRAM_BOT_TOKEN is not set")?; Some(TelegramSink::new(token, t.chat_id)?) } None => None, }; if args.test_notify { let telegram = telegram.context("config has no [telegram] section")?; telegram.send("✅ dns-monitor 测试消息").await?; info!("test message sent"); return Ok(()); } let cf = cloudflare::Client::new(cloudflare::Auth::from_env()?)?; let prober = Arc::new(IcmpProber::new(&cfg.probe)?); let mut monitors = Vec::new(); for g in cfg.groups { let zone = cloudflare::Zone::lookup(cf.clone(), &g.zone).await?; monitors.push(GroupMonitor::new(g, &cfg.probe, zone, prober.clone(), args.dry_run)); } let mut hub = Hub::new(); hub.add(LogSink); if let Some(telegram) = telegram.filter(|_| !args.dry_run) { hub.add(telegram); } if args.dry_run { info!("dry run: no DNS changes or notifications will be made"); } let mut ticker = tokio::time::interval(Duration::from_secs(cfg.interval_secs)); ticker.set_missed_tick_behavior(MissedTickBehavior::Delay); let mut sigterm = signal(SignalKind::terminate())?; loop { tokio::select! { _ = ticker.tick() => {} _ = sigterm.recv() => break, _ = tokio::signal::ctrl_c() => break, } for m in &mut monitors { hub.publish(m.run_round().await); } if args.once { break; } } hub.close(Duration::from_secs(10)).await; Ok(()) }