Files
cloudflare_ddns/README.md
Timothy Miller b1a2fa7af3 Migrate cloudflare-ddns to Rust
Add Cargo.toml, Cargo.lock and a full src/ tree with modules and tests
Update Dockerfile to build a Rust release binary and simplify CI/publish
Remove legacy Python script, requirements.txt, and startup helper
Switch .gitignore to Rust artifacts; update Dependabot and workflows to
cargo
Add .env example, docker-compose env, and update README and VSCode
settings

Remove the old Python implementation and requirements; add a Rust
implementation with Cargo.toml/Cargo.lock and full src/ modules, tests,
and notifier/heartbeat support. Update Dockerfile, build/publish
scripts, dependabot and workflows, README, and provide env-based
docker-compose and .env examples.
2026-03-10 01:21:21 -04:00

16 KiB
Executable File
Raw Blame History

Cloudflare DDNS

🌍 Cloudflare DDNS

Access your home network remotely via a custom domain name without a static IP!

A feature-complete dynamic DNS client for Cloudflare, written in Rust. Configure everything with environment variables. Supports notifications, heartbeat monitoring, WAF list management, flexible scheduling, and more.

Features

  • 🔍 Multiple IP detection providers — Cloudflare Trace, Cloudflare DNS-over-HTTPS, ipify, local interface, custom URL, or static IPs
  • 📡 IPv4 and IPv6 — Full dual-stack support with independent provider configuration
  • 🌐 Multiple domains and zones — Update any number of domains across multiple Cloudflare zones
  • 🃏 Wildcard domains — Support for *.example.com records
  • 🌍 Internationalized domain names — Full IDN/punycode support (e.g. münchen.de)
  • 🛡️ WAF list management — Automatically update Cloudflare WAF IP lists
  • 🔔 Notifications — Shoutrrr-compatible notifications (Discord, Slack, Telegram, Gotify, Pushover, generic webhooks)
  • 💓 Heartbeat monitoring — Healthchecks.io and Uptime Kuma integration
  • ⏱️ Cron scheduling — Flexible update intervals via cron expressions
  • 🧪 Dry-run mode — Preview changes without modifying DNS records
  • 🧹 Graceful shutdown — Signal handling (SIGINT/SIGTERM) with optional DNS record cleanup
  • 💬 Record comments — Tag managed records with comments for identification
  • 🎯 Managed record regex — Control which records the tool manages via regex matching
  • 🎨 Pretty output with emoji — Configurable emoji and verbosity levels
  • 🔒 Zero-log IP detection — Uses Cloudflare's cdn-cgi/trace by default
  • 🏠 CGNAT-aware local detection — Filters out shared address space (100.64.0.0/10) and private ranges
  • 🤏 Tiny static binary — Small Docker image, zero runtime dependencies

🚀 Quick Start

docker run -d \
  --name cloudflare-ddns \
  --restart unless-stopped \
  --network host \
  -e CLOUDFLARE_API_TOKEN=your-api-token \
  -e DOMAINS=example.com,www.example.com \
  timothyjmiller/cloudflare-ddns:latest

That's it. The container detects your public IP and updates the DNS records for your domains every 5 minutes.

⚠️ --network host is required to detect IPv6 addresses. If you only need IPv4, you can omit it and set IP6_PROVIDER=none.

🔑 Authentication

Variable Description
CLOUDFLARE_API_TOKEN API token with "Edit DNS" capability
CLOUDFLARE_API_TOKEN_FILE Path to a file containing the API token (Docker secrets compatible)

To generate an API token, go to your Cloudflare Profile and create a token capable of Edit DNS.

🌐 Domains

Variable Description
DOMAINS Comma-separated list of domains to update for both IPv4 and IPv6
IP4_DOMAINS Comma-separated list of IPv4-only domains
IP6_DOMAINS Comma-separated list of IPv6-only domains

Wildcard domains are supported: *.example.com

At least one of DOMAINS, IP4_DOMAINS, IP6_DOMAINS, or WAF_LISTS must be set.

🔍 IP Detection Providers

Variable Default Description
IP4_PROVIDER cloudflare.trace IPv4 detection method
IP6_PROVIDER cloudflare.trace IPv6 detection method

Available providers:

Provider Description
cloudflare.trace 🔒 Cloudflare's /cdn-cgi/trace endpoint (default, zero-log)
cloudflare.doh 🌐 Cloudflare DNS-over-HTTPS (whoami.cloudflare TXT query)
ipify 🌎 ipify.org API
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)
url:<url> 🔗 Custom HTTP(S) endpoint that returns an IP address
literal:<ips> 📌 Static IP addresses (comma-separated)
none 🚫 Disable this IP type

⏱️ Scheduling

Variable Default Description
UPDATE_CRON @every 5m Update schedule
UPDATE_ON_START true Run an update immediately on startup
DELETE_ON_STOP false Delete managed DNS records on shutdown

