mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 22:48:57 -03:00
Compare commits
1 Commits
v2.0.7
...
9b8aba5e20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b8aba5e20 |
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cloudflare-ddns"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
edition = "2021"
|
||||
description = "Access your home network remotely via a custom domain name without a static IP"
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::pp::{self, PP};
|
||||
use reqwest::Client;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
||||
const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
||||
@@ -157,6 +157,62 @@ impl CloudflareIpFilter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh interval for Cloudflare IP ranges (24 hours).
|
||||
const CF_RANGE_REFRESH: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
|
||||
/// Cached wrapper around [`CloudflareIpFilter`].
|
||||
///
|
||||
/// Fetches once, then re-uses the cached ranges for [`CF_RANGE_REFRESH`].
|
||||
/// If a refresh fails, the previously cached ranges are kept.
|
||||
pub struct CachedCloudflareFilter {
|
||||
filter: Option<CloudflareIpFilter>,
|
||||
fetched_at: Option<Instant>,
|
||||
}
|
||||
|
||||
impl CachedCloudflareFilter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
filter: None,
|
||||
fetched_at: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the current filter, refreshing if stale or absent.
|
||||
pub async fn get(
|
||||
&mut self,
|
||||
client: &Client,
|
||||
timeout: Duration,
|
||||
ppfmt: &PP,
|
||||
) -> Option<&CloudflareIpFilter> {
|
||||
let stale = match self.fetched_at {
|
||||
Some(t) => t.elapsed() >= CF_RANGE_REFRESH,
|
||||
None => true,
|
||||
};
|
||||
|
||||
if stale {
|
||||
match CloudflareIpFilter::fetch(client, timeout, ppfmt).await {
|
||||
Some(new_filter) => {
|
||||
self.filter = Some(new_filter);
|
||||
self.fetched_at = Some(Instant::now());
|
||||
}
|
||||
None => {
|
||||
if self.filter.is_some() {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
"Failed to refresh Cloudflare IP ranges; using cached version",
|
||||
);
|
||||
// Keep using cached filter, but don't update fetched_at
|
||||
// so we retry next cycle.
|
||||
}
|
||||
// If no cached filter exists, return None (caller handles fail-safe).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.filter.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
18
src/main.rs
18
src/main.rs
@@ -116,12 +116,14 @@ async fn main() {
|
||||
// Start heartbeat
|
||||
heartbeat.start().await;
|
||||
|
||||
let mut cf_cache = cf_ip_filter::CachedCloudflareFilter::new();
|
||||
|
||||
if app_config.legacy_mode {
|
||||
// --- Legacy mode (original cloudflare-ddns behavior) ---
|
||||
run_legacy_mode(&app_config, &handle, ¬ifier, &heartbeat, &ppfmt, running).await;
|
||||
run_legacy_mode(&app_config, &handle, ¬ifier, &heartbeat, &ppfmt, running, &mut cf_cache).await;
|
||||
} else {
|
||||
// --- Env var mode (cf-ddns behavior) ---
|
||||
run_env_mode(&app_config, &handle, ¬ifier, &heartbeat, &ppfmt, running).await;
|
||||
run_env_mode(&app_config, &handle, ¬ifier, &heartbeat, &ppfmt, running, &mut cf_cache).await;
|
||||
}
|
||||
|
||||
// On shutdown: delete records if configured
|
||||
@@ -143,6 +145,7 @@ async fn run_legacy_mode(
|
||||
heartbeat: &Heartbeat,
|
||||
ppfmt: &PP,
|
||||
running: Arc<AtomicBool>,
|
||||
cf_cache: &mut cf_ip_filter::CachedCloudflareFilter,
|
||||
) {
|
||||
let legacy = match &config.legacy_config {
|
||||
Some(l) => l,
|
||||
@@ -165,7 +168,7 @@ async fn run_legacy_mode(
|
||||
}
|
||||
|
||||
while running.load(Ordering::SeqCst) {
|
||||
updater::update_once(config, handle, notifier, heartbeat, ppfmt).await;
|
||||
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt).await;
|
||||
|
||||
for _ in 0..legacy.ttl {
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
@@ -175,7 +178,7 @@ async fn run_legacy_mode(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updater::update_once(config, handle, notifier, heartbeat, ppfmt).await;
|
||||
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,11 +189,12 @@ async fn run_env_mode(
|
||||
heartbeat: &Heartbeat,
|
||||
ppfmt: &PP,
|
||||
running: Arc<AtomicBool>,
|
||||
cf_cache: &mut cf_ip_filter::CachedCloudflareFilter,
|
||||
) {
|
||||
match &config.update_cron {
|
||||
CronSchedule::Once => {
|
||||
if config.update_on_start {
|
||||
updater::update_once(config, handle, notifier, heartbeat, ppfmt).await;
|
||||
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt).await;
|
||||
}
|
||||
}
|
||||
schedule => {
|
||||
@@ -206,7 +210,7 @@ async fn run_env_mode(
|
||||
|
||||
// Update on start if configured
|
||||
if config.update_on_start {
|
||||
updater::update_once(config, handle, notifier, heartbeat, ppfmt).await;
|
||||
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt).await;
|
||||
}
|
||||
|
||||
// Main loop
|
||||
@@ -233,7 +237,7 @@ async fn run_env_mode(
|
||||
return;
|
||||
}
|
||||
|
||||
updater::update_once(config, handle, notifier, heartbeat, ppfmt).await;
|
||||
updater::update_once(config, handle, notifier, heartbeat, cf_cache, ppfmt).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::cf_ip_filter::CloudflareIpFilter;
|
||||
use crate::cf_ip_filter::CachedCloudflareFilter;
|
||||
use crate::cloudflare::{CloudflareHandle, SetResult};
|
||||
use crate::config::{AppConfig, LegacyCloudflareEntry, LegacySubdomainEntry};
|
||||
use crate::domain::make_fqdn;
|
||||
@@ -16,6 +16,7 @@ pub async fn update_once(
|
||||
handle: &CloudflareHandle,
|
||||
notifier: &CompositeNotifier,
|
||||
heartbeat: &Heartbeat,
|
||||
cf_cache: &mut CachedCloudflareFilter,
|
||||
ppfmt: &PP,
|
||||
) -> bool {
|
||||
let detection_client = Client::builder()
|
||||
@@ -28,7 +29,7 @@ pub async fn update_once(
|
||||
let mut notify = false; // NEW: track meaningful events
|
||||
|
||||
if config.legacy_mode {
|
||||
all_ok = update_legacy(config, ppfmt).await;
|
||||
all_ok = update_legacy(config, cf_cache, ppfmt).await;
|
||||
} else {
|
||||
// Detect IPs for each provider
|
||||
let mut detected_ips: HashMap<IpType, Vec<IpAddr>> = HashMap::new();
|
||||
@@ -69,7 +70,7 @@ pub async fn update_once(
|
||||
// Filter out Cloudflare IPs if enabled
|
||||
if config.reject_cloudflare_ips {
|
||||
if let Some(cf_filter) =
|
||||
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||
cf_cache.get(&detection_client, config.detection_timeout, ppfmt).await
|
||||
{
|
||||
for (ip_type, ips) in detected_ips.iter_mut() {
|
||||
let before_count = ips.len();
|
||||
@@ -235,7 +236,7 @@ pub async fn update_once(
|
||||
/// IP-family-bound clients (0.0.0.0 for IPv4, [::] for IPv6). This prevents the old
|
||||
/// wrong-family warning on dual-stack hosts and honours `ip4_provider`/`ip6_provider`
|
||||
/// overrides from config.json.
|
||||
async fn update_legacy(config: &AppConfig, ppfmt: &PP) -> bool {
|
||||
async fn update_legacy(config: &AppConfig, cf_cache: &mut CachedCloudflareFilter, ppfmt: &PP) -> bool {
|
||||
let legacy = match &config.legacy_config {
|
||||
Some(l) => l,
|
||||
None => return false,
|
||||
@@ -301,7 +302,7 @@ async fn update_legacy(config: &AppConfig, ppfmt: &PP) -> bool {
|
||||
if config.reject_cloudflare_ips {
|
||||
let before_count = ips.len();
|
||||
if let Some(cf_filter) =
|
||||
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||
cf_cache.get(&detection_client, config.detection_timeout, ppfmt).await
|
||||
{
|
||||
ips.retain(|key, ip_info| {
|
||||
if let Ok(addr) = ip_info.ip.parse::<std::net::IpAddr>() {
|
||||
@@ -802,7 +803,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -850,7 +852,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -894,7 +897,8 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
// all_ok = true because no zone-level errors occurred (empty ips just noop or warn)
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
// Providers with None are not inserted in loop, so no IP detection warning is emitted,
|
||||
// no detected_ips entry is created, and set_ips is called with empty slice -> Noop.
|
||||
assert!(ok);
|
||||
@@ -943,7 +947,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(!ok, "Expected false when zone is not found");
|
||||
}
|
||||
|
||||
@@ -992,7 +997,8 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
// dry_run returns Updated from set_ips (it signals intent), all_ok should be true
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1057,7 +1063,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1110,7 +1117,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1149,7 +1157,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(!ok, "Expected false when WAF list is not found");
|
||||
}
|
||||
|
||||
@@ -1233,7 +1242,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1249,7 +1259,8 @@ mod tests {
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
|
||||
@@ -1633,7 +1644,8 @@ mod tests {
|
||||
let ppfmt = pp();
|
||||
|
||||
// set_ips with empty ips and no existing records = Noop; all_ok = true
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &ppfmt).await;
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(&config, &cf, ¬ifier, &heartbeat, &mut cf_cache, &ppfmt).await;
|
||||
assert!(ok);
|
||||
}
|
||||
// -------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user