3 Commits

Author SHA1 Message Date
Timothy Miller
f8d5b5cb7e Bump version to 2.0.5 2026-03-19 18:19:41 -04:00
Timothy Miller
bb5cc43651 Add ip4_provider and ip6_provider for legacy mode
Use the shared provider abstraction for IPv4/IPv6 detection in legacy
mode.
Allow per-family provider overrides in config.json (ip4_provider /
ip6_provider)
and support disabling a family with "none". Update config parsing,
examples,
and the legacy update flow to use the provider-based detection client.
2026-03-19 18:18:53 -04:00
Timothy Miller
7ff8379cfb Filter Cloudflare IPs in legacy mode
Add support for REJECT_CLOUDFLARE_IPS in legacy config and fetch
Cloudflare
IP ranges to drop matching detected addresses. Improve IP detection in
legacy mode by using literal-IP primary trace URLs with hostname
fallbacks, binding dedicated IPv4/IPv6 HTTP clients, and setting a Host
override for literal-IP trace endpoints so TLS SNI works. Expose
build_split_client and update tests accordingly.
2026-03-19 18:18:32 -04:00
8 changed files with 264 additions and 345 deletions

2
Cargo.lock generated
View File

@@ -109,7 +109,7 @@ dependencies = [
[[package]] [[package]]
name = "cloudflare-ddns" name = "cloudflare-ddns"
version = "2.0.4" version = "2.0.5"
dependencies = [ dependencies = [
"chrono", "chrono",
"idna", "idna",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cloudflare-ddns" name = "cloudflare-ddns"
version = "2.0.4" version = "2.0.5"
edition = "2021" edition = "2021"
description = "Access your home network remotely via a custom domain name without a static IP" description = "Access your home network remotely via a custom domain name without a static IP"
license = "GPL-3.0" license = "GPL-3.0"

View File

@@ -368,6 +368,42 @@ Some ISP provided modems only allow port forwarding over IPv4 or IPv6. Disable t
| `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates | | `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates |
| `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records | | `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records |
| `ttl` | int | `300` | DNS record TTL in seconds (30-86400, values < 30 become auto) | | `ttl` | int | `300` | DNS record TTL in seconds (30-86400, values < 30 become auto) |
| `ip4_provider` | string | `"cloudflare.trace"` | IPv4 detection provider (same values as `IP4_PROVIDER` env var) |
| `ip6_provider` | string | `"cloudflare.trace"` | IPv6 detection provider (same values as `IP6_PROVIDER` env var) |
### 🚫 Cloudflare IP Rejection (Legacy Mode)
The `REJECT_CLOUDFLARE_IPS` environment variable is supported in legacy config mode. Set it alongside your `config.json`:
```bash
REJECT_CLOUDFLARE_IPS=true cloudflare-ddns
```
Or in Docker Compose:
```yml
environment:
- REJECT_CLOUDFLARE_IPS=true
volumes:
- ./config.json:/config.json
```
### 🔍 IP Detection (Legacy Mode)
Legacy mode now uses the same shared provider abstraction as environment variable mode. By default it uses the `cloudflare.trace` provider, which builds an IP-family-bound HTTP client (`0.0.0.0` for IPv4, `[::]` for IPv6) to guarantee the correct address family on dual-stack hosts.
You can override the detection method per address family with `ip4_provider` and `ip6_provider` in your `config.json`. Supported values are the same as the `IP4_PROVIDER` / `IP6_PROVIDER` environment variables: `cloudflare.trace`, `cloudflare.doh`, `ipify`, `local`, `local.iface:<name>`, `url:<https://...>`, `none`.
Set a provider to `"none"` to disable detection for that address family (overrides `a`/`aaaa`):
```json
{
"a": true,
"aaaa": true,
"ip4_provider": "cloudflare.trace",
"ip6_provider": "none"
}
```
Each zone entry contains: Each zone entry contains:

View File

@@ -24,5 +24,7 @@
"a": true, "a": true,
"aaaa": true, "aaaa": true,
"purgeUnknownRecords": false, "purgeUnknownRecords": false,
"ttl": 300 "ttl": 300,
"ip4_provider": "cloudflare.trace",
"ip6_provider": "cloudflare.trace"
} }

View File

@@ -27,6 +27,10 @@ pub struct LegacyConfig {
pub purge_unknown_records: bool, pub purge_unknown_records: bool,
#[serde(default = "default_ttl")] #[serde(default = "default_ttl")]
pub ttl: i64, pub ttl: i64,
#[serde(default)]
pub ip4_provider: Option<String>,
#[serde(default)]
pub ip6_provider: Option<String>,
} }
fn default_true() -> bool { fn default_true() -> bool {
@@ -387,7 +391,7 @@ pub fn parse_legacy_config(content: &str) -> Result<LegacyConfig, String> {
} }
/// Convert a legacy config into a unified AppConfig /// Convert a legacy config into a unified AppConfig
fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> AppConfig { fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Result<AppConfig, String> {
// Extract auth from first entry // Extract auth from first entry
let auth = if let Some(entry) = legacy.cloudflare.first() { let auth = if let Some(entry) = legacy.cloudflare.first() {
if !entry.authentication.api_token.is_empty() if !entry.authentication.api_token.is_empty()
@@ -406,13 +410,27 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
Auth::Token(String::new()) Auth::Token(String::new())
}; };
// Build providers // Build providers — ip4_provider/ip6_provider override the default cloudflare.trace
let mut providers = HashMap::new(); let mut providers = HashMap::new();
if legacy.a { if legacy.a {
providers.insert(IpType::V4, ProviderType::CloudflareTrace { url: None }); let provider = match &legacy.ip4_provider {
Some(s) => ProviderType::parse(s)
.map_err(|e| format!("Invalid ip4_provider in config.json: {e}"))?,
None => ProviderType::CloudflareTrace { url: None },
};
if !matches!(provider, ProviderType::None) {
providers.insert(IpType::V4, provider);
}
} }
if legacy.aaaa { if legacy.aaaa {
providers.insert(IpType::V6, ProviderType::CloudflareTrace { url: None }); let provider = match &legacy.ip6_provider {
Some(s) => ProviderType::parse(s)
.map_err(|e| format!("Invalid ip6_provider in config.json: {e}"))?,
None => ProviderType::CloudflareTrace { url: None },
};
if !matches!(provider, ProviderType::None) {
providers.insert(IpType::V6, provider);
}
} }
let ttl = TTL::new(legacy.ttl); let ttl = TTL::new(legacy.ttl);
@@ -423,7 +441,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
CronSchedule::Once CronSchedule::Once
}; };
AppConfig { Ok(AppConfig {
auth, auth,
providers, providers,
domains: HashMap::new(), domains: HashMap::new(),
@@ -440,14 +458,14 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
managed_waf_comment_regex: None, managed_waf_comment_regex: None,
detection_timeout: Duration::from_secs(5), detection_timeout: Duration::from_secs(5),
update_timeout: Duration::from_secs(30), update_timeout: Duration::from_secs(30),
reject_cloudflare_ips: false, reject_cloudflare_ips: getenv_bool("REJECT_CLOUDFLARE_IPS", false),
dry_run, dry_run,
emoji: false, emoji: false,
quiet: false, quiet: false,
legacy_mode: true, legacy_mode: true,
legacy_config: Some(legacy), legacy_config: Some(legacy),
repeat, repeat,
} })
} }
// ============================================================ // ============================================================
@@ -583,7 +601,7 @@ pub fn load_config(dry_run: bool, repeat: bool, ppfmt: &PP) -> Result<AppConfig,
} else { } else {
ppfmt.infof(pp::EMOJI_CONFIG, "Using config.json configuration"); ppfmt.infof(pp::EMOJI_CONFIG, "Using config.json configuration");
let legacy = load_legacy_config()?; let legacy = load_legacy_config()?;
Ok(legacy_to_app_config(legacy, dry_run, repeat)) legacy_to_app_config(legacy, dry_run, repeat)
} }
} }
@@ -995,8 +1013,10 @@ mod tests {
aaaa: false, aaaa: false,
purge_unknown_records: false, purge_unknown_records: false,
ttl: 300, ttl: 300,
ip4_provider: None,
ip6_provider: None,
}; };
let config = legacy_to_app_config(legacy, false, false); let config = legacy_to_app_config(legacy, false, false).unwrap();
assert!(config.legacy_mode); assert!(config.legacy_mode);
assert!(matches!(config.auth, Auth::Token(ref t) if t == "my-token")); assert!(matches!(config.auth, Auth::Token(ref t) if t == "my-token"));
assert!(config.providers.contains_key(&IpType::V4)); assert!(config.providers.contains_key(&IpType::V4));
@@ -1021,8 +1041,10 @@ mod tests {
aaaa: true, aaaa: true,
purge_unknown_records: false, purge_unknown_records: false,
ttl: 120, ttl: 120,
ip4_provider: None,
ip6_provider: None,
}; };
let config = legacy_to_app_config(legacy, true, true); let config = legacy_to_app_config(legacy, true, true).unwrap();
assert!(matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120))); assert!(matches!(config.update_cron, CronSchedule::Every(d) if d == Duration::from_secs(120)));
assert!(config.repeat); assert!(config.repeat);
assert!(config.dry_run); assert!(config.dry_run);
@@ -1047,12 +1069,118 @@ mod tests {
aaaa: true, aaaa: true,
purge_unknown_records: false, purge_unknown_records: false,
ttl: 300, ttl: 300,
ip4_provider: None,
ip6_provider: None,
}; };
let config = legacy_to_app_config(legacy, false, false); let config = legacy_to_app_config(legacy, false, false).unwrap();
assert!(matches!(config.auth, Auth::Key { ref api_key, ref email } assert!(matches!(config.auth, Auth::Key { ref api_key, ref email }
if api_key == "key123" && email == "test@example.com")); if api_key == "key123" && email == "test@example.com"));
} }
#[test]
fn test_legacy_to_app_config_custom_providers() {
let legacy = LegacyConfig {
cloudflare: vec![LegacyCloudflareEntry {
authentication: LegacyAuthentication {
api_token: "tok".to_string(),
api_key: None,
},
zone_id: "z".to_string(),
subdomains: vec![],
proxied: false,
}],
a: true,
aaaa: true,
purge_unknown_records: false,
ttl: 300,
ip4_provider: Some("ipify".to_string()),
ip6_provider: Some("cloudflare.doh".to_string()),
};
let config = legacy_to_app_config(legacy, false, false).unwrap();
assert!(matches!(config.providers[&IpType::V4], ProviderType::Ipify));
assert!(matches!(config.providers[&IpType::V6], ProviderType::CloudflareDOH));
}
#[test]
fn test_legacy_to_app_config_provider_none_overrides_a_flag() {
let legacy = LegacyConfig {
cloudflare: vec![LegacyCloudflareEntry {
authentication: LegacyAuthentication {
api_token: "tok".to_string(),
api_key: None,
},
zone_id: "z".to_string(),
subdomains: vec![],
proxied: false,
}],
a: true,
aaaa: true,
purge_unknown_records: false,
ttl: 300,
ip4_provider: Some("none".to_string()),
ip6_provider: None,
};
let config = legacy_to_app_config(legacy, false, false).unwrap();
// ip4_provider=none should exclude V4 even though a=true
assert!(!config.providers.contains_key(&IpType::V4));
assert!(config.providers.contains_key(&IpType::V6));
}
#[test]
fn test_legacy_to_app_config_invalid_provider_returns_error() {
let legacy = LegacyConfig {
cloudflare: vec![LegacyCloudflareEntry {
authentication: LegacyAuthentication {
api_token: "tok".to_string(),
api_key: None,
},
zone_id: "z".to_string(),
subdomains: vec![],
proxied: false,
}],
a: true,
aaaa: false,
purge_unknown_records: false,
ttl: 300,
ip4_provider: Some("totally_invalid".to_string()),
ip6_provider: None,
};
let result = legacy_to_app_config(legacy, false, false);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(err.contains("ip4_provider"));
}
#[test]
fn test_legacy_config_deserializes_providers() {
let json = r#"{
"cloudflare": [{
"authentication": { "api_token": "tok" },
"zone_id": "z",
"subdomains": ["@"]
}],
"ip4_provider": "ipify",
"ip6_provider": "none"
}"#;
let config = parse_legacy_config(json).unwrap();
assert_eq!(config.ip4_provider, Some("ipify".to_string()));
assert_eq!(config.ip6_provider, Some("none".to_string()));
}
#[test]
fn test_legacy_config_deserializes_without_providers() {
let json = r#"{
"cloudflare": [{
"authentication": { "api_token": "tok" },
"zone_id": "z",
"subdomains": ["@"]
}]
}"#;
let config = parse_legacy_config(json).unwrap();
assert!(config.ip4_provider.is_none());
assert!(config.ip6_provider.is_none());
}
// --- is_env_config_mode --- // --- is_env_config_mode ---
#[test] #[test]