Schedule formats:

  • @every 5m — Every 5 minutes
  • @every 1h — Every hour
  • @every 30s — Every 30 seconds
  • @once — Run once and exit

When UPDATE_CRON=@once, UPDATE_ON_START must be true and DELETE_ON_STOP must be false.

📝 DNS Record Settings

Variable Default Description
TTL 1 (auto) DNS record TTL in seconds (1=auto, or 30-86400)
PROXIED false Expression controlling which domains are proxied through Cloudflare
RECORD_COMMENT (empty) Comment attached to managed DNS records
MANAGED_RECORDS_COMMENT_REGEX (empty) Regex to identify which records are managed (empty = all)

The PROXIED variable supports boolean expressions:

Expression Meaning
true ☁️ Proxy all domains
false 🔓 Don't proxy any domains
is(example.com) 🎯 Only proxy example.com
sub(cdn.example.com) 🌳 Proxy cdn.example.com and its subdomains
is(a.com) || is(b.com) 🔀 Proxy a.com or b.com
!is(vpn.example.com) 🚫 Proxy everything except vpn.example.com

Operators: is(), sub(), !, &&, ||, ()

🛡️ WAF Lists

Variable Default Description
WAF_LISTS (empty) Comma-separated WAF lists in account-id/list-name format
WAF_LIST_DESCRIPTION (empty) Description for managed WAF lists
WAF_LIST_ITEM_COMMENT (empty) Comment for WAF list items
MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX (empty) Regex to identify managed WAF list items

WAF list names must match the pattern [a-z0-9_]+.

🔔 Notifications (Shoutrrr)

Variable Description
SHOUTRRR Newline-separated list of notification service URLs

Supported services:

Service URL format
💬 Discord discord://token@webhook-id
📨 Slack slack://token-a/token-b/token-c
✈️ Telegram telegram://bot-token@telegram?chats=chat-id
📡 Gotify gotify://host/path?token=app-token
📲 Pushover pushover://user-key@api-token
🌐 Generic webhook generic://host/path or generic+https://host/path

Notifications are sent when DNS records are updated, created, deleted, or when errors occur.

💓 Heartbeat Monitoring

Variable Description
HEALTHCHECKS Healthchecks.io ping URL
UPTIMEKUMA Uptime Kuma push URL

Heartbeats are sent after each update cycle. On failure, a fail signal is sent. On shutdown, an exit signal is sent.

Timeouts

Variable Default Description
DETECTION_TIMEOUT 5s Timeout for IP detection requests
UPDATE_TIMEOUT 30s Timeout for Cloudflare API requests

🖥️ Output

Variable Default Description
EMOJI true Use emoji in output messages
QUIET false Suppress informational output

🏁 CLI Flags

Flag Description
--dry-run 🧪 Preview changes without modifying DNS records
--repeat 🔁 Run continuously (legacy config mode only; env var mode uses UPDATE_CRON)

📋 All Environment Variables

Variable Default Description
CLOUDFLARE_API_TOKEN 🔑 API token
CLOUDFLARE_API_TOKEN_FILE 📄 Path to API token file
DOMAINS 🌐 Domains for both IPv4 and IPv6
IP4_DOMAINS 4 IPv4-only domains
IP6_DOMAINS 6 IPv6-only domains
IP4_PROVIDER cloudflare.trace 🔍 IPv4 detection provider
IP6_PROVIDER cloudflare.trace 🔍 IPv6 detection provider
UPDATE_CRON @every 5m ⏱️ Update schedule
UPDATE_ON_START true 🚀 Update on startup
DELETE_ON_STOP false 🧹 Delete records on shutdown
TTL 1 DNS record TTL
PROXIED false ☁️ Proxied expression
RECORD_COMMENT 💬 DNS record comment
MANAGED_RECORDS_COMMENT_REGEX 🎯 Managed records regex
WAF_LISTS 🛡️ WAF lists to manage
WAF_LIST_DESCRIPTION 📝 WAF list description
WAF_LIST_ITEM_COMMENT 💬 WAF list item comment
MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX 🎯 Managed WAF items regex
DETECTION_TIMEOUT 5s IP detection timeout
UPDATE_TIMEOUT 30s API request timeout
EMOJI true 🎨 Enable emoji output
QUIET false 🤫 Suppress info output
HEALTHCHECKS 💓 Healthchecks.io URL
UPTIMEKUMA 💓 Uptime Kuma URL
SHOUTRRR 🔔 Notification URLs (newline-separated)

🚢 Deployment

🐳 Docker Compose

version: '3.9'
services:
  cloudflare-ddns:
    image: timothyjmiller/cloudflare-ddns:latest
    container_name: cloudflare-ddns
    security_opt:
      - no-new-privileges:true
    network_mode: 'host'
    environment:
      - CLOUDFLARE_API_TOKEN=your-api-token
      - DOMAINS=example.com,www.example.com
      - PROXIED=true
      - IP6_PROVIDER=none
      - HEALTHCHECKS=https://hc-ping.com/your-uuid
    restart: unless-stopped

