cloudflare-ddns

module
v1.11.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 23, 2023 License: Apache-2.0

README ΒΆ

🌟 Cloudflare DDNS

Github Source Go Reference Codecov Docker Image Size OpenSSF Best Practices OpenSSF Scorecard

A feature-rich and robust Cloudflare DDNS updater with a small footprint. The program will detect your machine's public IP addresses and update DNS records using the Cloudflare API.

πŸ“œ Highlights

⚑ Efficiency
  • 🀏 The Docker image takes less than 3 MB (after compression).
  • πŸ” The Go runtime re-uses existing HTTP connections.
  • πŸ—ƒοΈ Cloudflare API responses are cached to reduce the API usage.
πŸ’― Complete Support of Domain Names
  • 😌 You can simply list domains (e.g., www.a.org, hello.io) without knowing their DNS zones.
  • 🌍 Internationalized domain names (e.g., 🐱.example.org and ζ—₯本qcoqjp) are fully supported.
  • πŸƒ Wildcard domains (e.g., *.example.org) are also supported.
  • πŸ•ΉοΈ You can toggle IPv4 (A records), IPv6 (AAAA records) and Cloudflare proxying for each domain.
πŸ•΅οΈ Privacy

By default, public IP addresses are obtained via Cloudflare debugging page. This minimizes the impact on privacy because we are already using the Cloudflare API to update DNS records. Moreover, if Cloudflare servers are not reachable, chances are you cannot update DNS records anyways.

πŸ›‘οΈ Security
  • πŸ›‘ Superuser privileges are immediately dropped, minimizing the impact of undiscovered bugs.

  • πŸ›‘οΈ The updater uses only HTTPS or DNS over HTTPS to detect IP addresses; see the Security Model.

  • 🩺 The updater can notify you via Healthchecks and Uptime Kuma when the updating fails.

  • πŸ“š The updater uses only established open-source Go libraries (click to expand)
    • cap:
      The official Go binding of Linux capabilities.
    • cloudflare-go:
      The official Go binding of Cloudflare API v4.
    • cron:
      Parsing of Cron expressions.
    • go-retryablehttp:
      HTTP clients with automatic retries and exponential backoff.
    • ttlcache:
      In-memory cache to hold Cloudflare API responses.
    • mock (for testing only):
      A comprehensive, semi-official framework for mocking.
    • testify (for testing only):
      A comprehensive tool set for testing Go programs.

⛷️ Quick Start

(Click to expand the following items.)

πŸ‹ Directly run the Docker image.
docker run \
  --network host \
  -e CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
  -e DOMAINS=example.org,www.example.org,example.io \
  -e PROXIED=true \
  favonia/cloudflare-ddns
🧬 Directly run the updater from its source on Linux.

You need the Go tool to run the updater from its source.

CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
  DOMAINS=example.org,www.example.org,example.io \
  PROXIED=true \
  go run github.com/favonia/cloudflare-ddns/cmd/ddns@latest

πŸ‘‰ For non-Linux operating systems, please use the Docker method instead.

πŸ‹ Deployment with Docker Compose

πŸ“¦ Step 1: Updating the Compose File

Incorporate the following fragment into the compose file (typically docker-compose.yml or docker-compose.yaml).

version: "3"
services:
  cloudflare-ddns:
    image: favonia/cloudflare-ddns:latest
    network_mode: host
    # This makes IPv6 easier; see below
    restart: always
    # Restart the updater after reboot
    cap_add:
      - SETUID
        # Capability to change user ID; needed for using PUID
      - SETGID
        # Capability to change group ID; needed for using PGID
    cap_drop:
      - all
      # Drop all other capabilities
    read_only: true
    # Make the container filesystem read-only
    security_opt:
      - no-new-privileges:true
        # Another protection to restrict superuser privileges
    environment:
      - PUID=1000
        # Run the updater with user ID 1000
      - PGID=1000
        # Run the updater with group ID 1000
      - CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN
        # Your Cloudflare API token
      - DOMAINS=example.org,www.example.org,example.io
        # Your domains (separated by commas)
      - PROXIED=true
        # Tell Cloudflare to cache webpages and hide your IP

(Click to expand the following important tips.)

πŸ”‘ CF_API_TOKEN is your Cloudflare API token

