diff --git a/Cargo.lock b/Cargo.lock index 859dee6..338efab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efea76243612a2436fb4074ba0cf3ba9ea29efdeb72645d8fc63f116462be1de" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ "axum-core", "bytes", @@ -123,12 +123,12 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1b0df7cded837c40dacaa2e1c33aa17c84fc3356ae67b5645f1e83190753e" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "http-body-util", @@ -728,9 +728,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags", "errno", @@ -1086,9 +1086,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] name = "unicode-linebreak" @@ -1139,6 +1139,8 @@ dependencies = [ "insta", "miette", "ring", + "serde", + "serde_json", "tokio", "tower-http", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 0bafe90..89197a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ clap-verbosity-flag = { version = "3", default-features = false, features = [ http = "1" miette = { version = "7", features = ["fancy"] } ring = { version = "0.17", features = ["std"] } +serde = { version = "1.0.217", features = ["derive"] } +serde_json = "1.0.137" tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] } tower-http = { version = "0.6.2", features = ["validate-request"] } tracing = "0.1" diff --git a/flake.lock b/flake.lock index b9894b0..38a7fc1 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1737250794, - "narHash": "sha256-bdIPhvsAKyYQzqAIeay4kOxTHGwLGkhM+IlBIsmMYFI=", + "lastModified": 1737563566, + "narHash": "sha256-GLJvkOG29XCynQm8XWPyykMRqIhxKcBARVu7Ydrz02M=", "owner": "ipetkov", "repo": "crane", - "rev": "c5b7075f4a6d523fe8204618aa9754e56478c0e0", + "rev": "849376434956794ebc7a6b487d31aace395392ba", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1737062831, - "narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=", + "lastModified": 1737469691, + "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c", + "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab", "type": "github" }, "original": { diff --git a/src/main.rs b/src/main.rs index d2b9136..d909d3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use std::{ io::ErrorKind, - net::{IpAddr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, time::Duration, }; @@ -114,6 +114,48 @@ struct AppState<'a> { /// The file where the last IP is stored ip_file: &'a Path, + + /// Last recorded IPs + last_ips: std::sync::Arc>, +} + +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +struct SavedIPs { + #[serde(skip_serializing_if = "Option::is_none")] + ipv4: Option, + #[serde(skip_serializing_if = "Option::is_none")] + ipv6: Option, +} + +impl SavedIPs { + fn update(&mut self, ip: IpAddr) { + match ip { + IpAddr::V4(ipv4_addr) => self.ipv4 = Some(ipv4_addr), + IpAddr::V6(ipv6_addr) => self.ipv6 = Some(ipv6_addr), + } + } + + fn ips(&self) -> impl Iterator { + self.ipv4 + .map(IpAddr::V4) + .into_iter() + .chain(self.ipv6.map(IpAddr::V6)) + } + + fn from_str(data: &str) -> miette::Result { + match data.parse::() { + // Old format + Ok(IpAddr::V4(ipv4)) => Ok(Self { + ipv4: Some(ipv4), + ipv6: None, + }), + Ok(IpAddr::V6(ipv6)) => Ok(Self { + ipv4: None, + ipv6: Some(ipv6), + }), + Err(_) => serde_json::from_str(data).into_diagnostic(), + } + } } impl AppState<'static> { @@ -137,7 +179,7 @@ impl AppState<'static> { let ttl = Duration::from_secs(*ttl); // Use last registered IP address if available - let ip_file = data_dir.join("last-ip"); + let ip_file = Box::leak(data_dir.join("last-ip").into_boxed_path()); let state = AppState { ttl, @@ -155,7 +197,10 @@ impl AppState<'static> { Ok(&*Box::leak(path.into())) }) .transpose()?, - ip_file: Box::leak(ip_file.into_boxed_path()), + ip_file, + last_ips: std::sync::Arc::new(tokio::sync::Mutex::new( + load_ip(ip_file)?.unwrap_or_default(), + )), }; ensure!( @@ -167,7 +212,7 @@ impl AppState<'static> { } } -fn load_ip(path: &Path) -> Result> { +fn load_ip(path: &Path) -> Result> { debug!("loading last IP from {}", path.display()); let data = match std::fs::read_to_string(path) { Ok(ip) => ip, @@ -181,11 +226,9 @@ fn load_ip(path: &Path) -> Result> { } }; - Ok(Some( - data.parse() - .into_diagnostic() - .wrap_err("failed to parse last ip address")?, - )) + SavedIPs::from_str(&data) + .wrap_err_with(|| format!("failed to load last ip address from {}", path.display())) + .map(Some) } #[tracing::instrument(err)] @@ -266,28 +309,24 @@ fn main() -> Result<()> { .wrap_err("failed to start the tokio runtime")?; rt.block_on(async { - // Load previous IP and update DNS record to point to it (if available) - match load_ip(state.ip_file) { - Ok(Some(ip)) => { - match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await { - Ok(status) => { - if !status.success() { - error!("nsupdate failed: code {status}"); - bail!("nsupdate returned with code {status}"); - } - } - Err(err) => { - error!("Failed to update records with previous IP: {err}"); - return Err(err) - .into_diagnostic() - .wrap_err("failed to update records with previous IP"); + // Update DNS record with previous IPs (if available) + let ips = state.last_ips.lock().await.clone(); + for ip in ips.ips() { + match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await { + Ok(status) => { + if !status.success() { + error!("nsupdate failed: code {status}"); + bail!("nsupdate returned with code {status}"); } } + Err(err) => { + error!("Failed to update records with previous IP: {err}"); + return Err(err) + .into_diagnostic() + .wrap_err("failed to update records with previous IP"); + } } - Ok(None) => info!("No previous IP address set"), - - Err(err) => error!("Ignoring previous IP due to: {err}"), - }; + } // Create services let app = Router::new().route("/update", get(update_records)); @@ -324,13 +363,22 @@ async fn update_records( info!("accepted update from {ip}"); match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await { Ok(status) if status.success() => { + let ips = { + // Update state + let mut ips = state.last_ips.lock().await; + ips.update(ip); + ips.clone() + }; + tokio::task::spawn_blocking(move || { - info!("updating last ip to {ip}"); - if let Err(err) = std::fs::write(state.ip_file, format!("{ip}")) { + info!("updating last ips to {ips:?}"); + let data = serde_json::to_vec(&ips).expect("invalid serialization impl"); + if let Err(err) = std::fs::write(state.ip_file, data) { error!("Failed to update last IP: {err}"); } - info!("updated last ip to {ip}"); + info!("updated last ips to {ips:?}"); }); + Ok("successful update") } Ok(status) => {