check_ip/src/main.rs

269 lines
7.7 KiB
Rust

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<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);
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<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,
}