mirror of
https://github.com/timothymiller/cloudflare-ddns.git
synced 2026-06-20 12:35:37 -03:00
Compare commits
4 Commits
fddabc7a3d
...
bbe2ae4543
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbe2ae4543 | ||
|
|
572f94b9cf | ||
|
|
9574f67b98 | ||
|
|
ac11623127 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1257,9 +1257,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.1"
|
version = "1.52.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ Available providers:
|
|||||||
| `ipify` | 🌎 ipify.org API |
|
| `ipify` | 🌎 ipify.org API |
|
||||||
| `local` | 🏠 Local IP via system routing table (no network traffic, CGNAT-aware) |
|
| `local` | 🏠 Local IP via system routing table (no network traffic, CGNAT-aware) |
|
||||||
| `local.iface:<name>` | 🔌 IP from a specific network interface (e.g., `local.iface:eth0`) |
|
| `local.iface:<name>` | 🔌 IP from a specific network interface (e.g., `local.iface:eth0`) |
|
||||||
|
| `local.iface.stable:<name>` | 🔌 Preferred stable IPv6 address from a Linux network interface, excluding temporary/deprecated addresses |
|
||||||
| `url:<url>` | 🔗 Custom HTTP(S) endpoint that returns an IP address |
|
| `url:<url>` | 🔗 Custom HTTP(S) endpoint that returns an IP address |
|
||||||
| `literal:<ips>` | 📌 Static IP addresses (comma-separated) |
|
| `literal:<ips>` | 📌 Static IP addresses (comma-separated) |
|
||||||
| `none` | 🚫 Disable this IP type |
|
| `none` | 🚫 Disable this IP type |
|
||||||
@@ -411,7 +412,7 @@ volumes:
|
|||||||
|
|
||||||
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.
|
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`.
|
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>`, `local.iface.stable:<name>`, `url:<https://...>`, `none`.
|
||||||
|
|
||||||
Set a provider to `"none"` to disable detection for that address family (overrides `a`/`aaaa`):
|
Set a provider to `"none"` to disable detection for that address family (overrides `a`/`aaaa`):
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ DOMAINS=example.com,www.example.com
|
|||||||
|
|
||||||
# Provider for IPv4 detection (default: cloudflare.trace)
|
# Provider for IPv4 detection (default: cloudflare.trace)
|
||||||
# Options: cloudflare.trace, cloudflare.doh, ipify, local, local.iface:<name>,
|
# Options: cloudflare.trace, cloudflare.doh, ipify, local, local.iface:<name>,
|
||||||
# url:<custom-url>, literal:<ip1>,<ip2>, none
|
# local.iface.stable:<name>, url:<custom-url>, literal:<ip1>,<ip2>, none
|
||||||
# IP4_PROVIDER=cloudflare.trace
|
# IP4_PROVIDER=cloudflare.trace
|
||||||
|
|
||||||
# Provider for IPv6 detection (default: cloudflare.trace)
|
# Provider for IPv6 detection (default: cloudflare.trace)
|
||||||
|
# Use local.iface.stable:<name> on Linux to publish a stable address instead
|
||||||
|
# of temporary privacy addresses from the selected interface.
|
||||||
# IP6_PROVIDER=cloudflare.trace
|
# IP6_PROVIDER=cloudflare.trace
|
||||||
|
|
||||||
# === Scheduling ===
|
# === Scheduling ===
|
||||||
|
|||||||
167
src/provider.rs
167
src/provider.rs
@@ -1,6 +1,7 @@
|
|||||||
use crate::pp::{self, PP};
|
use crate::pp::{self, PP};
|
||||||
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use std::fs;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ pub enum ProviderType {
|
|||||||
Ipify,
|
Ipify,
|
||||||
Local,
|
Local,
|
||||||
LocalIface { interface: String },
|
LocalIface { interface: String },
|
||||||
|
StableLocalIface { interface: String },
|
||||||
CustomURL { url: String },
|
CustomURL { url: String },
|
||||||
Literal { ips: Vec<IpAddr> },
|
Literal { ips: Vec<IpAddr> },
|
||||||
None,
|
None,
|
||||||
@@ -49,6 +51,7 @@ impl ProviderType {
|
|||||||
ProviderType::Ipify => "ipify",
|
ProviderType::Ipify => "ipify",
|
||||||
ProviderType::Local => "local",
|
ProviderType::Local => "local",
|
||||||
ProviderType::LocalIface { .. } => "local.iface",
|
ProviderType::LocalIface { .. } => "local.iface",
|
||||||
|
ProviderType::StableLocalIface { .. } => "local.iface.stable",
|
||||||
ProviderType::CustomURL { .. } => "url:",
|
ProviderType::CustomURL { .. } => "url:",
|
||||||
ProviderType::Literal { .. } => "literal:",
|
ProviderType::Literal { .. } => "literal:",
|
||||||
ProviderType::None => "none",
|
ProviderType::None => "none",
|
||||||
@@ -78,6 +81,11 @@ impl ProviderType {
|
|||||||
if input == "local" {
|
if input == "local" {
|
||||||
return Ok(ProviderType::Local);
|
return Ok(ProviderType::Local);
|
||||||
}
|
}
|
||||||
|
if let Some(iface) = input.strip_prefix("local.iface.stable:") {
|
||||||
|
return Ok(ProviderType::StableLocalIface {
|
||||||
|
interface: iface.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
if let Some(iface) = input.strip_prefix("local.iface:") {
|
if let Some(iface) = input.strip_prefix("local.iface:") {
|
||||||
return Ok(ProviderType::LocalIface {
|
return Ok(ProviderType::LocalIface {
|
||||||
interface: iface.to_string(),
|
interface: iface.to_string(),
|
||||||
@@ -131,6 +139,9 @@ impl ProviderType {
|
|||||||
ProviderType::LocalIface { interface } => {
|
ProviderType::LocalIface { interface } => {
|
||||||
detect_local_iface(interface, ip_type, ppfmt)
|
detect_local_iface(interface, ip_type, ppfmt)
|
||||||
}
|
}
|
||||||
|
ProviderType::StableLocalIface { interface } => {
|
||||||
|
detect_stable_local_iface(interface, ip_type, ppfmt)
|
||||||
|
}
|
||||||
ProviderType::CustomURL { url } => {
|
ProviderType::CustomURL { url } => {
|
||||||
detect_custom_url(client, url, ip_type, timeout, ppfmt).await
|
detect_custom_url(client, url, ip_type, timeout, ppfmt).await
|
||||||
}
|
}
|
||||||
@@ -525,6 +536,105 @@ fn detect_local_iface(interface: &str, ip_type: IpType, ppfmt: &PP) -> Vec<IpAdd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Stable Local Interface ---
|
||||||
|
|
||||||
|
const IF_INET6_PATH: &str = "/proc/net/if_inet6";
|
||||||
|
const IFA_F_TEMPORARY: u32 = 0x01;
|
||||||
|
const IFA_F_DADFAILED: u32 = 0x08;
|
||||||
|
const IFA_F_DEPRECATED: u32 = 0x20;
|
||||||
|
const IFA_F_TENTATIVE: u32 = 0x40;
|
||||||
|
const IPV6_SCOPE_GLOBAL: u8 = 0x00;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct IfInet6Address {
|
||||||
|
ip: Ipv6Addr,
|
||||||
|
prefix_len: u8,
|
||||||
|
scope: u8,
|
||||||
|
flags: u32,
|
||||||
|
interface: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_stable_local_iface(interface: &str, ip_type: IpType, ppfmt: &PP) -> Vec<IpAddr> {
|
||||||
|
if ip_type == IpType::V4 {
|
||||||
|
return detect_local_iface(interface, ip_type, ppfmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contents = match fs::read_to_string(IF_INET6_PATH) {
|
||||||
|
Ok(contents) => contents,
|
||||||
|
Err(e) => {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!("Failed to read {IF_INET6_PATH} for stable IPv6 detection: {e}"),
|
||||||
|
);
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let ip = stable_ipv6_addresses_from_if_inet6(&contents, interface)
|
||||||
|
.into_iter()
|
||||||
|
.next();
|
||||||
|
if ip.is_none() {
|
||||||
|
ppfmt.warningf(
|
||||||
|
pp::EMOJI_WARNING,
|
||||||
|
&format!("No stable global IPv6 address found on interface {interface}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ip.into_iter().map(IpAddr::V6).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stable_ipv6_addresses_from_if_inet6(contents: &str, interface: &str) -> Vec<Ipv6Addr> {
|
||||||
|
let mut entries: Vec<IfInet6Address> = contents
|
||||||
|
.lines()
|
||||||
|
.filter_map(parse_if_inet6_line)
|
||||||
|
.filter(|addr| addr.interface == interface && is_stable_global_ipv6(addr))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| {
|
||||||
|
a.prefix_len
|
||||||
|
.cmp(&b.prefix_len)
|
||||||
|
.then_with(|| a.ip.to_string().cmp(&b.ip.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut ips: Vec<Ipv6Addr> = entries.into_iter().map(|addr| addr.ip).collect();
|
||||||
|
ips.dedup();
|
||||||
|
ips
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_stable_global_ipv6(addr: &IfInet6Address) -> bool {
|
||||||
|
addr.scope == IPV6_SCOPE_GLOBAL
|
||||||
|
&& IpAddr::V6(addr.ip).is_global_()
|
||||||
|
&& addr.flags & (IFA_F_TEMPORARY | IFA_F_DADFAILED | IFA_F_DEPRECATED | IFA_F_TENTATIVE)
|
||||||
|
== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_if_inet6_line(line: &str) -> Option<IfInet6Address> {
|
||||||
|
let mut fields = line.split_whitespace();
|
||||||
|
let addr_hex = fields.next()?;
|
||||||
|
let _ifindex = fields.next()?;
|
||||||
|
let prefix_hex = fields.next()?;
|
||||||
|
let scope_hex = fields.next()?;
|
||||||
|
let flags_hex = fields.next()?;
|
||||||
|
let interface = fields.next()?.to_string();
|
||||||
|
|
||||||
|
if addr_hex.len() != 32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut octets = [0_u8; 16];
|
||||||
|
for (index, octet) in octets.iter_mut().enumerate() {
|
||||||
|
let start = index * 2;
|
||||||
|
*octet = u8::from_str_radix(&addr_hex[start..start + 2], 16).ok()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(IfInet6Address {
|
||||||
|
ip: Ipv6Addr::from(octets),
|
||||||
|
prefix_len: u8::from_str_radix(prefix_hex, 16).ok()?,
|
||||||
|
scope: u8::from_str_radix(scope_hex, 16).ok()?,
|
||||||
|
flags: u32::from_str_radix(flags_hex, 16).ok()?,
|
||||||
|
interface,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- Custom URL ---
|
// --- Custom URL ---
|
||||||
|
|
||||||
async fn detect_custom_url(
|
async fn detect_custom_url(
|
||||||
@@ -695,6 +805,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_provider_parse_stable_local_iface() {
|
||||||
|
match ProviderType::parse("local.iface.stable:eth0").unwrap() {
|
||||||
|
ProviderType::StableLocalIface { interface } => {
|
||||||
|
assert_eq!(interface, "eth0");
|
||||||
|
}
|
||||||
|
_ => panic!("Expected StableLocalIface provider"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_provider_parse_custom_url() {
|
fn test_provider_parse_custom_url() {
|
||||||
match ProviderType::parse("url:https://example.com/ip").unwrap() {
|
match ProviderType::parse("url:https://example.com/ip").unwrap() {
|
||||||
@@ -1286,6 +1406,49 @@ mod tests {
|
|||||||
assert!(is_global_v6(&Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1)));
|
assert!(is_global_v6(&Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_if_inet6_line() {
|
||||||
|
let addr = parse_if_inet6_line(
|
||||||
|
"20010db8000000011111222233334444 03 40 00 00 eth0",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
addr.ip,
|
||||||
|
"2001:db8:0:1:1111:2222:3333:4444"
|
||||||
|
.parse::<Ipv6Addr>()
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(addr.prefix_len, 64);
|
||||||
|
assert_eq!(addr.scope, IPV6_SCOPE_GLOBAL);
|
||||||
|
assert_eq!(addr.flags, 0);
|
||||||
|
assert_eq!(addr.interface, "eth0");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stable_ipv6_addresses_from_if_inet6_filters_privacy_addresses() {
|
||||||
|
let contents = "\
|
||||||
|
20010db8000000015555666677778888 03 40 00 01 eth0
|
||||||
|
20010db8000000010000000000003486 03 80 00 00 eth0
|
||||||
|
20010db8000000011111222233334444 03 40 00 00 eth0
|
||||||
|
20010db8000000019999aaaabbbbcccc 03 40 00 21 eth0
|
||||||
|
fe80000000000000d399115858c872af 03 40 20 80 eth0
|
||||||
|
fdaa149d3b9900000000000000000001 0a 40 00 82 br-990e55930a86
|
||||||
|
";
|
||||||
|
|
||||||
|
let ips = stable_ipv6_addresses_from_if_inet6(contents, "eth0");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
ips,
|
||||||
|
vec![
|
||||||
|
"2001:db8:0:1:1111:2222:3333:4444"
|
||||||
|
.parse::<Ipv6Addr>()
|
||||||
|
.unwrap(),
|
||||||
|
"2001:db8:0:1::3486".parse::<Ipv6Addr>().unwrap(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- ProviderType::name ----
|
// ---- ProviderType::name ----
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1302,6 +1465,10 @@ mod tests {
|
|||||||
ProviderType::LocalIface { interface: "eth0".into() }.name(),
|
ProviderType::LocalIface { interface: "eth0".into() }.name(),
|
||||||
"local.iface"
|
"local.iface"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ProviderType::StableLocalIface { interface: "eth0".into() }.name(),
|
||||||
|
"local.iface.stable"
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ProviderType::CustomURL { url: "https://x".into() }.name(),
|
ProviderType::CustomURL { url: "https://x".into() }.name(),
|
||||||
"url:"
|
"url:"
|
||||||
|
|||||||
Reference in New Issue
Block a user