mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 14:38:56 -03:00
Add REJECT_CLOUDFLARE_IPS flag to filter out Cloudflare-owned IPs from
DNS updates IP detection providers can sometimes return a Cloudflare anycast IP instead of the user's real public IP, causing incorrect DNS updates. When REJECT_CLOUDFLARE_IPS=true, detected IPs are checked against Cloudflare's published IP ranges (ips-v4/ips-v6) and rejected if they match.
This commit is contained in:
228
Cargo.lock
generated
228
Cargo.lock
generated
@@ -20,6 +20,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
@@ -68,9 +74,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.56"
|
||||
version = "1.2.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -103,11 +109,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cloudflare-ddns"
|
||||
version = "2.0.2"
|
||||
version = "2.0.4"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"idna",
|
||||
"if-addrs",
|
||||
"ipnet",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
@@ -187,6 +194,12 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
@@ -306,11 +319,24 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"r-efi 5.3.0",
|
||||
"wasip2",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -330,12 +356,27 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
@@ -555,6 +596,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -593,7 +640,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -634,6 +683,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
@@ -702,9 +757,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
@@ -742,6 +797,16 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -821,6 +886,12 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.2"
|
||||
@@ -997,6 +1068,12 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -1140,7 +1217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -1178,9 +1255,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
|
||||
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
@@ -1317,6 +1394,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -1365,6 +1448,15 @@ dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.114"
|
||||
@@ -1424,6 +1516,40 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.91"
|
||||
@@ -1705,6 +1831,88 @@ name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cloudflare-ddns"
|
||||
version = "2.0.3"
|
||||
version = "2.0.4"
|
||||
edition = "2021"
|
||||
description = "Access your home network remotely via a custom domain name without a static IP"
|
||||
license = "GPL-3.0"
|
||||
@@ -15,6 +15,7 @@ chrono = { version = "0.4", features = ["clock"] }
|
||||
url = "2"
|
||||
idna = "1"
|
||||
if-addrs = "0.13"
|
||||
ipnet = "2"
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
181
src/cf_ip_filter.rs
Normal file
181
src/cf_ip_filter.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::pp::{self, PP};
|
||||
use ipnet::IpNet;
|
||||
use reqwest::Client;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
||||
const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
||||
|
||||
/// Holds parsed Cloudflare CIDR ranges for IP filtering.
|
||||
pub struct CloudflareIpFilter {
|
||||
ranges: Vec<IpNet>,
|
||||
}
|
||||
|
||||
impl CloudflareIpFilter {
|
||||
/// Fetch Cloudflare IP ranges from their published URLs and parse them.
|
||||
pub async fn fetch(client: &Client, timeout: Duration, ppfmt: &PP) -> Option<Self> {
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for url in [CF_IPV4_URL, CF_IPV6_URL] {
|
||||
match client.get(url).timeout(timeout).send().await {
|
||||
Ok(resp) if resp.status().is_success() => match resp.text().await {
|
||||
Ok(body) => {
|
||||
for line in body.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match line.parse::<IpNet>() {
|
||||
Ok(net) => ranges.push(net),
|
||||
Err(e) => {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!(
|
||||
"Failed to parse Cloudflare IP range '{line}': {e}"
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!("Failed to read Cloudflare IP ranges from {url}: {e}"),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
},
|
||||
Ok(resp) => {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!(
|
||||
"Failed to fetch Cloudflare IP ranges from {url}: HTTP {}",
|
||||
resp.status()
|
||||
),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Err(e) => {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!("Failed to fetch Cloudflare IP ranges from {url}: {e}"),
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ranges.is_empty() {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
"No Cloudflare IP ranges loaded; skipping filter",
|
||||
);
|
||||
return None;
|
||||
}
|
||||
|
||||
ppfmt.infof(
|
||||
pp::EMOJI_DETECT,
|
||||
&format!("Loaded {} Cloudflare IP ranges for filtering", ranges.len()),
|
||||
);
|
||||
|
||||
Some(Self { ranges })
|
||||
}
|
||||
|
||||
/// Parse ranges from raw text lines (for testing).
|
||||
#[cfg(test)]
|
||||
pub fn from_lines(lines: &str) -> Option<Self> {
|
||||
let ranges: Vec<IpNet> = lines
|
||||
.lines()
|
||||
.filter_map(|l| {
|
||||
let l = l.trim();
|
||||
if l.is_empty() {
|
||||
None
|
||||
} else {
|
||||
l.parse().ok()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if ranges.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Self { ranges })
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an IP address falls within any Cloudflare range.
|
||||
pub fn contains(&self, ip: &IpAddr) -> bool {
|
||||
self.ranges.iter().any(|net| net.contains(ip))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
const SAMPLE_RANGES: &str = "\
|
||||
173.245.48.0/20
|
||||
103.21.244.0/22
|
||||
103.22.200.0/22
|
||||
104.16.0.0/13
|
||||
2400:cb00::/32
|
||||
2606:4700::/32
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_parse_ranges() {
|
||||
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||
assert_eq!(filter.ranges.len(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_cloudflare_ipv4() {
|
||||
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||
// 104.16.0.1 is within 104.16.0.0/13
|
||||
let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(104, 16, 0, 1));
|
||||
assert!(filter.contains(&ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_non_cloudflare_ipv4() {
|
||||
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||
// 203.0.113.42 is a documentation IP, not Cloudflare
|
||||
let ip: IpAddr = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 42));
|
||||
assert!(!filter.contains(&ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_cloudflare_ipv6() {
|
||||
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||
// 2606:4700::1 is within 2606:4700::/32
|
||||
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 1));
|
||||
assert!(filter.contains(&ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_non_cloudflare_ipv6() {
|
||||
let filter = CloudflareIpFilter::from_lines(SAMPLE_RANGES).unwrap();
|
||||
// 2001:db8::1 is a documentation address, not Cloudflare
|
||||
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
|
||||
assert!(!filter.contains(&ip));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_input() {
|
||||
assert!(CloudflareIpFilter::from_lines("").is_none());
|
||||
assert!(CloudflareIpFilter::from_lines(" \n \n").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_of_range() {
|
||||
let filter = CloudflareIpFilter::from_lines("104.16.0.0/13").unwrap();
|
||||
// First IP in range
|
||||
assert!(filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 16, 0, 0))));
|
||||
// Last IP in range (104.23.255.255)
|
||||
assert!(filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 23, 255, 255))));
|
||||
// Just outside range (104.24.0.0)
|
||||
assert!(!filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 24, 0, 0))));
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ pub struct AppConfig {
|
||||
pub managed_waf_comment_regex: Option<regex::Regex>,
|
||||
pub detection_timeout: Duration,
|
||||
pub update_timeout: Duration,
|
||||
pub reject_cloudflare_ips: bool,
|
||||
pub dry_run: bool,
|
||||
pub emoji: bool,
|
||||
pub quiet: bool,
|
||||
@@ -439,6 +440,7 @@ fn legacy_to_app_config(legacy: LegacyConfig, dry_run: bool, repeat: bool) -> Ap
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run,
|
||||
emoji: false,
|
||||
quiet: false,
|
||||
@@ -509,6 +511,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
||||
|
||||
let emoji = getenv_bool("EMOJI", true);
|
||||
let quiet = getenv_bool("QUIET", false);
|
||||
let reject_cloudflare_ips = getenv_bool("REJECT_CLOUDFLARE_IPS", false);
|
||||
|
||||
// Validate: must have at least one update target
|
||||
if domains.is_empty() && waf_lists.is_empty() {
|
||||
@@ -559,6 +562,7 @@ pub fn load_env_config(ppfmt: &PP) -> Result<AppConfig, String> {
|
||||
managed_waf_comment_regex,
|
||||
detection_timeout,
|
||||
update_timeout,
|
||||
reject_cloudflare_ips,
|
||||
dry_run: false, // Set later from CLI args
|
||||
emoji,
|
||||
quiet,
|
||||
@@ -659,6 +663,10 @@ pub fn print_config_summary(config: &AppConfig, ppfmt: &PP) {
|
||||
inner.infof("", "Delete on stop: enabled");
|
||||
}
|
||||
|
||||
if config.reject_cloudflare_ips {
|
||||
inner.infof("", "Reject Cloudflare IPs: enabled");
|
||||
}
|
||||
|
||||
if let Some(ref comment) = config.record_comment {
|
||||
inner.infof("", &format!("Record comment: {comment}"));
|
||||
}
|
||||
@@ -1190,6 +1198,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run: false,
|
||||
emoji: false,
|
||||
quiet: false,
|
||||
@@ -1223,6 +1232,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run: false,
|
||||
emoji: false,
|
||||
quiet: false,
|
||||
@@ -1881,6 +1891,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run: false,
|
||||
emoji: false,
|
||||
quiet: false,
|
||||
@@ -1916,6 +1927,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run: false,
|
||||
emoji: false,
|
||||
quiet: true,
|
||||
@@ -1948,6 +1960,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(30),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run: false,
|
||||
emoji: false,
|
||||
quiet: false,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod cf_ip_filter;
|
||||
mod cloudflare;
|
||||
mod config;
|
||||
mod domain;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::cf_ip_filter::CloudflareIpFilter;
|
||||
use crate::cloudflare::{CloudflareHandle, SetResult};
|
||||
use crate::config::{AppConfig, LegacyCloudflareEntry, LegacySubdomainEntry};
|
||||
use crate::domain::make_fqdn;
|
||||
@@ -65,6 +66,49 @@ pub async fn update_once(
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out Cloudflare IPs if enabled
|
||||
if config.reject_cloudflare_ips {
|
||||
if let Some(cf_filter) =
|
||||
CloudflareIpFilter::fetch(&detection_client, config.detection_timeout, ppfmt).await
|
||||
{
|
||||
for (ip_type, ips) in detected_ips.iter_mut() {
|
||||
let before_count = ips.len();
|
||||
ips.retain(|ip| {
|
||||
if cf_filter.contains(ip) {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!(
|
||||
"Rejected {ip}: matches Cloudflare IP range ({})",
|
||||
ip_type.describe()
|
||||
),
|
||||
);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
if ips.is_empty() && before_count > 0 {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
&format!(
|
||||
"All detected {} addresses were Cloudflare IPs; skipping updates for this type",
|
||||
ip_type.describe()
|
||||
),
|
||||
);
|
||||
messages.push(Message::new_fail(&format!(
|
||||
"All {} addresses rejected (Cloudflare IPs)",
|
||||
ip_type.describe()
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ppfmt.warningf(
|
||||
pp::EMOJI_WARNING,
|
||||
"Could not fetch Cloudflare IP ranges; skipping filter",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -693,6 +737,7 @@ mod tests {
|
||||
managed_waf_comment_regex: None,
|
||||
detection_timeout: Duration::from_secs(5),
|
||||
update_timeout: Duration::from_secs(5),
|
||||
reject_cloudflare_ips: false,
|
||||
dry_run,
|
||||
emoji: false,
|
||||
quiet: true,
|
||||
|
||||
Reference in New Issue
Block a user