feat(webnsupdate): add handling for multiple IPs
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:
parent
542336867a
commit
a2735b46b5
3 changed files with 83 additions and 31 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1139,6 +1139,8 @@ dependencies = [
|
||||||
"insta",
|
"insta",
|
||||||
"miette",
|
"miette",
|
||||||
"ring",
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
86
src/main.rs
86
src/main.rs
|
@ -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) => {
|
||||||
|
|
Loading…
Add table
Reference in a new issue