The value of CF_API_TOKEN should be an API token (not an API key), which can be obtained from the API Tokens page. Use the Edit zone DNS template to create and copy a token into the environment file. (The less secure API key authentication is deliberately not supported.)

πŸ“ DOMAINS is the list of domains to update

The value of DOMAINS should be a list of fully qualified domain names (FQDNs) separated by commas. For example, DOMAINS=example.org,www.example.org,example.io instructs the updater to manage the domains example.org, www.example.org, and example.io. These domains do not have to be in the same zone---the updater will identify their zones automatically.

🚨 Remove PROXIED=true if you are not running a web server

The setting PROXIED=true instructs Cloudflare to cache webpages and hide your IP addresses. If you wish to bypass that and expose your actual IP addresses, remove PROXIED=true. If your traffic is not HTTP(S), then Cloudflare cannot proxy it and you should turn off the proxying by removing PROXIED=true. The default value of PROXIED is false.

πŸ“‘ Expand this if you want IPv6 without using network_mode: host

The easiest way to enable IPv6 is to use network_mode: host so that the updater can access the host IPv6 network directly. This has the downside of bypassing the network isolation. If you wish to keep the updater isolated from the host network, check out the experimental ip6tables option. If your host OS is Linux, here’s the tl;dr:

  1. Use network_mode: bridge instead of network_mode: host.
  2. Edit or create /etc/docker/daemon.json with these settings:
    {
      "ipv6": true,
      "fixed-cidr-v6": "fd00::/8",
      "experimental": true,
      "ip6tables": true
    }
    
  3. Restart the Docker daemon (if you are using systemd):
    systemctl restart docker.service
    
πŸ›‘οΈ Change PUID=1000 and PGID=1000 to the user and group IDs you want to use

Change 1000 to the user or group IDs you wish to use to run the updater. The settings cap_drop, read_only, and no-new-privileges provide additional protection, especially when you run the container as a non-superuser. The updater itself will read PUID and PGID and attempt to drop all superuser privileges.

πŸš€ Step 2: Building the Container
docker-compose pull cloudflare-ddns
docker-compose up --detach --build cloudflare-ddns

πŸŽ›οΈ Further Customization

βš™οΈ All Settings

(Click to expand the following items.)

πŸ”‘ Cloudflare accounts and API tokens
Name Valid Values Meaning Required? Default Value
CF_ACCOUNT_ID Cloudflare Account IDs The account ID used to distinguish multiple zone IDs with the same name No (unset)
CF_API_TOKEN_FILE Paths to files containing Cloudflare API tokens A file that contains the token to access the Cloudflare API Exactly one of CF_API_TOKEN and CF_API_TOKEN_FILE should be set N/A
CF_API_TOKEN Cloudflare API tokens The token to access the Cloudflare API Exactly one of CF_API_TOKEN and CF_API_TOKEN_FILE should be set N/A

🀷 In most cases, CF_ACCOUNT_ID is not needed.

πŸ“ Domains and IP providers
Name Valid Values Meaning Required? Default Value
DOMAINS Comma-separated fully qualified domain names or wildcard domain names The domains the updater should manage for both A and AAAA records (See below) (empty list)
IP4_DOMAINS Comma-separated fully qualified domain names or wildcard domain names The domains the updater should manage for A records (See below) (empty list)
IP6_DOMAINS Comma-separated fully qualified domain names or wildcard domain names The domains the updater should manage for AAAA records (See below) (empty list)
IP4_PROVIDER cloudflare.doh, cloudflare.trace, local, url:URL, or none How to detect IPv4 addresses. (See below) No cloudflare.trace
IP6_PROVIDER cloudflare.doh, cloudflare.trace, local, url:URL, or none How to detect IPv6 addresses. (See below) No cloudflare.trace
πŸ“ At least one of DOMAINS and IP4/6_DOMAINS must be non-empty.

At least one domain should be listed in DOMAINS, IP4_DOMAINS, or IP6_DOMAINS. Otherwise, if all of them are empty, then the updater has nothing to do. It is fine to list the same domain in both IP4_DOMAINS and IP6_DOMAINS, which is equivalent to listing it in DOMAINS. Internationalized domain names are supported using the non-transitional processing that is fully compatible with IDNA2008.

