extern crate anyhow; extern crate clokwerk; extern crate lettre; extern crate lettre_email; extern crate log; extern crate native_tls; extern crate serde; extern crate simplelog; extern crate systemstat; extern crate tokio; extern crate toml; extern crate trust_dns_resolver; use self::native_tls::{Protocol, TlsConnector}; use anyhow::{anyhow, bail, Result}; use clokwerk::{Scheduler, TimeUnits}; use lettre::smtp::authentication::Credentials; use lettre::{ClientSecurity, ClientTlsParameters, SmtpClient, Transport}; use lettre_email::EmailBuilder; 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_str("%d/%m/%Y %H:%M:%S") .set_time_to_local(true) .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"); } // Lauch 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); if let Some(false) = self.config.test { let email = EmailBuilder::new() .to(self.config.mail.from.clone()) .from(self.config.mail.to.clone()) .subject(subject) .text(body) .build()?; // Create transport let creds = Credentials::new( self.config.mail.username.clone(), self.config.mail.password.clone(), ); let mut tls_builder = TlsConnector::builder(); tls_builder.min_protocol_version(Some(Protocol::Sslv3)); let tls_parameters = ClientTlsParameters::new( self.config.mail.server.clone(), tls_builder.build().unwrap(), ); let mut mailer = SmtpClient::new( (self.config.mail.server.clone(), self.config.mail.port), ClientSecurity::Required(tls_parameters), )? .credentials(creds) .transport(); mailer.send(email.into())?; } else { println!("subject: {} - body: {}", subject, body); }; 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, }