use anyhow::{anyhow, bail, Result}; use clokwerk::{Job, Scheduler, TimeUnits}; use lettre::message::Mailbox; use lettre::transport::smtp::authentication::Credentials; use lettre::{Address, Message, SmtpTransport, Transport}; use log::{error, info, warn}; use query_external_ip::Consensus; use serde::Deserialize; use simplelog::{ConfigBuilder, LevelFilter, WriteLogger}; use std::fs::{self, OpenOptions}; use std::net::{Ipv4Addr, Ipv6Addr}; use std::string::String; use std::{thread, time::Duration}; use systemstat::{Platform, System}; use tokio::runtime::{Builder, Runtime}; use trust_dns_resolver::{config::*, Name, Resolver}; fn main() -> Result<()> { // Init log let logfile = OpenOptions::new() .append(true) .create(true) .open("log.txt") .unwrap(); WriteLogger::init( LevelFilter::Info, ConfigBuilder::new() .set_time_format_custom(simplelog::format_description!( "[day]/[month]/[year] [hour]:[minute]:[second]" )) .set_time_offset_to_local() .unwrap() .build(), logfile, ) .unwrap(); // Init tester info!("Init tester"); let tester = match Tester::new() { Err(e) => { error!("Error {}", e); bail!("Cannot init tester"); } Ok(tester) => tester, }; // Test Ethernet if let Err(e) = wait_eth_connection() { error!("Error {}", e); return Err(e); } // Initial check if let Err(e) = tester.check() { error!("Error {}", e); bail!("Cannot get initial IP"); } // Launch scheduler if let Some(false) = tester.config.test { // Init scheduler info!("Init scheduler"); let mut scheduler = Scheduler::new(); scheduler.every(1.day()).at("06:00").run(move || { if let Err(e) = tester.check() { error!("Error {}", e); } }); // Run loop { scheduler.run_pending(); thread::sleep(Duration::from_secs(60)); } } Ok(()) } fn wait_eth_connection() -> Result<()> { let mut wait_s = 0; loop { let sys = System::new(); for netif in sys.networks()?.values() { if netif.addrs.len() > 0 { for addr in netif.addrs.iter() { if let systemstat::IpAddr::V4(ipv4_addr) = addr.addr { if ipv4_addr != Ipv4Addr::LOCALHOST { return Ok(()); } } } } } if wait_s >= 60 { break; } warn!("Cannot get ethernet connection, wait 5s and retry"); wait_s += 5; std::thread::sleep(std::time::Duration::from_secs(5)); } bail!("Cannot get ethernet connection") } struct Tester { config: Config, dns_resolver: Resolver, async_runtime: Runtime, } impl Tester { fn new() -> Result { let mut resolver_opt = ResolverOpts::default(); resolver_opt.ip_strategy = LookupIpStrategy::Ipv4AndIpv6; Ok(Tester { config: toml::from_str(&fs::read_to_string("config.toml")?)?, dns_resolver: Resolver::from_system_conf()?, async_runtime: Builder::new_multi_thread().enable_all().build().unwrap(), }) } fn check(&self) -> Result<()> { info!("Check"); match self.get_ip() { Ok((ipv4, ipv6)) => { let msg = self.test_domains(ipv4, ipv6)?; if !msg.is_empty() { self.send_mail("IP check error".to_owned(), msg)?; } } Err(err) => { let msg = format!("Cannot get IP ({})", err); error!("{}", msg); self.send_mail("Error on getting IP".to_owned(), msg)?; } } Ok(()) } fn get_ip(&self) -> Result<(Ipv4Addr, Ipv6Addr)> { let cons = self.async_runtime.block_on(Consensus::get())?; let ipv4 = cons .v4() .ok_or(anyhow!("Cannot get current IPv4 address"))?; let ipv6 = cons .v6() .ok_or(anyhow!("Cannot get current IPv6 address"))?; info!("Current IPv4 is {} and IPv6 {}", ipv4, ipv6); Ok((ipv4, ipv6)) } fn test_domains(&self, ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Result { macro_rules! msg { ($full_msg:expr, $($arg:tt)*) => {{ let msg = format!($($arg)*); info!("{}", msg); $full_msg.push_str(&msg); $full_msg.push('\n'); }} } let mut full_msg = String::new(); for domain in self.config.server.domains.iter() { info!("Test domain {}", domain); if let Ok(dns_ipv4_lookup) = self.dns_resolver.ipv4_lookup(domain.clone()) { let dns_ipv4 = dns_ipv4_lookup.iter().next().unwrap(); if *dns_ipv4 != ipv4 { msg!( full_msg, "Wrong IPv4 for {} (DNS: {}, current: {})", domain, dns_ipv4, ipv4 ); } } else { msg!(full_msg, "Cannot get IPv4 address for {}", domain); } if let Ok(dns_ipv6_lookup) = self.dns_resolver.ipv6_lookup(domain.clone()) { let dns_ipv6 = dns_ipv6_lookup.iter().next().unwrap(); if *dns_ipv6 != ipv6 { msg!( full_msg, "Wrong IPv6 for {} (DNS: {}, current: {})", domain, dns_ipv6, ipv6 ); } } else { msg!(full_msg, "Cannot get IPv6 address for {}", domain); } } Ok(full_msg) } fn send_mail(&self, subject: String, body: String) -> Result<()> { info!("Send mail {}", subject); let email = Message::builder() .from(Mailbox::new( Some(self.config.mail.from.1.clone()), self.config.mail.from.0.parse::
()?, )) .to(self.config.mail.to.parse()?) .subject(subject) .body(body)?; // Create transport let creds = Credentials::new( self.config.mail.username.clone(), self.config.mail.password.clone(), ); // Open a remote connection let mailer = if let Some(true) = self.config.test { SmtpTransport::builder_dangerous(&self.config.mail.server) .port(self.config.mail.port) .build() } else { SmtpTransport::relay(&self.config.mail.server) .unwrap() .credentials(creds) .port(self.config.mail.port) .build() }; mailer.send(&email)?; Ok(()) } } #[derive(Debug, Deserialize)] struct Config { mail: MailConfig, server: ServerConfig, test: Option, } #[derive(Debug, Deserialize)] struct ServerConfig { domains: Vec, } #[derive(Debug, Deserialize)] struct MailConfig { server: String, port: u16, username: String, password: String, from: (String, String), to: String, }