πŸ“œ Available providers for IP4_PROVIDER and IP6_PROVIDER:
  • cloudflare.doh
    Get the public IP address by querying whoami.cloudflare. against Cloudflare via DNS-over-HTTPS and update DNS records accordingly.
  • cloudflare.trace
    Get the public IP address by parsing the Cloudflare debugging page and update DNS records accordingly. This is the default provider.
  • local
    Get the address via local network interfaces and update DNS records accordingly. When multiple local network interfaces or in general multiple IP addresses are present, the updater will use the address that would have been used for outbound UDP connections to Cloudflare servers. ⚠️ You need access to the host network (such as network_mode: host in Docker Compose) for this policy, for otherwise the updater will detect the addresses inside the bridge network in Docker instead of those in the host network.
  • url:URL
    Fetch the content at a URL via the HTTP(S) protocol as the IP address. The provider format is url: followed by the URL. For example, IP4_PROVIDER=url:https://api4.ipify.org will fetch the IPv4 addresses from https://api4.ipify.org, a server maintained by ipify. ⚠️ Currently, the updater will not force IPv4 or IPv6 when retrieving the IPv4 or IPv6 address at the URL, and thus the service must either restrict its access to the correct IP network or return the correct IP address regardless of what IP network is used. As an example, https://api4.ipify.org has restricted its access to IPv4. The reason is that there are no elegant ways to force IPv4 or IPv6 using the Go standard library; please open a GitHub issue if you have a use case so that I might add some ugly hack to force it.
  • none
    Stop the DNS updating completely. Existing DNS records will not be removed.

The option IP4_PROVIDER is governing IPv4 addresses and A-type records, while the option IP6_PROVIDER is governing IPv6 addresses and AAAA-type records. The two options act independently of each other; that is, you can specify different address providers for IPv4 and IPv6.

Some technical details: For the providers cloudflare.doh and cloudflare.trace, the updater will connect to the servers 1.1.1.1 for IPv4 and 2606:4700:4700::1111 for IPv6. Since version 1.9.3, the updater will switch to 1.0.0.1 for IPv4 if 1.1.1.1 appears to be blocked or intercepted by your ISP or your router (which is still not uncommon).

πŸƒ What are wildcard domains?

Wildcard domains (*.example.org) represent all subdomains that would not exist otherwise. Therefore, if you have another subdomain entry sub.example.org, the wildcard domain is independent of it, because it only represents the other subdomains which do not have their own entries. Also, you can only have one layer of *---*.*.example.org would not work.

⏳ Schedules, triggers, and timeouts
Name Valid Values Meaning Required? Default Value
CACHE_EXPIRATION Positive time durations with a unit, such as 1h and 10m. See time.ParseDuration The expiration of cached Cloudflare API responses No 6h0m0s (6 hours)
DELETE_ON_STOP Boolean values, such as true, false, 0 and 1. See strconv.ParseBool Whether managed DNS records should be deleted on exit No false
DETECTION_TIMEOUT Positive time durations with a unit, such as 1h and 10m. See time.ParseDuration The timeout of each attempt to detect IP addresses No 5s (5 seconds)
TZ Recognized timezones, such as UTC The timezone used for logging and parsing UPDATE_CRON No UTC
UPDATE_CRON Cron expressions and the special value @once. See the documentation of cron for cron expressions. The schedule to re-check IP addresses and update DNS records (if necessary). If the special value @once is used, cron is disabled and the updater will terminate immediately after updating the DNS records. No @every 5m (every 5 minutes)
UPDATE_ON_START Boolean values, such as true, false, 0 and 1. See strconv.ParseBool Whether to check IP addresses on start regardless of UPDATE_CRON No true
UPDATE_TIMEOUT Positive time durations with a unit, such as 1h and 10m. See time.ParseDuration The timeout of each attempt to update DNS records, per domain, per record type No 30s (30 seconds)

⚠️ The update schedule does not take the time to update records into consideration. For example, if the schedule is β€œfor every 5 minutes”, and if the updating itself takes 2 minutes, then the actual interval between adjacent updates is 3 minutes, not 5 minutes.

🐣 Parameters of new DNS records
Name Valid Values Meaning Required? Default Value
PROXIED Boolean values, such as true, false, 0 and 1. See strconv.ParseBool. πŸ§ͺ See below for experimental support of per-domain proxy settings. Whether new DNS records should be proxied by Cloudflare No false
TTL Time-to-live (TTL) values in seconds The TTL values used to create new DNS records No 1 (This means β€œautomatic” to Cloudflare)

