8 Commits

Author SHA1 Message Date
Timothy Miller
54ca4a5eae Bump version to 2.0.3 and update GitHub Actions to Node.js 24
Update all Docker GitHub Actions to their latest major versions to
  resolve Node.js 20 deprecation warnings ahead of the June 2026 cutoff.
2026-03-18 19:01:50 -04:00
Timothy Miller
94ce10fccc Only set Host header for literal-IP trace URLs
The fallback hostname-based URL and custom URLs resolve correctly
without a Host override, so restrict the header to the cases that
need it (direct IP connections to 1.1.1.1 / [2606:4700:4700::1111]).
2026-03-18 18:19:55 -04:00
Timothy Miller
7e96816740 Merge pull request #240 from masterwishx/dev-test
Fix proxyIP + Notify
2026-03-18 16:34:28 -04:00
DaRK AnGeL
8a4b57c163 undo FIX: remove duplicates so CloudflareHandle::set_ips sees stable input
Signed-off-by: DaRK AnGeL <28630321+masterwishx@users.noreply.github.com>
2026-03-17 10:10:00 +02:00
DaRK AnGeL
3c7072f4b6 Merge branch 'master' of https://github.com/masterwishx/cloudflare-ddns 2026-03-17 10:05:15 +02:00
DaRK AnGeL
3d796d470c Deduplicate IPs before DNS record update
Remove duplicate IPs before updating DNS records to ensure stable input.

Signed-off-by: DaRK AnGeL <28630321+masterwishx@users.noreply.github.com>
2026-03-17 10:04:20 +02:00
DaRK AnGeL
36bdbea568 Deduplicate IPs before DNS record update
Remove duplicate IPs before updating DNS records to ensure stable input.
2026-03-16 20:28:26 +02:00
DaRK AnGeL
6085ba0cc2 Add Host header to fetch_trace_ip function 2026-03-16 09:02:10 +02:00
4 changed files with 35 additions and 22 deletions

View File

