Release v2.2.0

- Add native Zulip notification support via zulip:// shoutrrr URLs (#271)
- Add ?messagekey= option for generic webhooks to rename the JSON payload
  field (#271)
- Change DELETE_ON_FAILURE default to false: preserve existing DNS records
  and skip WAF list updates when IP detection fails (#277)
- Document the local.iface.stable IPv6 provider and Helm chart added since
  v2.1.2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Timothy Miller
2026-07-20 23:33:17 -04:00
parent d697c28aa1
commit 6cb1e0c874
15 changed files with 2080 additions and 530 deletions

View File

@@ -11,11 +11,11 @@ use crate::cloudflare::{Auth, CloudflareHandle};
use crate::config::{AppConfig, CronSchedule};
use crate::notifier::{CompositeNotifier, Heartbeat, Message};
use crate::pp::PP;
use rand::RngExt;
use reqwest::Client;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use rand::RngExt;
use reqwest::Client;
use tokio::signal;
use tokio::time::{sleep, Duration};
@@ -131,10 +131,30 @@ async fn main() {
if app_config.legacy_mode {
// --- Legacy mode (original cloudflare-ddns behavior) ---
run_legacy_mode(&app_config, &handle, &notifier, &heartbeat, &ppfmt, running, &mut cf_cache, &detection_client).await;
run_legacy_mode(
&app_config,
&handle,
&notifier,
&heartbeat,
&ppfmt,
running,
&mut cf_cache,
&detection_client,
)
.await;
} else {
// --- Env var mode (cf-ddns behavior) ---
run_env_mode(&app_config, &handle, &notifier, &heartbeat, &ppfmt, running, &mut cf_cache, &detection_client).await;
run_env_mode(
&app_config,
&handle,
&notifier,
&heartbeat,
&ppfmt,
running,
&mut cf_cache,
&detection_client,
)
.await;
}
// On shutdown: delete records if configured
@@ -144,9 +164,7 @@ async fn main() {
}
// Exit heartbeat
heartbeat
.exit(&Message::new_ok("Shutting down"))
.await;
heartbeat.exit(&Message::new_ok("Shutting down")).await;
}
async fn run_legacy_mode(
@@ -182,7 +200,17 @@ async fn run_legacy_mode(
}
while running.load(Ordering::SeqCst) {
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
updater::update_once(
config,
handle,
notifier,
heartbeat,
cf_cache,
ppfmt,
&mut noop_reported,
detection_client,
)
.await;
for _ in 0..legacy.ttl {
if !running.load(Ordering::SeqCst) {
@@ -192,7 +220,17 @@ async fn run_legacy_mode(
}
}
} else {
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
updater::update_once(
config,
handle,
notifier,
heartbeat,
cf_cache,
ppfmt,
&mut noop_reported,
detection_client,
)
.await;
}
}
@@ -211,7 +249,17 @@ async fn run_env_mode(
match &config.update_cron {
CronSchedule::Once => {
if config.update_on_start {
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
updater::update_once(
config,
handle,
notifier,
heartbeat,
cf_cache,
ppfmt,
&mut noop_reported,
detection_client,
)
.await;
}
}
schedule => {
@@ -227,7 +275,17 @@ async fn run_env_mode(
// Update on start if configured
if config.update_on_start {
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
updater::update_once(
config,
handle,
notifier,
heartbeat,
cf_cache,
ppfmt,
&mut noop_reported,
detection_client,
)
.await;
}
// Main loop
@@ -260,7 +318,17 @@ async fn run_env_mode(
sleep(std::time::Duration::from_secs(jitter_secs)).await;
}
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt, &mut noop_reported, detection_client).await;
updater::update_once(
config,
handle,
notifier,
heartbeat,
cf_cache,
ppfmt,
&mut noop_reported,
detection_client,
)
.await;
}
}
}
@@ -319,8 +387,8 @@ pub(crate) fn test_client() -> reqwest::Client {
#[cfg(test)]
mod tests {
use crate::config::{
LegacyAuthentication, LegacyCloudflareEntry, LegacyConfig, LegacySubdomainEntry,
parse_legacy_config,
parse_legacy_config, LegacyAuthentication, LegacyCloudflareEntry, LegacyConfig,
LegacySubdomainEntry,
};
use crate::provider::parse_trace_ip;
use reqwest::Client;
@@ -566,8 +634,7 @@ mod tests {
println!("[DRY RUN] Would add new record {fqdn} -> {ip}");
} else {
println!("Adding new record {fqdn} -> {ip}");
let create_endpoint =
format!("zones/{}/dns_records", entry.zone_id);
let create_endpoint = format!("zones/{}/dns_records", entry.zone_id);
let _: Option<serde_json::Value> = self
.cf_api(
&create_endpoint,
@@ -696,8 +763,15 @@ mod tests {
let ddns = TestDdnsClient::new(&mock_server.uri());
let config = test_config(zone_id);
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, false, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"198.51.100.7",
"A",
&config.cloudflare,
300,
false,
&mut std::collections::HashSet::new(),
)
.await;
}
#[tokio::test]
@@ -745,8 +819,15 @@ mod tests {
let ddns = TestDdnsClient::new(&mock_server.uri());
let config = test_config(zone_id);
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, false, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"198.51.100.7",
"A",
&config.cloudflare,
300,
false,
&mut std::collections::HashSet::new(),
)
.await;
}
#[tokio::test]
@@ -788,8 +869,15 @@ mod tests {
let ddns = TestDdnsClient::new(&mock_server.uri());
let config = test_config(zone_id);
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, false, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"198.51.100.7",
"A",
&config.cloudflare,
300,
false,
&mut std::collections::HashSet::new(),
)
.await;
}
#[tokio::test]
@@ -822,8 +910,15 @@ mod tests {
let ddns = TestDdnsClient::new(&mock_server.uri()).dry_run();
let config = test_config(zone_id);
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, false, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"198.51.100.7",
"A",
&config.cloudflare,
300,
false,
&mut std::collections::HashSet::new(),
)
.await;
}
#[tokio::test]
@@ -879,8 +974,15 @@ mod tests {
ip4_provider: None,
ip6_provider: None,
};
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"198.51.100.7",
"A",
&config.cloudflare,
300,
true,
&mut std::collections::HashSet::new(),
)
.await;
}
// --- jitter_duration tests ---
@@ -1004,7 +1106,14 @@ mod tests {
ip6_provider: None,
};
ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false, &mut std::collections::HashSet::new())
.await;
ddns.commit_record(
"203.0.113.99",
"A",
&config.cloudflare,
300,
false,
&mut std::collections::HashSet::new(),
)
.await;
}
}