257 lines
7.4 KiB
Rust
257 lines
7.4 KiB
Rust
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<Tester> {
|
|
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<String> {
|
|
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::<Address>()?,
|
|
))
|
|
.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<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ServerConfig {
|
|
domains: Vec<Name>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct MailConfig {
|
|
server: String,
|
|
port: u16,
|
|
username: String,
|
|
password: String,
|
|
from: (String, String),
|
|
to: String,
|
|
}
|