View File

@@ -301,6 +301,8 @@ mod tests {
aaaa: false, aaaa: false,
purge_unknown_records: false, purge_unknown_records: false,
ttl: 300, ttl: 300,
ip4_provider: None,
ip6_provider: None,
} }
} }
@@ -814,6 +816,8 @@ mod tests {
aaaa: false, aaaa: false,
purge_unknown_records: true, purge_unknown_records: true,
ttl: 300, ttl: 300,
ip4_provider: None,
ip6_provider: None,
}; };
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true) ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true)
.await; .await;
@@ -913,6 +917,8 @@ mod tests {
aaaa: false, aaaa: false,
purge_unknown_records: false, purge_unknown_records: false,
ttl: 300, ttl: 300,
ip4_provider: None,
ip6_provider: None,
}; };
ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false) ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false)

View File

@@ -183,7 +183,7 @@ async fn fetch_trace_ip(
/// Build an HTTP client that only connects via the given IP family. /// Build an HTTP client that only connects via the given IP family.
/// Binding to 0.0.0.0 forces IPv4-only; binding to [::] forces IPv6-only. /// Binding to 0.0.0.0 forces IPv4-only; binding to [::] forces IPv6-only.
/// This ensures the trace endpoint sees the correct address family. /// This ensures the trace endpoint sees the correct address family.
fn build_split_client(ip_type: IpType, timeout: Duration) -> Client { pub fn build_split_client(ip_type: IpType, timeout: Duration) -> Client {
let local_addr: IpAddr = match ip_type { let local_addr: IpAddr = match ip_type {
IpType::V4 => Ipv4Addr::UNSPECIFIED.into(), IpType::V4 => Ipv4Addr::UNSPECIFIED.into(),
IpType::V6 => Ipv6Addr::UNSPECIFIED.into(), IpType::V6 => Ipv6Addr::UNSPECIFIED.into(),

View File

@@ -229,7 +229,12 @@ pub async fn update_once(
} }
/// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config). /// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config).
async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool { ///
/// IP detection uses the shared provider abstraction (`config.providers`), which builds
/// 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 {
let legacy = match &config.legacy_config { let legacy = match &config.legacy_config {
Some(l) => l, Some(l) => l,
None => return false, None => return false,
@@ -243,29 +248,82 @@ async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client, client,
cf_api_base: "https://api.cloudflare.com/client/v4".to_string(), cf_api_base: "https://api.cloudflare.com/client/v4".to_string(),
ipv4_urls: vec![
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
"https://1.0.0.1/cdn-cgi/trace".to_string(),
],
ipv6_urls: vec![
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
"https://[2606:4700:4700::1001]/cdn-cgi/trace".to_string(),
],
dry_run: config.dry_run, dry_run: config.dry_run,
}; };
let mut warnings = LegacyWarningState::default(); // Detect IPs using the shared provider abstraction
let detection_client = Client::builder()
.timeout(config.detection_timeout)
.build()
.unwrap_or_default();
let ips = ddns let mut ips = HashMap::new();
.get_ips(
legacy.a, for (ip_type, provider) in &config.providers {
legacy.aaaa, ppfmt.infof(
legacy.purge_unknown_records, pp::EMOJI_DETECT,
&legacy.cloudflare, &format!("Detecting {} via {}", ip_type.describe(), provider.name()),
&mut warnings, );
) let detected = provider
.detect_ips(&detection_client, *ip_type, config.detection_timeout, ppfmt)
.await; .await;
if detected.is_empty() {
ppfmt.warningf(
pp::EMOJI_WARNING,
&format!("No {} address detected", ip_type.describe()),
);
if legacy.purge_unknown_records {
ddns.delete_entries(ip_type.record_type(), &legacy.cloudflare)
.await;
}
} else {
let key = match ip_type {
IpType::V4 => "ipv4",
IpType::V6 => "ipv6",
};
ppfmt.infof(
pp::EMOJI_DETECT,
&format!("Detected {}: {}", ip_type.describe(), detected[0]),
);
ips.insert(
key.to_string(),
LegacyIpInfo {
record_type: ip_type.record_type().to_string(),
ip: detected[0].to_string(),
},
);
}
}
// 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
{
ips.retain(|key, ip_info| {
if let Ok(addr) = ip_info.ip.parse::<std::net::IpAddr>() {
if cf_filter.contains(&addr) {
ppfmt.warningf(
pp::EMOJI_WARNING,
&format!(
"Rejected {}: matches Cloudflare IP range ({})",
ip_info.ip, key
),
);
return false;
}
}
true
});
} else {
ppfmt.warningf(
pp::EMOJI_WARNING,
"Could not fetch Cloudflare IP ranges; skipping filter",
);
}
}
ddns.update_ips( ddns.update_ips(
&ips, &ips,
&legacy.cloudflare, &legacy.cloudflare,
@@ -323,141 +381,13 @@ pub struct LegacyIpInfo {
pub ip: String, pub ip: String,
} }
struct LegacyWarningState {
shown_ipv4: bool,
shown_ipv4_secondary: bool,
shown_ipv6: bool,
shown_ipv6_secondary: bool,
}
impl Default for LegacyWarningState {
fn default() -> Self {
Self {
shown_ipv4: false,
shown_ipv4_secondary: false,
shown_ipv6: false,
shown_ipv6_secondary: false,
}
}
}
struct LegacyDdnsClient { struct LegacyDdnsClient {
client: Client, client: Client,
cf_api_base: String, cf_api_base: String,
ipv4_urls: Vec<String>,
ipv6_urls: Vec<String>,
dry_run: bool, dry_run: bool,
} }
impl LegacyDdnsClient { impl LegacyDdnsClient {
async fn get_ips(
&self,
ipv4_enabled: bool,
ipv6_enabled: bool,
purge_unknown_records: bool,
config: &[LegacyCloudflareEntry],
warnings: &mut LegacyWarningState,
) -> HashMap<String, LegacyIpInfo> {
let mut ips = HashMap::new();
if ipv4_enabled {
let a = self
.try_trace_urls(
&self.ipv4_urls,
&mut warnings.shown_ipv4,
&mut warnings.shown_ipv4_secondary,
"IPv4",
true,
)
.await;
if a.is_none() && purge_unknown_records {
self.delete_entries("A", config).await;
}
if let Some(ip) = a {
ips.insert(
"ipv4".to_string(),
LegacyIpInfo {
record_type: "A".to_string(),
ip,
},
);
}
}
if ipv6_enabled {
let aaaa = self
.try_trace_urls(
&self.ipv6_urls,
&mut warnings.shown_ipv6,
&mut warnings.shown_ipv6_secondary,
"IPv6",
false,
)
.await;
if aaaa.is_none() && purge_unknown_records {
self.delete_entries("AAAA", config).await;
}
if let Some(ip) = aaaa {
ips.insert(
"ipv6".to_string(),
LegacyIpInfo {
record_type: "AAAA".to_string(),
ip,
},
);
}
}
ips
}
async fn try_trace_urls(
&self,
urls: &[String],
shown_primary: &mut bool,
shown_secondary: &mut bool,
label: &str,
expect_v4: bool,
) -> Option<String> {
for (i, url) in urls.iter().enumerate() {
match self.client.get(url).send().await {
Ok(resp) => {
if let Some(ip) =
crate::provider::parse_trace_ip(&resp.text().await.unwrap_or_default())
{
// Validate the IP matches the expected address family
if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
if expect_v4 && !addr.is_ipv4() {
eprintln!("{label} trace returned IPv6 address, skipping");
continue;
}
if !expect_v4 && !addr.is_ipv6() {
eprintln!("{label} trace returned IPv4 address, skipping");
continue;
}
}
return Some(ip);
}
}
Err(_) => {
if i == 0 && !*shown_primary {
*shown_primary = true;
let next = if urls.len() > 1 {
", trying fallback"
} else {
""
};
eprintln!("{label} not detected via primary{next}");
} else if i > 0 && !*shown_secondary {
*shown_secondary = true;
eprintln!("{label} not detected via fallback. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.");
}
}
}
}
None
}
async fn cf_api<T: serde::de::DeserializeOwned>( async fn cf_api<T: serde::de::DeserializeOwned>(
&self, &self,
endpoint: &str, endpoint: &str,
@@ -1701,86 +1631,6 @@ mod tests {
// LegacyDdnsClient tests (internal/private struct) // LegacyDdnsClient tests (internal/private struct)
// ------------------------------------------------------- // -------------------------------------------------------
#[tokio::test]
async fn test_legacy_try_trace_urls_primary_success() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/trace"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("fl=1\nh=mock\nip=198.51.100.1\nts=0\n"),
)
.mount(&server)
.await;
let ddns = LegacyDdnsClient {
client: Client::new(),
cf_api_base: server.uri(),
ipv4_urls: vec![format!("{}/trace", server.uri())],
ipv6_urls: vec![],
dry_run: false,
};
let mut shown_primary = false;
let mut shown_secondary = false;
let result = ddns
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
.await;
assert_eq!(result, Some("198.51.100.1".to_string()));
}
#[tokio::test]
async fn test_legacy_try_trace_urls_primary_fails_fallback_succeeds() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/fallback"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("fl=1\nh=mock\nip=198.51.100.2\nts=0\n"),
)
.mount(&server)
.await;
let ddns = LegacyDdnsClient {
client: Client::new(),
cf_api_base: server.uri(),
ipv4_urls: vec![
"http://127.0.0.1:1/nonexistent".to_string(), // will fail
format!("{}/fallback", server.uri()),
],
ipv6_urls: vec![],
dry_run: false,
};
let mut shown_primary = false;
let mut shown_secondary = false;
let result = ddns
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
.await;
assert_eq!(result, Some("198.51.100.2".to_string()));
assert!(shown_primary);
}
#[tokio::test]
async fn test_legacy_try_trace_urls_all_fail() {
let ddns = LegacyDdnsClient {
client: Client::builder().timeout(Duration::from_millis(100)).build().unwrap(),
cf_api_base: String::new(),
ipv4_urls: vec![
"http://127.0.0.1:1/fail1".to_string(),
"http://127.0.0.1:1/fail2".to_string(),
],
ipv6_urls: vec![],
dry_run: false,
};
let mut shown_primary = false;
let mut shown_secondary = false;
let result = ddns
.try_trace_urls(&ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
.await;
assert!(result.is_none());
assert!(shown_primary);
assert!(shown_secondary);
}
#[tokio::test] #[tokio::test]
async fn test_legacy_cf_api_get_success() { async fn test_legacy_cf_api_get_success() {
let server = MockServer::start().await; let server = MockServer::start().await;
@@ -1795,8 +1645,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let entry = crate::config::LegacyCloudflareEntry { let entry = crate::config::LegacyCloudflareEntry {
@@ -1829,8 +1677,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let entry = crate::config::LegacyCloudflareEntry { let entry = crate::config::LegacyCloudflareEntry {
@@ -1860,8 +1706,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let entry = crate::config::LegacyCloudflareEntry { let entry = crate::config::LegacyCloudflareEntry {
@@ -1884,8 +1728,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: "http://localhost".to_string(), cf_api_base: "http://localhost".to_string(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let entry = crate::config::LegacyCloudflareEntry { let entry = crate::config::LegacyCloudflareEntry {
@@ -1916,8 +1758,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let entry = crate::config::LegacyCloudflareEntry { let entry = crate::config::LegacyCloudflareEntry {
@@ -1938,75 +1778,6 @@ mod tests {
assert!(result.is_some()); assert!(result.is_some());
} }
#[tokio::test]
async fn test_legacy_get_ips_ipv4_enabled() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/trace"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("ip=198.51.100.42\n"),
)
.mount(&server)
.await;
let ddns = LegacyDdnsClient {
client: Client::new(),
cf_api_base: server.uri(),
ipv4_urls: vec![format!("{}/trace", server.uri())],
ipv6_urls: vec![],
dry_run: false,
};
let mut warnings = LegacyWarningState::default();
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
let ips = ddns.get_ips(true, false, false, &config, &mut warnings).await;
assert!(ips.contains_key("ipv4"));
assert_eq!(ips["ipv4"].ip, "198.51.100.42");
assert_eq!(ips["ipv4"].record_type, "A");
}
#[tokio::test]
async fn test_legacy_get_ips_ipv6_enabled() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/trace6"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("ip=2001:db8::1\n"),
)
.mount(&server)
.await;
let ddns = LegacyDdnsClient {
client: Client::new(),
cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![format!("{}/trace6", server.uri())],
dry_run: false,
};
let mut warnings = LegacyWarningState::default();
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
let ips = ddns.get_ips(false, true, false, &config, &mut warnings).await;
assert!(ips.contains_key("ipv6"));
assert_eq!(ips["ipv6"].ip, "2001:db8::1");
assert_eq!(ips["ipv6"].record_type, "AAAA");
}
#[tokio::test]
async fn test_legacy_get_ips_both_disabled() {
let ddns = LegacyDdnsClient {
client: Client::new(),
cf_api_base: String::new(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false,
};
let mut warnings = LegacyWarningState::default();
let config: Vec<crate::config::LegacyCloudflareEntry> = vec![];
let ips = ddns.get_ips(false, false, false, &config, &mut warnings).await;
assert!(ips.is_empty());
}
#[tokio::test] #[tokio::test]
async fn test_legacy_commit_record_creates_new() { async fn test_legacy_commit_record_creates_new() {
let server = MockServer::start().await; let server = MockServer::start().await;
@@ -2043,8 +1814,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let ip = LegacyIpInfo { let ip = LegacyIpInfo {
@@ -2101,8 +1870,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let ip = LegacyIpInfo { let ip = LegacyIpInfo {
@@ -2145,8 +1912,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: true, dry_run: true,
}; };
let ip = LegacyIpInfo { let ip = LegacyIpInfo {
@@ -2198,8 +1963,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let ip = LegacyIpInfo { let ip = LegacyIpInfo {
@@ -2255,8 +2018,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let ip = LegacyIpInfo { let ip = LegacyIpInfo {
@@ -2306,8 +2067,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let mut ips = HashMap::new(); let mut ips = HashMap::new();
@@ -2352,8 +2111,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: false, dry_run: false,
}; };
let config = vec![crate::config::LegacyCloudflareEntry { let config = vec![crate::config::LegacyCloudflareEntry {
@@ -2386,8 +2143,6 @@ mod tests {
let ddns = LegacyDdnsClient { let ddns = LegacyDdnsClient {
client: Client::new(), client: Client::new(),
cf_api_base: server.uri(), cf_api_base: server.uri(),
ipv4_urls: vec![],
ipv6_urls: vec![],
dry_run: true, dry_run: true,
}; };
let config = vec![crate::config::LegacyCloudflareEntry { let config = vec![crate::config::LegacyCloudflareEntry {
@@ -2403,14 +2158,6 @@ mod tests {
ddns.delete_entries("A", &config).await; ddns.delete_entries("A", &config).await;
} }
#[test]
fn test_legacy_warning_state_default() {
let w = LegacyWarningState::default();
assert!(!w.shown_ipv4);
assert!(!w.shown_ipv4_secondary);
assert!(!w.shown_ipv6);
assert!(!w.shown_ipv6_secondary);
}
} }
// Legacy types for backwards compatibility // Legacy types for backwards compatibility