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.
This commit is contained in:
Jalil David Salamé Messina 2025-01-23 18:01:58 +01:00
parent 542336867a
commit a2735b46b5
Signed by: jalil
GPG key ID: F016B9E770737A0B
3 changed files with 83 additions and 31 deletions

2
Cargo.lock generated
View file

@ -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"

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,9 +309,9 @@ 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() {
@ -284,10 +327,6 @@ fn main() -> Result<()> {
} }
} }
} }
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) => {