Compare commits

...

2 commits

Author SHA1 Message Date
71760a59a7
chore(deps): lock file maintenance
All checks were successful
/ build (push) Successful in 1m3s
/ check (push) Successful in 44s
/ report-size (push) Successful in 6s
2025-01-23 21:40:21 +01:00
a2735b46b5
feat(webnsupdate): add handling for multiple IPs
All checks were successful
/ build (push) Successful in 1s
/ check (push) Successful in 8s
/ report-size (push) Successful in 2s
Specifically, for when both and IPv6 and and IPv4 addr is provided. This
ensures we can forward both addrs to webnsupdate, instead of only
allowing IPv4.
2025-01-23 21:10:21 +01:00
4 changed files with 98 additions and 46 deletions

20
Cargo.lock generated
View file

@ -78,9 +78,9 @@ dependencies = [
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.8.2" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efea76243612a2436fb4074ba0cf3ba9ea29efdeb72645d8fc63f116462be1de" checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [ dependencies = [
"axum-core", "axum-core",
"bytes", "bytes",
@ -123,12 +123,12 @@ dependencies = [
[[package]] [[package]]
name = "axum-core" name = "axum-core"
version = "0.5.1" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1b0df7cded837c40dacaa2e1c33aa17c84fc3356ae67b5645f1e83190753e" checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [ dependencies = [
"bytes", "bytes",
"futures-core", "futures-util",
"http", "http",
"http-body", "http-body",
"http-body-util", "http-body-util",
@ -728,9 +728,9 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.43" version = "0.38.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -1086,9 +1086,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243"
[[package]] [[package]]
name = "unicode-linebreak" name = "unicode-linebreak"
@ -1139,6 +1139,8 @@ dependencies = [
"insta", "insta",
"miette", "miette",
"ring", "ring",
"serde",
"serde_json",
"tokio", "tokio",
"tower-http", "tower-http",
"tracing", "tracing",

View file

@ -27,6 +27,8 @@ clap-verbosity-flag = { version = "3", default-features = false, features = [
http = "1" http = "1"
miette = { version = "7", features = ["fancy"] } miette = { version = "7", features = ["fancy"] }
ring = { version = "0.17", features = ["std"] } 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"] } tokio = { version = "1", features = ["macros", "rt", "process", "io-util"] }
tower-http = { version = "0.6.2", features = ["validate-request"] } tower-http = { version = "0.6.2", features = ["validate-request"] }
tracing = "0.1" tracing = "0.1"

12
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1737250794, "lastModified": 1737563566,
"narHash": "sha256-bdIPhvsAKyYQzqAIeay4kOxTHGwLGkhM+IlBIsmMYFI=", "narHash": "sha256-GLJvkOG29XCynQm8XWPyykMRqIhxKcBARVu7Ydrz02M=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "c5b7075f4a6d523fe8204618aa9754e56478c0e0", "rev": "849376434956794ebc7a6b487d31aace395392ba",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -37,11 +37,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1737062831, "lastModified": 1737469691,
"narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=", "narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c", "rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,6 +1,6 @@
use std::{ use std::{
io::ErrorKind, io::ErrorKind,
net::{IpAddr, SocketAddr}, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Duration, time::Duration,
}; };
@ -114,6 +114,48 @@ struct AppState<'a> {
/// The file where the last IP is stored /// The file where the last IP is stored
ip_file: &'a Path, ip_file: &'a Path,
/// Last recorded IPs
last_ips: std::sync::Arc<tokio::sync::Mutex<SavedIPs>>,
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
struct SavedIPs {
#[serde(skip_serializing_if = "Option::is_none")]
ipv4: Option<Ipv4Addr>,
#[serde(skip_serializing_if = "Option::is_none")]
ipv6: Option<Ipv6Addr>,
}
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<Item = IpAddr> {
self.ipv4
.map(IpAddr::V4)
.into_iter()
.chain(self.ipv6.map(IpAddr::V6))
}
fn from_str(data: &str) -> miette::Result<Self> {
match data.parse::<IpAddr>() {
// 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> { impl AppState<'static> {
@ -137,7 +179,7 @@ impl AppState<'static> {
let ttl = Duration::from_secs(*ttl); let ttl = Duration::from_secs(*ttl);
// Use last registered IP address if available // 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 { let state = AppState {
ttl, ttl,
@ -155,7 +197,10 @@ impl AppState<'static> {
Ok(&*Box::leak(path.into())) Ok(&*Box::leak(path.into()))
}) })
.transpose()?, .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!( ensure!(
@ -167,7 +212,7 @@ impl AppState<'static> {
} }
} }
fn load_ip(path: &Path) -> Result<Option<IpAddr>> { fn load_ip(path: &Path) -> Result<Option<SavedIPs>> {
debug!("loading last IP from {}", path.display()); debug!("loading last IP from {}", path.display());
let data = match std::fs::read_to_string(path) { let data = match std::fs::read_to_string(path) {
Ok(ip) => ip, Ok(ip) => ip,
@ -181,11 +226,9 @@ fn load_ip(path: &Path) -> Result<Option<IpAddr>> {
} }
}; };
Ok(Some( SavedIPs::from_str(&data)
data.parse() .wrap_err_with(|| format!("failed to load last ip address from {}", path.display()))
.into_diagnostic() .map(Some)
.wrap_err("failed to parse last ip address")?,
))
} }
#[tracing::instrument(err)] #[tracing::instrument(err)]
@ -266,28 +309,24 @@ fn main() -> Result<()> {
.wrap_err("failed to start the tokio runtime")?; .wrap_err("failed to start the tokio runtime")?;
rt.block_on(async { rt.block_on(async {
// Load previous IP and update DNS record to point to it (if available) // Update DNS record with previous IPs (if available)
match load_ip(state.ip_file) { let ips = state.last_ips.lock().await.clone();
Ok(Some(ip)) => { for ip in ips.ips() {
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await { match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
Ok(status) => { Ok(status) => {
if !status.success() { if !status.success() {
error!("nsupdate failed: code {status}"); error!("nsupdate failed: code {status}");
bail!("nsupdate returned with 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");
} }
} }
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 // Create services
let app = Router::new().route("/update", get(update_records)); let app = Router::new().route("/update", get(update_records));
@ -324,13 +363,22 @@ async fn update_records(
info!("accepted update from {ip}"); info!("accepted update from {ip}");
match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await { match nsupdate::nsupdate(ip, state.ttl, state.key_file, state.records).await {
Ok(status) if status.success() => { 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 || { tokio::task::spawn_blocking(move || {
info!("updating last ip to {ip}"); info!("updating last ips to {ips:?}");
if let Err(err) = std::fs::write(state.ip_file, format!("{ip}")) { 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}"); error!("Failed to update last IP: {err}");
} }
info!("updated last ip to {ip}"); info!("updated last ips to {ips:?}");
}); });
Ok("successful update") Ok("successful update")
} }
Ok(status) => { Ok(status) => {