
cloudflare-ddns
favonia/cloudflare-ddns

favonia/cloudflare-ddns
๐ A small, feature-rich, and robust Cloudflare DDNS updater
README
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.
www.a.org, hello.io) without knowing their DNS zones.๐ฑ.example.org and ๆฅๆฌ๏ฝกco๏ฝกjp) are fully supported.*.example.org) are also supported.A records), IPv6 (AAAA records) and Cloudflare proxying for each domain.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.
๐ 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.
(Click to expand the following items.)
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
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.
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 tokenThe 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 updateThe 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.
PROXIED=true if you are not running a web serverThe 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 probably turn off the proxying by removing PROXIED=true. The default value of PROXIED is false.
IP6_PROVIDER=none if you want to disable IPv6 completelyThe updater, by default, will attempt to update DNS records for both IPv4 and IPv6, and there is no harm in leaving the automatic detection on even if your network does not work for one of them. However, if you want to disable IPv6 entirely (perhaps to avoid all the detection errors), add the setting IP6_PROVIDER=none.
network_mode: hostThe 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:
network_mode: bridge instead of network_mode: host./etc/docker/daemon.json with these settings:{
"ipv6": true,
"fixed-cidr-v6": "fd00::/8",
"experimental": true,
"ip6tables": true
}
systemctl restart docker.service
PUID=1000 and PGID=1000 to the user and group IDs you want to useChange 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.
docker-compose pull cloudflare-ddns
docker-compose up --detach --build cloudflare-ddns
(Click to expand the following items.)
| Name | Valid Values | Meaning | Required? | Default Value |
|---|---|---|---|---|
CF_ACCOUNT_ID |
Cloudflare Account IDs | The Cloudflare account ID used to distinguish multiple DNS zones with the same name. It is not your email address! | No (in most cases you can leave it blank) | (unset) |
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 |
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 |
| 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, or none to disable IPv4. (See below for the detailed description of each provider.) |
No | cloudflare.trace |
IP6_PROVIDER |
cloudflare.doh, cloudflare.trace, local, url:URL, or none |
How to detect IPv6 addresses, or none to disable IPv6. (See below for the detailed description of each provider.) |
No | cloudflare.trace |
๐ At least one of
DOMAINSandIP4/6_DOMAINSmust be non-empty.At least one domain should be listed in
DOMAINS,IP4_DOMAINS, orIP6_DOMAINS. Otherwise, if all of them are empty, then the updater has nothing to do. It is fine to list the same domain in bothIP4_DOMAINSandIP6_DOMAINS, which is equivalent to listing it inDOMAINS. Internationalized domain names are supported using the non-transitional processing that is fully compatible with IDNA2008.
๐ Available providers for
IP4_PROVIDERandIP6_PROVIDER:
cloudflare.doh
Get the public IP address by queryingwhoami.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 asnetwork_mode: hostin 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 isurl:followed by the URL. For example,IP4_PROVIDER=url:https://api4.ipify.orgwill 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_PROVIDERis governing IPv4 addresses andA-type records, while the optionIP6_PROVIDERis governing IPv6 addresses andAAAA-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.dohandcloudflare.trace, the updater will connect to the servers1.1.1.1for IPv4 and2606:4700:4700::1111for IPv6. Since version 1.9.3, the updater will switch to1.0.0.1for IPv4 if1.1.1.1appears 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 entrysub.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.orgwould not work.
| 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.
| 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
PROXIEDcan be a boolean expression. Here are some examples:
PROXIED=is(example.org): proxy only the domainexample.orgPROXIED=is(example1.org) || sub(example2.org): proxy only the domainexample1.organd subdomains ofexample2.orgPROXIED=!is(example.org): proxy every managed domain except forexample.orgPROXIED=is(example1.org) || is(example2.org) || is(example3.org): proxy only the domainsexample1.org,example2.org, andexample3.orgA boolean expression has one of the following forms (all whitespace is ignored):
- A boolean value accepted by strconv.ParseBool, such as
tastrueorFALSEasfalse.is(d)which matches the domaind. Note thatis(*.a)only matches the wildcard domain*.a; usesub(a)to match all subdomains ofa(including*.a).sub(d)which matches subdomains ofd, such asa.dandb.d. It does not match the domainditself.! ewhereeis a boolean expression, representing logical negation ofe.e1 || e2wheree1ande2are boolean expressions, representing logical disjunction ofe1ande2.e1 && e2wheree1ande2are boolean expressions, representing logical conjunction ofe1ande2.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)isis(d1) || is(d2) || ... || is(dn)sub(d1, d2, ..., dn)issub(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)
| 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.
| 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.
โ ๏ธ If using Healthchecks or Uptime Kuma, please note that a failure of IPv6 would be reported as down even if IPv4 records are updated successfully (and similarly if IPv6 works but IPv4 fails). If your setup does not support IPv6, please add
IP6_PROVIDER=noneto disable IPv6 completely.
If you are using Docker Compose, run docker-compose up --detach to reload settings.
(Click to expand the following items.)
โ ๏ธ 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) |
| 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 |
Questions, suggestions, feature requests, and contributions are all welcome! Feel free to open a GitHub issue.