⚠️ Docker requires network_mode: host to access the IPv6 public address.

☸️ Kubernetes

The included manifest uses the legacy JSON config mode. Create a secret containing your config.json and apply:

kubectl create secret generic config-cloudflare-ddns --from-file=config.json -n ddns
kubectl apply -f k8s/cloudflare-ddns.yml

🐧 Linux + Systemd

  1. Build and install:
cargo build --release
sudo cp target/release/cloudflare-ddns /usr/local/bin/
  1. Copy the systemd units from the systemd/ directory:
sudo cp systemd/cloudflare-ddns.service /etc/systemd/system/
sudo cp systemd/cloudflare-ddns.timer /etc/systemd/system/
  1. Place a config.json at /etc/cloudflare-ddns/config.json (the systemd service uses legacy config mode).

  2. Enable the timer:

sudo systemctl enable --now cloudflare-ddns.timer

The timer runs the service every 15 minutes (configurable in cloudflare-ddns.timer).

🔨 Building from Source

cargo build --release

The binary is at target/release/cloudflare-ddns.

🐳 Docker builds

# Single architecture (linux/amd64)
./scripts/docker-build.sh

# Multi-architecture (linux/amd64, linux/arm64, linux/arm/v7)
./scripts/docker-build-all.sh

💻 Supported Platforms


📁 Legacy JSON Config File

For backwards compatibility, cloudflare-ddns still supports configuration via a config.json file. This mode is used automatically when no CLOUDFLARE_API_TOKEN environment variable is set.

🚀 Quick Start

cp config-example.json config.json
# Edit config.json with your values
cloudflare-ddns

🔑 Authentication

Use either an API token (recommended) or a legacy API key:

"authentication": {
  "api_token": "Your cloudflare API token with Edit DNS capability"
}

Or with a legacy API key:

"authentication": {
  "api_key": {
    "api_key": "Your cloudflare API Key",
    "account_email": "The email address you use to sign in to cloudflare"
  }
}

📡 IPv4 and IPv6

Some ISP provided modems only allow port forwarding over IPv4 or IPv6. Disable the interface that is not accessible:

"a": true,
"aaaa": true

⚙️ Config Options

Key Type Default Description
cloudflare array required List of zone configurations
a bool true Enable IPv4 (A record) updates
aaaa bool true Enable IPv6 (AAAA record) updates
purgeUnknownRecords bool false Delete stale/duplicate DNS records
ttl int 300 DNS record TTL in seconds (30-86400, values < 30 become auto)

Each zone entry contains:

Key Type Description
authentication object API token or API key credentials
zone_id string Cloudflare zone ID (found in zone dashboard)
subdomains array Subdomain entries to update
proxied bool Default proxied status for subdomains in this zone

Subdomain entries can be a simple string or a detailed object:

"subdomains": [
  "",
  "@",
  "www",
  { "name": "vpn", "proxied": true }
]

Use "" or "@" for the root domain. Do not include the base domain name.

🔄 Environment Variable Substitution

In the legacy config file, values can reference environment variables with the CF_DDNS_ prefix:

{
  "cloudflare": [{
    "authentication": {
      "api_token": "${CF_DDNS_API_TOKEN}"
    },
    ...
  }]
}

📠 Example: Multiple Subdomains

{
  "cloudflare": [
    {
      "authentication": {
        "api_token": "your-api-token"
      },
      "zone_id": "your_zone_id",
      "subdomains": [
        { "name": "", "proxied": true },
        { "name": "www", "proxied": true },
        { "name": "vpn", "proxied": false }
      ]
    }
  ],
  "a": true,
  "aaaa": true,
  "purgeUnknownRecords": false,
  "ttl": 300
}

🌐 Example: Multiple Zones

{
  "cloudflare": [
    {
      "authentication": { "api_token": "your-api-token" },
      "zone_id": "first_zone_id",
      "subdomains": [
        { "name": "", "proxied": false }
      ]
    },
    {
      "authentication": { "api_token": "your-api-token" },
      "zone_id": "second_zone_id",
      "subdomains": [
        { "name": "", "proxied": false }
      ]
    }
  ],
  "a": true,
  "aaaa": true,
  "purgeUnknownRecords": false
}

🐳 Docker Compose (legacy config file)

version: '3.9'
services:
  cloudflare-ddns:
    image: timothyjmiller/cloudflare-ddns:latest
    container_name: cloudflare-ddns
    security_opt:
      - no-new-privileges:true
    network_mode: 'host'
    volumes:
      - /YOUR/PATH/HERE/config.json:/config.json
    restart: unless-stopped

🏁 Legacy CLI Flags

In legacy config mode, use --repeat to run continuously (the TTL value is used as the update interval):

cloudflare-ddns --repeat
cloudflare-ddns --repeat --dry-run

📜 License

This project is licensed under the GNU General Public License, version 3 (GPLv3).

👨‍💻 Author

Timothy Miller

View my GitHub profile 💡

View my personal website 💻