@@ -15,14 +15,14 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
- name: Login to DockerHub - name: Login to DockerHub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -35,7 +35,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v6
with: with:
images: timothyjmiller/cloudflare-ddns images: timothyjmiller/cloudflare-ddns
tags: | tags: |
@@ -46,7 +46,7 @@ jobs:
type=raw,enable=${{ github.ref == 'refs/heads/master' }},value=${{ steps.version.outputs.version }} type=raw,enable=${{ github.ref == 'refs/heads/master' }},value=${{ steps.version.outputs.version }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v7
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "cloudflare-ddns" name = "cloudflare-ddns"
version = "2.0.2" version = "2.0.3"
edition = "2021" edition = "2021"
description = "Access your home network remotely via a custom domain name without a static IP" description = "Access your home network remotely via a custom domain name without a static IP"
license = "GPL-3.0" license = "GPL-3.0"

View File

@@ -164,13 +164,17 @@ pub fn parse_trace_ip(body: &str) -> Option<String> {
None None
} }
async fn fetch_trace_ip(client: &Client, url: &str, timeout: Duration) -> Option<IpAddr> { async fn fetch_trace_ip(
let resp = client client: &Client,
.get(url) url: &str,
.timeout(timeout) timeout: Duration,
.send() host_override: Option<&str>,
.await ) -> Option<IpAddr> {
.ok()?; let mut req = client.get(url).timeout(timeout);
if let Some(host) = host_override {
req = req.header("Host", host);
}
let resp = req.send().await.ok()?;
let body = resp.text().await.ok()?; let body = resp.text().await.ok()?;
let ip_str = parse_trace_ip(&body)?; let ip_str = parse_trace_ip(&body)?;
ip_str.parse::<IpAddr>().ok() ip_str.parse::<IpAddr>().ok()
@@ -202,7 +206,7 @@ async fn detect_cloudflare_trace(
let client = build_split_client(ip_type, timeout); let client = build_split_client(ip_type, timeout);
if let Some(url) = custom_url { if let Some(url) = custom_url {
if let Some(ip) = fetch_trace_ip(&client, url, timeout).await { if let Some(ip) = fetch_trace_ip(&client, url, timeout, None).await {
if validate_detected_ip(&ip, ip_type, ppfmt) { if validate_detected_ip(&ip, ip_type, ppfmt) {
return vec![ip]; return vec![ip];
} }
@@ -220,7 +224,7 @@ async fn detect_cloudflare_trace(
}; };
// Try primary (literal IP — guarantees correct address family) // Try primary (literal IP — guarantees correct address family)
if let Some(ip) = fetch_trace_ip(&client, primary, timeout).await { if let Some(ip) = fetch_trace_ip(&client, primary, timeout, Some("one.one.one.one")).await {
if validate_detected_ip(&ip, ip_type, ppfmt) { if validate_detected_ip(&ip, ip_type, ppfmt) {
return vec![ip]; return vec![ip];
} }
@@ -231,7 +235,7 @@ async fn detect_cloudflare_trace(
); );
// Try fallback (hostname-based — works when literal IPs are intercepted by WARP/Zero Trust) // Try fallback (hostname-based — works when literal IPs are intercepted by WARP/Zero Trust)
if let Some(ip) = fetch_trace_ip(&client, CF_TRACE_FALLBACK, timeout).await { if let Some(ip) = fetch_trace_ip(&client, CF_TRACE_FALLBACK, timeout, None).await {
if validate_detected_ip(&ip, ip_type, ppfmt) { if validate_detected_ip(&ip, ip_type, ppfmt) {
return vec![ip]; return vec![ip];
} }

View File

@@ -24,6 +24,7 @@ pub async fn update_once(
let mut all_ok = true; let mut all_ok = true;
let mut messages = Vec::new(); let mut messages = Vec::new();
let mut notify = false; // NEW: track meaningful events
if config.legacy_mode { if config.legacy_mode {
all_ok = update_legacy(config, ppfmt).await; all_ok = update_legacy(config, ppfmt).await;
@@ -108,6 +109,7 @@ pub async fn update_once(
match result { match result {
SetResult::Updated => { SetResult::Updated => {
notify = true; // NEW
let ip_strs: Vec<String> = ips.iter().map(|ip| ip.to_string()).collect(); let ip_strs: Vec<String> = ips.iter().map(|ip| ip.to_string()).collect();
messages.push(Message::new_ok(&format!( messages.push(Message::new_ok(&format!(
"Updated {domain_str} -> {}", "Updated {domain_str} -> {}",
@@ -115,6 +117,7 @@ pub async fn update_once(
))); )));
} }
SetResult::Failed => { SetResult::Failed => {
notify = true; // NEW
all_ok = false; all_ok = false;
messages.push(Message::new_fail(&format!( messages.push(Message::new_fail(&format!(
"Failed to update {domain_str}" "Failed to update {domain_str}"
@@ -147,12 +150,14 @@ pub async fn update_once(
match result { match result {
SetResult::Updated => { SetResult::Updated => {
notify = true; // NEW
messages.push(Message::new_ok(&format!( messages.push(Message::new_ok(&format!(
"Updated WAF list {}", "Updated WAF list {}",
waf_list.describe() waf_list.describe()
))); )));
} }
SetResult::Failed => { SetResult::Failed => {
notify = true; // NEW
all_ok = false; all_ok = false;
messages.push(Message::new_fail(&format!( messages.push(Message::new_fail(&format!(
"Failed to update WAF list {}", "Failed to update WAF list {}",
@@ -164,13 +169,17 @@ pub async fn update_once(
} }
} }
// Send heartbeat // Send heartbeat ONLY if something meaningful happened
let heartbeat_msg = Message::merge(messages.clone()); if notify {
heartbeat.ping(&heartbeat_msg).await; let heartbeat_msg = Message::merge(messages.clone());
heartbeat.ping(&heartbeat_msg).await;
}
// Send notifications // Send notifications ONLY when IP changed or failed
let notifier_msg = Message::merge(messages); if notify {
notifier.send(&notifier_msg).await; let notifier_msg = Message::merge(messages);
notifier.send(&notifier_msg).await;
}
all_ok all_ok
} }