mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 22:48:57 -03:00
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.
This commit is contained in:
105
src/updater.rs
105
src/updater.rs
@@ -229,7 +229,7 @@ pub async fn update_once(
|
||||
}
|
||||
|
||||
/// Run legacy mode update (using the original cloudflare-ddns logic with zone_id-based config).
|
||||
async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool {
|
||||
async fn update_legacy(config: &AppConfig, ppfmt: &PP) -> bool {
|
||||
let legacy = match &config.legacy_config {
|
||||
Some(l) => l,
|
||||
None => return false,
|
||||
@@ -243,20 +243,23 @@ 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://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
||||
"https://1.0.0.1/cdn-cgi/trace".to_string(),
|
||||
"https://api.cloudflare.com/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(),
|
||||
"https://api.cloudflare.com/cdn-cgi/trace".to_string(),
|
||||
],
|
||||
detection_timeout: config.detection_timeout,
|
||||
dry_run: config.dry_run,
|
||||
};
|
||||
|
||||
let mut warnings = LegacyWarningState::default();
|
||||
|
||||
let ips = ddns
|
||||
let mut ips = ddns
|
||||
.get_ips(
|
||||
legacy.a,
|
||||
legacy.aaaa,
|
||||
@@ -266,6 +269,38 @@ async fn update_legacy(config: &AppConfig, _ppfmt: &PP) -> bool {
|
||||
)
|
||||
.await;
|
||||
|
||||
// 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
|
||||
{
|
||||
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(
|
||||
&ips,
|
||||
&legacy.cloudflare,
|
||||
@@ -346,9 +381,19 @@ struct LegacyDdnsClient {
|
||||
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,
|
||||
@@ -361,8 +406,11 @@ impl LegacyDdnsClient {
|
||||
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,
|
||||
@@ -385,8 +433,11 @@ impl LegacyDdnsClient {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -413,6 +464,7 @@ impl LegacyDdnsClient {
|
||||
|
||||
async fn try_trace_urls(
|
||||
&self,
|
||||
trace_client: &Client,
|
||||
urls: &[String],
|
||||
shown_primary: &mut bool,
|
||||
shown_secondary: &mut bool,
|
||||
@@ -420,7 +472,11 @@ impl LegacyDdnsClient {
|
||||
expect_v4: bool,
|
||||
) -> Option<String> {
|
||||
for (i, url) in urls.iter().enumerate() {
|
||||
match self.client.get(url).send().await {
|
||||
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())
|
||||
@@ -1718,12 +1774,13 @@ mod tests {
|
||||
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.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
||||
.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()));
|
||||
}
|
||||
@@ -1748,12 +1805,13 @@ mod tests {
|
||||
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.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
||||
.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);
|
||||
@@ -1769,12 +1827,13 @@ mod tests {
|
||||
"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.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
||||
.try_trace_urls(&ddns.client, &ddns.ipv4_urls, &mut shown_primary, &mut shown_secondary, "IPv4", true)
|
||||
.await;
|
||||
assert!(result.is_none());
|
||||
assert!(shown_primary);
|
||||
@@ -1797,6 +1856,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -1831,6 +1891,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -1862,6 +1923,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -1886,6 +1948,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -1918,6 +1981,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -1955,6 +2019,7 @@ mod tests {
|
||||
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();
|
||||
@@ -1967,6 +2032,8 @@ mod tests {
|
||||
|
||||
#[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"))
|
||||
@@ -1982,14 +2049,15 @@ mod tests {
|
||||
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 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");
|
||||
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]
|
||||
@@ -1999,6 +2067,7 @@ mod tests {
|
||||
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();
|
||||
@@ -2045,6 +2114,7 @@ mod tests {
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2103,6 +2173,7 @@ mod tests {
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2147,6 +2218,7 @@ mod tests {
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: true,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2200,6 +2272,7 @@ mod tests {
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2257,6 +2330,7 @@ mod tests {
|
||||
cf_api_base: server.uri(),
|
||||
ipv4_urls: vec![],
|
||||
ipv6_urls: vec![],
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
dry_run: false,
|
||||
};
|
||||
let ip = LegacyIpInfo {
|
||||
@@ -2308,6 +2382,7 @@ mod tests {
|
||||
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();
|
||||
@@ -2354,6 +2429,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -2388,6 +2464,7 @@ mod tests {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user