mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 14:38:56 -03:00
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.
This commit is contained in:
18
README.md
18
README.md
@@ -368,6 +368,8 @@ Some ISP provided modems only allow port forwarding over IPv4 or IPv6. Disable t
|
||||
| `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates |
|
||||
| `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records |
|
||||
| `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)
|
||||
|
||||
@@ -388,12 +390,20 @@ volumes:
|
||||
|
||||
### 🔍 IP Detection (Legacy Mode)
|
||||
|
||||
Legacy mode uses [Cloudflare's `/cdn-cgi/trace`](https://www.cloudflare.com/cdn-cgi/trace) endpoint for IP detection. To ensure the correct address family is detected on dual-stack hosts:
|
||||
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.
|
||||
|
||||
- **Primary:** Literal IP URLs (`1.0.0.1` for IPv4, `[2606:4700:4700::1001]` for IPv6) — guarantees the connection uses the correct address family
|
||||
- **Fallback:** Hostname URL (`api.cloudflare.com`) — works when literal IPs are intercepted (e.g. Cloudflare WARP or Zero Trust)
|
||||
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`.
|
||||
|
||||
Each address family uses a dedicated HTTP client bound to the correct local address (`0.0.0.0` for IPv4, `[::]` for IPv6), preventing the wrong address type from being returned on dual-stack networks.
|
||||
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:
|
||||
|
||||
|
||||
@@ -24,5 +24,7 @@
|
||||
"a": true,
|
||||
"aaaa": true,
|
||||
"purgeUnknownRecords": false,
|
||||
"ttl": 300
|
||||
"ttl": 300,
|
||||
"ip4_provider": "cloudflare.trace",
|
||||
"ip6_provider": "cloudflare.trace"
|
||||
}
|
||||
|
||||
148
src/config.rs
148
src/config.rs
@@ -27,6 +27,10 @@ pub struct LegacyConfig {
|
||||
pub purge_unknown_records: bool,
|
||||
#[serde(default = "default_ttl")]
|
||||
pub ttl: i64,
|
||||
#[serde(default)]
|
||||
pub ip4_provider: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ip6_provider: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
let auth = if let Some(entry) = legacy.cloudflare.first() {
|
||||
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())
|
||||
};
|
||||
|
||||
// Build providers
|
||||
// Build providers — ip4_provider/ip6_provider override the default cloudflare.trace
|
||||
let mut providers = HashMap::new();
|
||||
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 {
|
||||
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);
|
||||
@@ -423,7 +441,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
||||
CronSchedule::Once
|
||||
};
|
||||
|
||||
AppConfig {
|
||||
Ok(AppConfig {
|
||||
auth,
|
||||
providers,
|
||||
domains: HashMap::new(),
|
||||
@@ -447,7 +465,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
||||
legacy_mode: true,
|
||||
legacy_config: Some(legacy),
|
||||
repeat,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -583,7 +601,7 @@ pub fn load_config(dry_run: bool, repeat: bool, ppfmt: &PP) -> Result<AppConfig,
|
||||
} else {
|
||||
ppfmt.infof(pp::EMOJI_CONFIG, "Using config.json configuration");
|
||||
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,
|
||||
purge_unknown_records: false,
|
||||
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!(matches!(config.auth, Auth::Token(ref t) if t == "my-token"));
|
||||
assert!(config.providers.contains_key(&IpType::V4));
|
||||
@@ -1021,8 +1041,10 @@ mod tests {
|
||||
aaaa: true,
|
||||
purge_unknown_records: false,
|
||||
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!(config.repeat);
|
||||
assert!(config.dry_run);
|
||||
@@ -1047,12 +1069,118 @@ mod tests {
|
||||
aaaa: true,
|
||||
purge_unknown_records: false,
|
||||
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 }
|
||||
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 ---
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -301,6 +301,8 @@ mod tests {
|
||||
aaaa: false,
|
||||
purge_unknown_records: false,
|
||||
ttl: 300,
|
||||
ip4_provider: None,
|
||||
ip6_provider: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,6 +816,8 @@ mod tests {
|
||||
aaaa: false,
|
||||
purge_unknown_records: true,
|
||||
ttl: 300,
|
||||
ip4_provider: None,
|
||||
ip6_provider: None,
|
||||
};
|
||||
ddns.commit_record("198.51.100.7", "A", &config.cloudflare, 300, true)
|
||||
.await;
|
||||
@@ -913,6 +917,8 @@ mod tests {
|
||||
aaaa: false,
|
||||
purge_unknown_records: false,
|
||||
ttl: 300,
|
||||
ip4_provider: None,
|
||||
ip6_provider: None,
|
||||
};
|
||||
|
||||
ddns.commit_record("203.0.113.99", "A", &config.cloudflare, 300, false)
|
||||
|
||||
426
src/updater.rs
426
src/updater.rs
@@ -229,6 +229,11 @@ pub async fn update_once(
|
||||
}
|
||||
|
||||
/// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config).
|
||||
///
|
||||
/// 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 {
|
||||
Some(l) => l,
|
||||
@@ -243,38 +248,56 @@ async fn update_legacy(config: &AppConfig, ppfmt: &PP) -> bool {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client,
|
||||
cf_api_base: "https://api.cloudflare.com/client/v4".to_string(),
|
||||
// Literal IPs primary (guarantees correct address family on dual-stack hosts),
|
||||
// hostname fallback (works when literal IPs are intercepted by WARP/Zero Trust).
|
||||
ipv4_urls: vec![
|
||||
"https://1.0.0.1/cdn-cgi/trace".to_string(),
|
||||
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
||||
],
|
||||
ipv6_urls: vec![
|
||||
"https://[2606:4700:4700::1001]/cdn-cgi/trace".to_string(),
|
||||
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
||||
],
|
||||
detection_timeout: config.detection_timeout,
|
||||
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 mut ips = ddns
|
||||
.get_ips(
|
||||
legacy.a,
|
||||
legacy.aaaa,
|
||||
legacy.purge_unknown_records,
|
||||
&legacy.cloudflare,
|
||||
&mut warnings,
|
||||
)
|
||||
.await;
|
||||
let mut ips = HashMap::new();
|
||||
|
||||
for (ip_type, provider) in &config.providers {
|
||||
ppfmt.infof(
|
||||
pp::EMOJI_DETECT,
|
||||
&format!("Detecting {} via {}", ip_type.describe(), provider.name()),
|
||||
);
|
||||
let detected = provider
|
||||
.detect_ips(&detection_client, *ip_type, config.detection_timeout, ppfmt)
|
||||
.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 {
|
||||
let detection_client = Client::builder()
|
||||
.timeout(config.detection_timeout)
|
||||
.build()
|
||||
.unwrap_or_default();
|
||||
if let Some(cf_filter) =
|
||||
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||
{
|
||||
@@ -358,162 +381,13 @@ pub struct LegacyIpInfo {
|
||||
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 {
|
||||
client: Client,
|
||||
cf_api_base: String,
|
||||
ipv4_urls: Vec<String>,
|
||||
ipv6_urls: Vec<String>,
|
||||
detection_timeout: Duration,
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
/// Return a Host header override for literal-IP trace URLs so TLS SNI works.
|
||||
fn legacy_host_override(url: &str) -> Option<&'static str> {
|
||||
if url.contains("1.0.0.1") || url.contains("2606:4700:4700::1001") {
|
||||
Some("one.one.one.one")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// Use an IPv4-bound client so the trace endpoint sees the correct address family.
|
||||
let v4_client = crate::provider::build_split_client(IpType::V4, self.detection_timeout);
|
||||
let a = self
|
||||
.try_trace_urls(
|
||||
&v4_client,
|
||||
&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 {
|
||||
// Use an IPv6-bound client so the trace endpoint sees the correct address family.
|
||||
let v6_client = crate::provider::build_split_client(IpType::V6, self.detection_timeout);
|
||||
let aaaa = self
|
||||
.try_trace_urls(
|
||||
&v6_client,
|
||||
&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,
|
||||
trace_client: &Client,
|
||||
urls: &[String],
|
||||
shown_primary: &mut bool,
|
||||
shown_secondary: &mut bool,
|
||||
label: &str,
|
||||
expect_v4: bool,
|
||||
) -> Option<String> {
|
||||
for (i, url) in urls.iter().enumerate() {
|
||||
let mut req = trace_client.get(url);
|
||||
if let Some(host) = legacy_host_override(url) {
|
||||
req = req.header("Host", host);
|
||||
}
|
||||
match req.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>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
@@ -1757,89 +1631,6 @@ mod tests {
|
||||
// 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![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let mut shown_primary = false;
|
||||
let mut shown_secondary = false;
|
||||
let result = ddns
|
||||
.try_trace_urls(&ddns.client, &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![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let mut shown_primary = false;
|
||||
let mut shown_secondary = false;
|
||||
let result = ddns
|
||||
.try_trace_urls(&ddns.client, &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![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let mut shown_primary = false;
|
||||
let mut shown_secondary = false;
|
||||
let result = ddns
|
||||
.try_trace_urls(&ddns.client, &ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
||||
.await;
|
||||
assert!(result.is_none());
|
||||
assert!(shown_primary);
|
||||
assert!(shown_secondary);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_legacy_cf_api_get_success() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -1854,9 +1645,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let entry = crate::config::LegacyCloudflareEntry {
|
||||
@@ -1889,9 +1677,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let entry = crate::config::LegacyCloudflareEntry {
|
||||
@@ -1921,9 +1706,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let entry = crate::config::LegacyCloudflareEntry {
|
||||
@@ -1946,9 +1728,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: "http://localhost".to_string(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let entry = crate::config::LegacyCloudflareEntry {
|
||||
@@ -1979,9 +1758,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let entry = crate::config::LegacyCloudflareEntry {
|
||||
@@ -2002,80 +1778,6 @@ mod tests {
|
||||
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![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
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() {
|
||||
// Test the IPv6 trace URL parsing via try_trace_urls directly, since
|
||||
// get_ips creates an IPv6-bound client that can't reach IPv4-only mock servers.
|
||||
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())],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let mut shown_primary = false;
|
||||
let mut shown_secondary = false;
|
||||
let result = ddns
|
||||
.try_trace_urls(&ddns.client, &ddns.ipv6_urls, &mut shown_primary, &mut shown_secondary, "IPv6", false)
|
||||
.await;
|
||||
assert_eq!(result, Some("2001:db8::1".to_string()));
|
||||
}
|
||||
|
||||
#[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![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
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]
|
||||
async fn test_legacy_commit_record_creates_new() {
|
||||
let server = MockServer::start().await;
|
||||
@@ -2112,9 +1814,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2171,9 +1870,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2216,9 +1912,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: true,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2270,9 +1963,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2328,9 +2018,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2380,9 +2067,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let mut ips = HashMap::new();
|
||||
@@ -2427,9 +2111,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let config = vec![crate::config::LegacyCloudflareEntry {
|
||||
@@ -2462,9 +2143,6 @@ mod tests {
|
||||
let ddns = LegacyDdnsClient {
|
||||
client: Client::new(),
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: true,
|
||||
};
|
||||
let config = vec![crate::config::LegacyCloudflareEntry {
|
||||
@@ -2480,14 +2158,6 @@ mod tests {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user