diff --git a/Cargo.lock b/Cargo.lock index 4d6ac2f..d29837c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -156,6 +171,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -204,6 +233,7 @@ name = "dynip-cloudflare" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "dirs", "env_logger", "futures", @@ -559,6 +589,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -769,6 +822,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -1637,6 +1699,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index a68f3a2..43a2f2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.86" +chrono = "0.4.38" dirs = "5.0.1" env_logger = "0.11.3" futures = "0.3.30" diff --git a/src/cloudflare.rs b/src/cloudflare.rs index 2be3e40..5b1b45a 100644 --- a/src/cloudflare.rs +++ b/src/cloudflare.rs @@ -5,14 +5,13 @@ use serde::{self, Deserialize, Serialize}; use std::{ collections::HashMap, net::{IpAddr, Ipv4Addr}, - sync::Arc, }; use crate::get_current_public_ipv4; pub struct CloudflareClient { client: Client, - domains: Vec>, + domains: Vec>, current_ip: Ipv4Addr, api_key: Box, zone_id: Box, @@ -36,7 +35,7 @@ struct DnsRecord { } impl CloudflareClient { - pub async fn new(api_key: Box, zone_id: Box, domains: Vec>) -> Result { + pub async fn new(api_key: Box, zone_id: Box, domains: Vec>) -> Result { let force_ipv4 = IpAddr::from([0, 0, 0, 0]); let client = reqwest::ClientBuilder::new() .local_address(force_ipv4) @@ -44,13 +43,15 @@ impl CloudflareClient { let current_ip = get_current_public_ipv4(&client).await?; - Ok(Self { + let this = Self { client, domains, current_ip, api_key, zone_id, - }) + }; + this.update_dns_records(current_ip).await?; + Ok(this) } pub async fn check(&mut self) -> Result<()> { @@ -58,6 +59,10 @@ impl CloudflareClient { if new_ip == self.current_ip { return Ok(()); } + info!( + "Ip has changed from '{}' -> '{}'", + &self.current_ip, &new_ip + ); self.update_dns_records(new_ip).await?; self.current_ip = new_ip; diff --git a/src/config.rs b/src/config.rs index ba08f24..bf4f9df 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use anyhow; use dirs; use log::warn; -use serde::{Deserialize, Serialize}; +use serde::{self, Deserialize, Serialize}; use std::{env, path::PathBuf}; use tokio::{fs, io::AsyncReadExt}; @@ -12,6 +12,7 @@ pub struct Config { pub zone_id: Box, pub api_key: Box, pub domains: Vec>, + pub max_errors_in_row: Option, } pub async fn get_config_path() -> Result> { diff --git a/src/lib.rs b/src/lib.rs index e335333..e9a9b05 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,11 @@ mod cloudflare; mod config; mod public_ip; +pub mod utils; + +pub use cloudflare::CloudflareClient; pub use config::{get_config_path, read_config, Config}; pub use public_ip::get_current_public_ipv4; pub const PROGRAM_NAME: &'static str = "dynip-cloudflare"; +pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10; diff --git a/src/main.rs b/src/main.rs index 0477ded..9668048 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use env_logger; use futures::stream::StreamExt; use log::{debug, error, info, log_enabled, Level}; use netlink_packet_core::NetlinkPayload; @@ -6,7 +5,7 @@ use netlink_packet_route::RouteNetlinkMessage as RtnlMessage; use netlink_sys::{AsyncSocket, SocketAddr}; use rtnetlink::new_connection; -use dynip_cloudflare::{get_config_path, read_config, Config}; +use dynip_cloudflare::{utils, CloudflareClient, MAX_ERORS_IN_ROW_DEFAULT}; const RTNLGRP_LINK: u32 = 1; const RTNLGRP_IPV4_IFADDR: u32 = 5; @@ -24,32 +23,22 @@ const fn nl_mgrp(group: u32) -> u32 { #[tokio::main] async fn main() { - env_logger::init(); - let config_path = match get_config_path().await { - Ok(cp) => cp, - Err(tried_paths) => { - let extra: String = if !tried_paths.is_empty() { - let joined = tried_paths - .iter() - .filter_map(|path| path.to_str()) - .collect::>() - .join(", "); - format!(", tried the paths: '{}'", &joined) - } else { - String::with_capacity(0) - }; - error!("Failed to find any config file{}", &extra); - return; - } - }; - let config = match read_config(&config_path).await { - Ok(config) => config, - Err(e) => { - error!("Failed to read and parse config file {:?}", &e); - return; - } + utils::init_logger(); + let config = if let Some(aux) = utils::get_config().await { + aux + } else { + return; }; + let mut cloudflare = + match CloudflareClient::new(config.api_key, config.zone_id, config.domains).await { + Ok(cloudflare) => cloudflare, + Err(e) => { + error!("Failed to initialize cloudflare client: {:?}", &e); + return; + } + }; + let (mut conn, mut _handle, mut messages) = new_connection().unwrap(); let groups = nl_mgrp(RTNLGRP_LINK) | nl_mgrp(RTNLGRP_IPV4_IFADDR); @@ -63,6 +52,9 @@ async fn main() { tokio::spawn(conn); info!("Listening for IPv4 address changes and interface connect/disconnect events..."); + let mut errs_counter: usize = 0; + let errs_max = config.max_errors_in_row.unwrap_or(MAX_ERORS_IN_ROW_DEFAULT); + while let Some((message, _)) = messages.next().await { match message.payload { NetlinkPayload::InnerMessage(RtnlMessage::NewAddress(msg)) => { @@ -70,6 +62,17 @@ async fn main() { debug!("New IPv4 address message: {:?}", msg); } else { info!("New IPv4 address"); + + if let Err(e) = cloudflare.check().await { + errs_counter += 1; + error!( + "Failed to check cloudflare ({}/{}): {:?}", + errs_counter, errs_max, &e + ); + if errs_counter >= errs_max { + return; + } + } } } NetlinkPayload::InnerMessage(RtnlMessage::DelAddress(msg)) => { @@ -84,6 +87,17 @@ async fn main() { debug!("New link message (interface connected): {:?}", link); } else { info!("New link (interface connected)"); + + if let Err(e) = cloudflare.check().await { + errs_counter += 1; + error!( + "Failed to check cloudflare ({}/{}): {:?}", + errs_counter, errs_max, &e + ); + if errs_counter >= errs_max { + return; + } + } } } NetlinkPayload::InnerMessage(RtnlMessage::DelLink(link)) => { diff --git a/src/public_ip.rs b/src/public_ip.rs index b3329fe..8d67a3f 100644 --- a/src/public_ip.rs +++ b/src/public_ip.rs @@ -1,44 +1,50 @@ - use anyhow::{anyhow, Context, Result}; - use log::error; - use reqwest::Client; - use std::{collections::HashMap, net::Ipv4Addr}; +use anyhow::{anyhow, Context, Result}; +use log::error; +use reqwest::Client; +use std::{collections::HashMap, net::Ipv4Addr}; - async fn ipify_org(client: &Client) -> Result { - let response = client - .get("https://api.ipify.org?format=json") - .send() - .await? - .error_for_status()? - .json::>() - .await?; +async fn ipify_org(client: &Client) -> Result { + let response = client + .get("https://api.ipify.org?format=json") + .send() + .await? + .error_for_status()? + .json::>() + .await?; - Ok(response - .get("ip") - .context("Field 'ip' wasn't found")? - .parse()?) - } - async fn ifconfig_me(client: &Client) -> Result { - Ok(client - .get("https://ifconfig.me") - .header("user-agent", "curl/8.8.0") - .send() - .await? - .error_for_status()? - .text() - .await? - .parse()?) - } - pub async fn get_current_public_ipv4(client: &Client) -> Result { - let e_ipify = match ipify_org(client).await { - Ok(ipv4) => return Ok(ipv4), - Err(e) => { - error!("Failed to get ip from ipify.org: {:?}", &e); - e - } - }; + Ok(response + .get("ip") + .context("Field 'ip' wasn't found")? + .parse()?) +} - ifconfig_me(client).await.map_err(|e_ifconfig| { - error!("Failed to get ip from ifconfig.me: {:?}", &e_ifconfig); - anyhow!("Failed to get ip from ipify.org with error '{:?}', and ifconfig.me with error {:?}", &e_ipify, &e_ifconfig) - }) - } \ No newline at end of file +async fn ifconfig_me(client: &Client) -> Result { + Ok(client + .get("https://ifconfig.me") + .header("user-agent", "curl/8.8.0") + .send() + .await? + .error_for_status()? + .text() + .await? + .parse()?) +} + +pub async fn get_current_public_ipv4(client: &Client) -> Result { + let e_ipify = match ipify_org(client).await { + Ok(ipv4) => return Ok(ipv4), + Err(e) => { + error!("Failed to get ip from ipify.org: {:?}", &e); + e + } + }; + + ifconfig_me(client).await.map_err(|e_ifconfig| { + error!("Failed to get ip from ifconfig.me: {:?}", &e_ifconfig); + anyhow!( + "Failed to get ip from ipify.org with error '{:?}', and ifconfig.me with error {:?}", + &e_ipify, + &e_ifconfig + ) + }) +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..f4967e7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,46 @@ +use env_logger::{Builder, Env}; +use log::{error, LevelFilter}; +use std::io::Write; + +use crate::{get_config_path, read_config, Config}; + +pub fn init_logger() { + Builder::from_env(Env::default().default_filter_or("info")) + .format(|buf, record| { + writeln!( + buf, + "{} [{}] - {}", + chrono::Local::now().format("%Y:%m:%d %H:%M:%S"), + record.level(), + record.args() + ) + }) + .filter(None, LevelFilter::Info) + .init(); +} + +pub async fn get_config() -> Option { + let config_path = match get_config_path().await { + Ok(path) => path, + Err(tried_paths) => { + let extra: String = if !tried_paths.is_empty() { + let joined = tried_paths + .iter() + .filter_map(|path| path.to_str()) + .collect::>() + .join(", "); + format!(", tried the paths: {}", &joined) + } else { + String::with_capacity(0) + }; + error!("Failed to find any config file{}", &extra); + return None; + } + }; + + let read_result = read_config(&config_path).await; + if let Err(e) = &read_result { + error!("Error occurred while getting config path: {:?}", e); + } + read_result.ok() +}