mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-03-21 22:48:57 -03:00
Replace ipnet dependency with inline CidrRange for CIDR matching
Remove the ipnet crate and implement a lightweight CidrRange struct that handles IPv4/IPv6 CIDR parsing and containment checks using bitwise masking. Adds tests for invalid prefixes and cross-family non-matching.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -114,7 +114,6 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"idna",
|
"idna",
|
||||||
"if-addrs",
|
"if-addrs",
|
||||||
"ipnet",
|
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ chrono = { version = "0.4", features = ["clock"] }
|
|||||||
url = "2"
|
url = "2"
|
||||||
idna = "1"
|
idna = "1"
|
||||||
if-addrs = "0.13"
|
if-addrs = "0.13"
|
||||||
ipnet = "2"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::pp::{self, PP};
|
use crate::pp::{self, PP};
|
||||||
use ipnet::IpNet;
|
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -7,9 +6,52 @@ use std::time::Duration;
|
|||||||
const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
const CF_IPV4_URL: &str = "https://www.cloudflare.com/ips-v4";
|
||||||
const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
const CF_IPV6_URL: &str = "https://www.cloudflare.com/ips-v6";
|
||||||
|
|
||||||
|
/// A CIDR range parsed from "address/prefix" notation.
|
||||||
|
struct CidrRange {
|
||||||
|
addr: IpAddr,
|
||||||
|
prefix_len: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CidrRange {
|
||||||
|
fn parse(s: &str) -> Option<Self> {
|
||||||
|
let (addr_str, prefix_str) = s.split_once('/')?;
|
||||||
|
let addr: IpAddr = addr_str.parse().ok()?;
|
||||||
|
let prefix_len: u8 = prefix_str.parse().ok()?;
|
||||||
|
match addr {
|
||||||
|
IpAddr::V4(_) if prefix_len > 32 => None,
|
||||||
|
IpAddr::V6(_) if prefix_len > 128 => None,
|
||||||
|
_ => Some(Self { addr, prefix_len }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(&self, ip: &IpAddr) -> bool {
|
||||||
|
match (self.addr, ip) {
|
||||||
|
(IpAddr::V4(net), IpAddr::V4(ip)) => {
|
||||||
|
let net_bits = u32::from(net);
|
||||||
|
let ip_bits = u32::from(*ip);
|
||||||
|
if self.prefix_len == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mask = !0u32 << (32 - self.prefix_len);
|
||||||
|
(net_bits & mask) == (ip_bits & mask)
|
||||||
|
}
|
||||||
|
(IpAddr::V6(net), IpAddr::V6(ip)) => {
|
||||||
|
let net_bits = u128::from(net);
|
||||||
|
let ip_bits = u128::from(*ip);
|
||||||
|
if self.prefix_len == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mask = !0u128 << (128 - self.prefix_len);
|
||||||
|
(net_bits & mask) == (ip_bits & mask)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Holds parsed Cloudflare CIDR ranges for IP filtering.
|
/// Holds parsed Cloudflare CIDR ranges for IP filtering.
|
||||||
pub struct CloudflareIpFilter {
|
pub struct CloudflareIpFilter {
|
||||||
ranges: Vec<IpNet>,
|
ranges: Vec<CidrRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CloudflareIpFilter {
|
impl CloudflareIpFilter {
|
||||||
@@ -26,13 +68,13 @@ impl CloudflareIpFilter {
|
|||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match line.parse::<IpNet>() {
|
match CidrRange::parse(line) {
|
||||||
Ok(net) => ranges.push(net),
|
Some(range) => ranges.push(range),
|
||||||
Err(e) => {
|
None => {
|
||||||
ppfmt.warningf(
|
ppfmt.warningf(
|
||||||
pp::EMOJI_WARNING,
|
pp::EMOJI_WARNING,
|
||||||
&format!(
|
&format!(
|
||||||
"Failed to parse Cloudflare IP range '{line}': {e}"
|
"Failed to parse Cloudflare IP range '{line}'"
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,14 +128,14 @@ impl CloudflareIpFilter {
|
|||||||
/// Parse ranges from raw text lines (for testing).
|
/// Parse ranges from raw text lines (for testing).
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn from_lines(lines: &str) -> Option<Self> {
|
pub fn from_lines(lines: &str) -> Option<Self> {
|
||||||
let ranges: Vec<IpNet> = lines
|
let ranges: Vec<CidrRange> = lines
|
||||||
.lines()
|
.lines()
|
||||||
.filter_map(|l| {
|
.filter_map(|l| {
|
||||||
let l = l.trim();
|
let l = l.trim();
|
||||||
if l.is_empty() {
|
if l.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
l.parse().ok()
|
CidrRange::parse(l)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -178,4 +220,18 @@ mod tests {
|
|||||||
// Just outside range (104.24.0.0)
|
// Just outside range (104.24.0.0)
|
||||||
assert!(!filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 24, 0, 0))));
|
assert!(!filter.contains(&IpAddr::V4(Ipv4Addr::new(104, 24, 0, 0))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_prefix_rejected() {
|
||||||
|
assert!(CidrRange::parse("10.0.0.0/33").is_none());
|
||||||
|
assert!(CidrRange::parse("::1/129").is_none());
|
||||||
|
assert!(CidrRange::parse("not-an-ip/24").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_v4_does_not_match_v6() {
|
||||||
|
let filter = CloudflareIpFilter::from_lines("104.16.0.0/13").unwrap();
|
||||||
|
let ip: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2606, 0x4700, 0, 0, 0, 0, 0, 1));
|
||||||
|
assert!(!filter.contains(&ip));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user