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]]
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",

View file

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

12
flake.lock generated
View file

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

View file

@ -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<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> {
@ -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<Option<IpAddr>> {
fn load_ip(path: &Path) -> Result<Option<SavedIPs>> {
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<Option<IpAddr>> {
}
};
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) => {