πŸ‘‰ The updater will preserve existing proxy and TTL settings until it has to create new DNS records (or recreate deleted ones). Only when it creates DNS records, the above settings will apply. To change existing proxy and TTL settings now, you can go to your Cloudflare Dashboard and change them directly. If you think you have a use case where the updater should actively overwrite existing proxy and TTL settings in addition to IP addresses, please let me know. It is not hard to implement optional overwriting.

πŸ§ͺ Experimental per-domain proxy settings (subject to changes):

The PROXIED can be a boolean expression. Here are some examples:

  • PROXIED=is(example.org): proxy only the domain example.org
  • PROXIED=is(example1.org) || sub(example2.org): proxy only the domain example1.org and subdomains of example2.org
  • PROXIED=!is(example.org): proxy every managed domain except for example.org
  • PROXIED=is(example1.org) || is(example2.org) || is(example3.org): proxy only the domains example1.org, example2.org, and example3.org

A boolean expression has one of the following forms (all whitespace is ignored):

  • A boolean value accepted by strconv.ParseBool, such as t as true or FALSE as false.
  • is(d) which matches the domain d. Note that is(*.a) only matches the wildcard domain *.a; use sub(a) to match all subdomains of a (including *.a).
  • sub(d) which matches subdomains of d, such as a.d and b.d. It does not match the domain d itself.
  • ! e where e is a boolean expression, representing logical negation of e.
  • e1 || e2 where e1 and e2 are boolean expressions, representing logical disjunction of e1 and e2.
  • e1 && e2 where e1 and e2 are boolean expressions, representing logical conjunction of e1 and e2.

One can use parentheses to group expressions, such as !(is(a) && (is(b) || is(c))). For convenience, the engine also accepts these short forms:

  • is(d1, d2, ..., dn) is is(d1) || is(d2) || ... || is(dn)
  • sub(d1, d2, ..., dn) is sub(d1) || sub(d2) || ... || sub(dn)

For example, these two settings are equivalent:

  • PROXYD=is(example1.org) || is(example2.org) || is(example3.org)
  • PROXIED=is(example1.org,example2.org,example3.org)
πŸ›‘οΈ Dropping superuser privileges
Name Valid Values Meaning Required? Default Value
PGID Non-zero POSIX group ID The group ID the updater should assume No Effective group ID; if it is zero, then the real group ID; if it is still zero, then 1000
PUID Non-zero POSIX user ID The user ID the updater should assume No Effective user ID; if it is zero, then the real user ID; if it is still zero, then 1000

πŸ‘‰ The updater will also try to drop supplementary group IDs.

πŸ‘οΈ Logging, Healthchecks, and Uptime Kuma
Name Valid Values Meaning Required? Default Value
QUIET Boolean values, such as true, false, 0 and 1. See strconv.ParseBool Whether the updater should reduce the logging No false
EMOJI Boolean values, such as true, false, 0 and 1. See strconv.ParseBool Whether the updater should use emojis in the logging No true
HEALTHCHECKS Healthchecks ping URLs, such as https://hc-ping.com/<uuid> or https://hc-ping.com/<project-ping-key>/<name-slug> (see below) If set, the updater will ping the URL when it successfully updates IP addresses No (unset)
πŸ§ͺ UPTIMEKUMA (experimental) πŸ§ͺ Uptime Kuma’s Push URLs, such as https://<host>/push/<id>. For convenience, you can directly copy the β€˜Push URL’ from the Uptime Kuma configuration page. πŸ§ͺ If set, the updater will ping the URL when it successfully updates IP addresses. ⚠️ Remember to change the β€œHeartbeat Interval” to match your DNS updating schedule specified in UPDATE_CRON. No (unset)

🩺 For HEALTHCHECKS, the updater can work with any server following the same notification protocol, including but not limited to self-hosted instances of Healthchecks. Both UUID and Slug URLs are supported, and the updater works regardless whether the POST-only mode is enabled.

πŸ”‚ Restarting the Container

If you are using Docker Compose, run docker-compose up --detach to reload settings.

🚡 Migration Guides

(Click to expand the following items.)

I am migrating from oznu/cloudflare-ddns (now archived)

⚠️ oznu/cloudflare-ddns relies on the insecure DNS protocol to obtain public IP addresses; a malicious hacker could more easily forge DNS responses and trick it into updating your domain with any IP address. In comparison, we use only verified responses from Cloudflare, which makes the attack much more difficult. See the design document for more information on security.

