mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-05-06 17:53:42 -03:00
Compare commits
11 Commits
b1d8721e8d
...
v2.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22320bea79 | ||
|
|
1bb347bea7 | ||
|
|
1d5ad2738c | ||
|
|
08ff76f443 | ||
|
|
199bbae2bd | ||
|
|
591f3e4905 | ||
|
|
687d299bda | ||
|
|
25122d2ce3 | ||
|
|
64c971b198 | ||
|
|
b748e80592 | ||
|
|
714ec4f11f |
104
Cargo.lock
generated
104
Cargo.lock
generated
@@ -79,9 +79,20 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudflare-ddns"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
dependencies = [
|
||||
"if-addrs",
|
||||
"rand",
|
||||
@@ -122,6 +133,15 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deadpool"
|
||||
version = "0.12.3"
|
||||
@@ -299,18 +319,6 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
@@ -319,7 +327,8 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"r-efi",
|
||||
"rand_core",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -785,15 +794,6 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@@ -822,12 +822,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
@@ -836,32 +830,20 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.3"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"chacha20",
|
||||
"getrandom 0.4.2",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
@@ -900,9 +882,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.13.2"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||
checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
@@ -964,9 +946,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.39"
|
||||
version = "0.23.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
|
||||
checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -1887,26 +1869,6 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.48"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.6"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cloudflare-ddns"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
edition = "2021"
|
||||
description = "Access your home network remotely via a custom domain name without a static IP"
|
||||
license = "GPL-3.0"
|
||||
@@ -14,7 +14,7 @@ tokio = { version = "1", features = ["rt", "macros", "time", "signal", "net"] }
|
||||
regex-lite = "0.1"
|
||||
url = "2"
|
||||
if-addrs = "0.15"
|
||||
rand = "0.9"
|
||||
rand = "0.10"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
|
||||
@@ -107,6 +107,7 @@ To disable this protection, set `REJECT_CLOUDFLARE_IPS=false`.
|
||||
| `UPDATE_CRON` | `@every 5m` | Update schedule |
|
||||
| `UPDATE_ON_START` | `true` | Run an update immediately on startup |
|
||||
| `DELETE_ON_STOP` | `false` | Delete managed DNS records on shutdown |
|
||||
| `DELETE_ON_FAILURE` | `true` | Delete managed DNS records when failed to obtain IP from provider |
|
||||
|
||||
Schedule formats:
|
||||
|
||||
@@ -213,6 +214,7 @@ Heartbeats are sent after each update cycle. On failure, a fail signal is sent.
|
||||
| `UPDATE_CRON` | `@every 5m` | ⏱️ Update schedule |
|
||||
| `UPDATE_ON_START` | `true` | 🚀 Update on startup |
|
||||
| `DELETE_ON_STOP` | `false` | 🧹 Delete records on shutdown |
|
||||
| `DELETE_ON_FAILURE` | `true` | 🧹 Delete records if failed to obtain new records |
|
||||
| `TTL` | `1` | ⏳ DNS record TTL |
|
||||
| `PROXIED` | `false` | ☁️ Proxied expression |
|
||||
| `RECORD_COMMENT` | — | 💬 DNS record comment |
|
||||
|
||||
49
RELEASE_NOTES_2.1.1.md
Normal file
49
RELEASE_NOTES_2.1.1.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# cloudflare-ddns v2.1.1
|
||||
|
||||
Maintenance release. Bug fix for `rand` 0.10 API change, plus opt-in failure-safe deletion behavior contributed in the v2.1.0 → v2.1.1 window, dependency refresh, and proportional jitter for IP detection.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Fix:** Restore the build under `rand` 0.10 — `random_range` moved to the `RngExt` trait, and the unconditional jitter sleep in `--repeat` mode no longer fails to compile.
|
||||
- **New:** `DELETE_ON_FAILURE` (env-var mode) controls whether DNS records are removed when an IP detection or update fails. Defaults to `true` to preserve existing behavior; set `DELETE_ON_FAILURE=false` to keep stale records on transient failures instead of yanking them.
|
||||
- **Improvement:** Proportional jitter (up to 20% of the update interval) is added before each scheduled update to spread requests across clients and reduce synchronized spikes against the Cloudflare API.
|
||||
|
||||
## Changes since v2.1.0
|
||||
|
||||
### Features
|
||||
- `DELETE_ON_FAILURE` env var to prevent DNS record deletion on failed updates (#263, thanks @DMaxter)
|
||||
- Proportional jitter on update intervals to desynchronize API traffic (#253, thanks @jhutchings1)
|
||||
|
||||
### Fixes
|
||||
- Compile fix for `rand` 0.10: import `RngExt` so `random_range` resolves
|
||||
- `delete_on_failure` regression test coverage added
|
||||
|
||||
### Dependencies
|
||||
- `rustls` 0.23.37 → 0.23.40
|
||||
- `rustls-webpki` 0.103.10 → 0.103.13
|
||||
- `tokio` 1.50.0 → 1.52.1
|
||||
- `reqwest` 0.13.2 → 0.13.3
|
||||
- `rand` 0.9.2 → 0.10.1
|
||||
|
||||
### Docs
|
||||
- Document `DELETE_ON_FAILURE` in the README
|
||||
|
||||
## Upgrade notes
|
||||
|
||||
- **Default behavior unchanged.** `DELETE_ON_FAILURE` defaults to `true`, matching pre-2.1.1 behavior. Set it to `false` if you want stale records preserved during outages.
|
||||
- No config file schema changes. Existing `config.json` deployments continue to work without edits.
|
||||
|
||||
## Docker
|
||||
|
||||
```sh
|
||||
docker pull timothyjmiller/cloudflare-ddns:2.1.1
|
||||
docker pull timothyjmiller/cloudflare-ddns:latest
|
||||
```
|
||||
|
||||
Multi-arch: `linux/amd64`, `linux/arm64`, `linux/ppc64le`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `cargo test` — 352 tests pass
|
||||
- Release build succeeds, binary size ~1.7 MiB (pre-UPX)
|
||||
- Smoke tested in both legacy `config.json` mode and env-var mode against the live Cloudflare API
|
||||
@@ -84,6 +84,7 @@ pub struct AppConfig {
|
||||
pub update_cron: CronSchedule,
|
||||
pub update_on_start: bool,
|
||||
pub delete_on_stop: bool,
|
||||
pub delete_on_failure: bool,
|
||||
pub ttl: TTL,
|
||||
pub proxied_expression: Option<Box<dyn Fn(&str) -> bool + Send + Sync>>,
|
||||
pub record_comment: Option<String>,
|
||||
@@ -449,6 +450,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Re
|
||||
update_cron: schedule,
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
ttl,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
@@ -503,6 +505,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
||||
let update_cron = read_cron_from_env(ppfmt)?;
|
||||
let update_on_start = getenv_bool("UPDATE_ON_START", true);
|
||||
let delete_on_stop = getenv_bool("DELETE_ON_STOP", false);
|
||||
let delete_on_failure = getenv_bool("DELETE_ON_FAILURE", true);
|
||||
|
||||
let ttl_val = getenv("TTL")
|
||||
.and_then(|s| s.parse::<i64>().ok())
|
||||
@@ -571,6 +574,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
||||
update_cron,
|
||||
update_on_start,
|
||||
delete_on_stop,
|
||||
delete_on_failure,
|
||||
ttl,
|
||||
proxied_expression,
|
||||
record_comment,
|
||||
@@ -1317,6 +1321,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Once,
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::AUTO,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
@@ -1351,6 +1356,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Every(Duration::from_secs(300)),
|
||||
update_on_start: true,
|
||||
delete_on_stop: true,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::new(60),
|
||||
proxied_expression: None,
|
||||
record_comment: Some("managed".to_string()),
|
||||
@@ -2003,6 +2009,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Every(Duration::from_secs(300)),
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::AUTO,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
@@ -2039,6 +2046,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Every(Duration::from_secs(600)),
|
||||
update_on_start: true,
|
||||
delete_on_stop: true,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::new(120),
|
||||
proxied_expression: None,
|
||||
record_comment: Some("cf-ddns".to_string()),
|
||||
@@ -2072,6 +2080,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Once,
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::AUTO,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::pp::PP;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use rand::Rng;
|
||||
use rand::RngExt;
|
||||
use reqwest::Client;
|
||||
use tokio::signal;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
280
src/updater.rs
280
src/updater.rs
@@ -115,6 +115,19 @@ pub async fn update_once(
|
||||
// Update DNS records (env var mode - domain-based)
|
||||
for (ip_type, domains) in &config.domains {
|
||||
let ips = detected_ips.get(ip_type).cloned().unwrap_or_default();
|
||||
|
||||
if ips.is_empty() && !config.delete_on_failure {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!(
|
||||
"Skipping {} domain update for {}",
|
||||
ip_type.describe(),
|
||||
domains.join(", ")
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let record_type = ip_type.record_type();
|
||||
|
||||
for domain_str in domains {
|
||||
@@ -713,6 +726,7 @@ mod tests {
|
||||
update_cron: CronSchedule::Once,
|
||||
update_on_start: true,
|
||||
delete_on_stop: false,
|
||||
delete_on_failure: true,
|
||||
ttl: TTL::AUTO,
|
||||
proxied_expression: None,
|
||||
record_comment: None,
|
||||
@@ -2307,6 +2321,272 @@ mod tests {
|
||||
ddns.delete_entries("A", &config).await;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------
|
||||
// delete_on_failure tests
|
||||
// -------------------------------------------------------
|
||||
|
||||
/// When IPv4 detection fails but IPv6 succeeds, and delete_on_failure=false, skip V4 domains but update V6
|
||||
#[tokio::test]
|
||||
async fn test_skip_v4_domains_when_v4_detection_fails() {
|
||||
let server = MockServer::start().await;
|
||||
let zone_id = "zone-abc";
|
||||
let ip_v6 = "2001:db8::1";
|
||||
|
||||
// Zone lookup for V6 domain
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/zones"))
|
||||
.and(query_param("name", "v6.example.com"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(zones_response(zone_id, "example.com")),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// LIST existing records for V6
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_records_empty()))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// POST for V6 should be called (V6 succeeds)
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_record_created(
|
||||
"rec-1",
|
||||
"v6.example.com",
|
||||
"2001:db8::1",
|
||||
)))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Providers: V4 fails (None), V6 succeeds
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert(IpType::V4, ProviderType::None);
|
||||
providers.insert(
|
||||
IpType::V6,
|
||||
ProviderType::Literal {
|
||||
ips: vec![ip_v6.parse().unwrap()],
|
||||
},
|
||||
);
|
||||
|
||||
let mut domains = HashMap::new();
|
||||
domains.insert(IpType::V4, vec!["v4.example.com".to_string()]);
|
||||
domains.insert(IpType::V6, vec!["v6.example.com".to_string()]);
|
||||
|
||||
let mut config = make_config(providers, domains, vec![], false);
|
||||
config.delete_on_failure = false;
|
||||
|
||||
let cf = handle(&server.uri());
|
||||
let notifier = empty_notifier();
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(
|
||||
&config,
|
||||
&cf,
|
||||
¬ifier,
|
||||
&heartbeat,
|
||||
&mut cf_cache,
|
||||
&ppfmt,
|
||||
&mut HashSet::new(),
|
||||
&crate::test_client(),
|
||||
)
|
||||
.await;
|
||||
assert!(ok, "Should succeed with partial detection");
|
||||
}
|
||||
|
||||
/// When IPv6 detection fails but IPv4 succeeds, and delete_on_failure=false, skip V6 domains but update V4
|
||||
#[tokio::test]
|
||||
async fn test_skip_v6_domains_when_v6_detection_fails() {
|
||||
let server = MockServer::start().await;
|
||||
let zone_id = "zone-abc";
|
||||
let ip_v4 = "198.51.100.42";
|
||||
|
||||
// Zone lookup for V4 domain
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/zones"))
|
||||
.and(query_param("name", "v4.example.com"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(zones_response(zone_id, "example.com")),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// LIST existing records for V4
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_records_empty()))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// POST for V4 should be called (V4 succeeds)
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_record_created(
|
||||
"rec-1",
|
||||
"v4.example.com",
|
||||
"198.51.100.42",
|
||||
)))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Providers: V4 succeeds, V6 fails (None)
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert(
|
||||
IpType::V4,
|
||||
ProviderType::Literal {
|
||||
ips: vec![ip_v4.parse().unwrap()],
|
||||
},
|
||||
);
|
||||
providers.insert(IpType::V6, ProviderType::None);
|
||||
|
||||
let mut domains = HashMap::new();
|
||||
domains.insert(IpType::V4, vec!["v4.example.com".to_string()]);
|
||||
domains.insert(IpType::V6, vec!["v6.example.com".to_string()]);
|
||||
|
||||
let mut config = make_config(providers, domains, vec![], false);
|
||||
config.delete_on_failure = false;
|
||||
|
||||
let cf = handle(&server.uri());
|
||||
let notifier = empty_notifier();
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(
|
||||
&config,
|
||||
&cf,
|
||||
¬ifier,
|
||||
&heartbeat,
|
||||
&mut cf_cache,
|
||||
&ppfmt,
|
||||
&mut HashSet::new(),
|
||||
&crate::test_client(),
|
||||
)
|
||||
.await;
|
||||
assert!(ok, "Should succeed with partial detection");
|
||||
}
|
||||
|
||||
/// When both IPv4 and IPv6 detection fail, and delete_on_failure=false, skip all domains
|
||||
#[tokio::test]
|
||||
async fn test_skip_all_domains_when_both_detect_fail() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
// No POST/DELETE should be called at all
|
||||
|
||||
// Providers: both fail (None)
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert(IpType::V4, ProviderType::None);
|
||||
providers.insert(IpType::V6, ProviderType::None);
|
||||
|
||||
let mut domains = HashMap::new();
|
||||
domains.insert(IpType::V4, vec!["v4.example.com".to_string()]);
|
||||
domains.insert(IpType::V6, vec!["v6.example.com".to_string()]);
|
||||
|
||||
let mut config = make_config(providers, domains, vec![], false);
|
||||
config.delete_on_failure = false;
|
||||
|
||||
let cf = handle(&server.uri());
|
||||
let notifier = empty_notifier();
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(
|
||||
&config,
|
||||
&cf,
|
||||
¬ifier,
|
||||
&heartbeat,
|
||||
&mut cf_cache,
|
||||
&ppfmt,
|
||||
&mut HashSet::new(),
|
||||
&crate::test_client(),
|
||||
)
|
||||
.await;
|
||||
assert!(ok, "Should succeed (no updates, no failures)");
|
||||
}
|
||||
|
||||
/// When both IPv4 and IPv6 detection succeed, and delete_on_failure=false, update all domains
|
||||
#[tokio::test]
|
||||
async fn test_update_all_domains_when_both_detect() {
|
||||
let server = MockServer::start().await;
|
||||
let zone_id = "zone-abc";
|
||||
let ip_v4 = "198.51.100.42";
|
||||
let ip_v6 = "2001:db8::1";
|
||||
|
||||
// Zone lookups for both domains
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/zones"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200).set_body_json(zones_response(zone_id, "example.com")),
|
||||
)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// LIST existing records (empty for both)
|
||||
Mock::given(method("GET"))
|
||||
.and(path_regex(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_records_empty()))
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// POST for both should be called
|
||||
Mock::given(method("POST"))
|
||||
.and(path(format!("/zones/{zone_id}/dns_records")))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(dns_record_created(
|
||||
"rec-new",
|
||||
"example.com",
|
||||
"198.51.100.42",
|
||||
)))
|
||||
.expect(2) // Two POSTs: one for V4, one for V6
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
// Providers: both succeed
|
||||
let mut providers = HashMap::new();
|
||||
providers.insert(
|
||||
IpType::V4,
|
||||
ProviderType::Literal {
|
||||
ips: vec![ip_v4.parse().unwrap()],
|
||||
},
|
||||
);
|
||||
providers.insert(
|
||||
IpType::V6,
|
||||
ProviderType::Literal {
|
||||
ips: vec![ip_v6.parse().unwrap()],
|
||||
},
|
||||
);
|
||||
|
||||
let mut domains = HashMap::new();
|
||||
domains.insert(IpType::V4, vec!["v4.example.com".to_string()]);
|
||||
domains.insert(IpType::V6, vec!["v6.example.com".to_string()]);
|
||||
|
||||
let mut config = make_config(providers, domains, vec![], false);
|
||||
config.delete_on_failure = false;
|
||||
|
||||
let cf = handle(&server.uri());
|
||||
let notifier = empty_notifier();
|
||||
let heartbeat = empty_heartbeat();
|
||||
let ppfmt = pp();
|
||||
|
||||
let mut cf_cache = CachedCloudflareFilter::new();
|
||||
let ok = update_once(
|
||||
&config,
|
||||
&cf,
|
||||
¬ifier,
|
||||
&heartbeat,
|
||||
&mut cf_cache,
|
||||
&ppfmt,
|
||||
&mut HashSet::new(),
|
||||
&crate::test_client(),
|
||||
)
|
||||
.await;
|
||||
assert!(ok, "Should succeed with both detections");
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy types for backwards compatibility
|
||||
|
||||
Reference in New Issue
Block a user