Old Parameter Note
API_KEY=key βœ”οΈ Use CF_API_TOKEN=key
API_KEY_FILE=file βœ”οΈ Use CF_API_TOKEN_FILE=file
ZONE=example.org and SUBDOMAIN=sub βœ”οΈ Use DOMAINS=sub.example.org directly
PROXIED=true βœ”οΈ Same (PROXIED=true)
RRTYPE=A βœ”οΈ Both IPv4 and IPv6 are enabled by default; use IP6_PROVIDER=none to disable IPv6
RRTYPE=AAAA βœ”οΈ Both IPv4 and IPv6 are enabled by default; use IP4_PROVIDER=none to disable IPv4
DELETE_ON_STOP=true βœ”οΈ Same (DELETE_ON_STOP=true)
INTERFACE=iface βœ”οΈ Not required for local providers; we can handle multiple network interfaces
CUSTOM_LOOKUP_CMD=cmd ❌ There are no shells in the minimal Docker image
DNS_SERVER=server ❌ Only Cloudflare is supported, except the url:URL provider via HTTP(S)
I am migrating from timothymiller/cloudflare-ddns
Old JSON Key Note
cloudflare.authentication.api_token βœ”οΈ Use CF_API_TOKEN=key
cloudflare.authentication.api_key ❌ Please use the newer, more secure API tokens
cloudflare.zone_id βœ”οΈ Not needed; automatically retrieved from the server
cloudflare.subdomains[].name βœ”οΈ Use DOMAINS with fully qualified domain names (FQDNs) directly; for example, if your zone is example.org and your subdomain is sub, use DOMAINS=sub.example.org
cloudflare.subdomains[].proxied πŸ§ͺ (experimental) Write boolean expressions for PROXIED to specify per-domain settings; see above for the detailed documentation for this experimental feature
load_balancer ❌ Not supported yet; please make a request if you want it
a βœ”οΈ Both IPv4 and IPv6 are enabled by default; use IP4_PROVIDER=none to disable IPv4
aaaa βœ”οΈ Both IPv4 and IPv6 are enabled by default; use IP6_PROVIDER=none to disable IPv6
proxied βœ”οΈ Use PROXIED=true or PROXIED=false
purgeUnknownRecords ❌ The updater never deletes unmanaged DNS records

πŸ’– Feedback

Questions, suggestions, feature requests, and contributions are all welcome! Feel free to open a GitHub issue.

Directories ΒΆ

Path Synopsis
cmd
ddns
Package main is the entry point of the Cloudflare DDNS updater.
Package main is the entry point of the Cloudflare DDNS updater.
internal
api
Package api implements protocols to update DNS records.
Package api implements protocols to update DNS records.
config
Package config reads and parses configurations.
Package config reads and parses configurations.
cron
Package cron handles anything related to time.
Package cron handles anything related to time.
domain
Package domain parses DNS domain names.
Package domain parses DNS domain names.
domainexp
Package domainexp parses expressions containing domains.
Package domainexp parses expressions containing domains.
droproot
Package droproot drops root privileges.
Package droproot drops root privileges.
file
Package file virtualizes file systems for mock testing.
Package file virtualizes file systems for mock testing.
ipnet
Package ipnet contains utility functions for IPv4 and IPv6 networks.
Package ipnet contains utility functions for IPv4 and IPv6 networks.
mocks
Code generated by MockGen.
Code generated by MockGen.
monitor
Package monitor implements dead man's switches.
Package monitor implements dead man's switches.
pp
Package pp handles pretty-printing.
Package pp handles pretty-printing.
provider
Package provider implements protocols to detect public IP addresses.
Package provider implements protocols to detect public IP addresses.
provider/protocol
Package protocol implements all the generic protocols to detect public IP addresses.
Package protocol implements all the generic protocols to detect public IP addresses.
setter
Package setter implements the logic to update DNS records using [api.Handle].
Package setter implements the logic to update DNS records using [api.Handle].
signal
Package signal implements the handling of signals.
Package signal implements the handling of signals.
updater
Package updater implements the logic to detect and update IP addresses, combining the packages setter and provider.
Package updater implements the logic to detect and update IP addresses, combining the packages setter and provider.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL