diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3f2cf30 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: 'pip' + directory: '/' + schedule: + interval: 'daily' + + - package-ecosystem: 'docker' + directory: '/' + schedule: + interval: 'daily' diff --git a/.gitignore b/.gitignore index 843b7e0..c6b8a6d 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,7 @@ env/ venv/ ENV/ env.bak/ -venv.bak/ \ No newline at end of file +venv.bak/ + +# Git History +**/.history/* diff --git a/README.md b/README.md index fb8caad..ed67c91 100755 --- a/README.md +++ b/README.md @@ -1,47 +1,29 @@ -# ποΈ [Find a remote job. Work from anywhere.](http://nfty.sh/SRxfA) - -100% Remote. 40 hours/week. 10+ positions available. - -[π§ Send me an email](mailto:porschejobs@timknowsbest.com) for more information. -
# π Cloudflare DDNS Access your home network remotely via a custom domain name without a static IP! -A small, π΅οΈ privacy centric, and β‘ lightning fast multi-architecture Docker image for self hosting projects. +## β‘ Efficiency -## π Table of Contents +- β€οΈ Easy config. List your domains and you're done. +- π The Python runtime will re-use existing HTTP connections. +- ποΈ Cloudflare API responses are cached to reduce API usage. +- π€ The Docker image is small and efficient. +- 0οΈβ£ Zero dependencies. +- πͺ Supports all platforms. +- π Enables low cost self hosting to promote a more decentralized internet. +- π Zero-log IP provider ([cdn-cgi/trace](https://www.cloudflare.com/cdn-cgi/trace)) +- π GPL-3.0 License. Open source for open audits. -- πΊπΈ [Origin](https://github.com/timothymiller/cloudflare-ddns#-origin) -- π [Stats](https://github.com/timothymiller/cloudflare-ddns#-stats) -- βοΈ [How Private & Secure Is This?](https://github.com/timothymiller/cloudflare-ddns#%EF%B8%8F-how-private--secure-is-this) -- π§° [Requirements](https://github.com/timothymiller/cloudflare-ddns#-requirements) -- βοΈ [Equipment](https://github.com/timothymiller/cloudflare-ddns#-equipment) -- π¦ [Getting Started](https://github.com/timothymiller/cloudflare-ddns#-getting-started) - - π [Authentication methods](https://github.com/timothymiller/cloudflare-ddns#-authentication-methods) - - π [Hosting multiple subdomains on the same IP](https://github.com/timothymiller/cloudflare-ddns#-hosting-multiple-subdomains-on-the-same-ip) - - π [Hosting multiple domains (zones) on the same IP](https://github.com/timothymiller/cloudflare-ddns#-hosting-multiple-domains-zones-on-the-same-ip) -- π [Deployment](https://github.com/timothymiller/cloudflare-ddns#-deploy-with-docker-compose) - - π³ [Docker Compose](https://github.com/timothymiller/cloudflare-ddns#-deploy-with-docker-compose) - - π [Kubernetes](https://github.com/timothymiller/cloudflare-ddns#-kubernetes) - - π§ [Crontab](https://github.com/timothymiller/cloudflare-ddns#-deploy-with-linux--cron) -- [Building from source](https://github.com/timothymiller/cloudflare-ddns#building-from-source) -- [License](https://github.com/timothymiller/cloudflare-ddns#license) -- [Author](https://github.com/timothymiller/cloudflare-ddns#author) +## π― Complete Support of Domain Names, Subdomains, IPv4 & IPv6, and Load Balancing -## πΊπΈ Origin - -This script was written for the Raspberry Pi platform to enable low cost self hosting to promote a more decentralized internet. - -### π§Ή Safe for use with existing records - -`cloudflare-ddns` handles the busy work for you, so deploying web apps is less of a clickfest. Every 5 minutes, the script fetches public IPv4 and IPv6 addresses and then creates/updates DNS records for each subdomain in Cloudflare. - -#### Optional features - -Stale, duplicate DNS records are removed for housekeeping. +- π Supports multiple domains (zones) on the same IP. +- π Supports multiple subdomains on the same IP. +- π‘ IPv4 and IPv6 support. +- π Supports all Cloudflare regions. +- βοΈ Supports [Cloudflare Load Balancing](https://developers.cloudflare.com/load-balancing/understand-basics/pools/). +- πΊπΈ Made in the U.S.A. ## π Stats @@ -49,80 +31,6 @@ Stale, duplicate DNS records are removed for housekeeping. | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | [](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns 'cloudflare-ddns docker image size') | [](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns 'Total DockerHub pulls') | [](https://discord.gg/UgGmwMvNxm 'Official Discord Server') | -## βοΈ How Private & Secure Is This? - -1. Uses zero-log external IPv4 & IPv6 provider ([cdn-cgi/trace](https://www.cloudflare.com/cdn-cgi/trace)) -2. Alpine Linux base image -3. HTTPS only via Python Software Foundation requests module -4. Docker runtime -5. Open source for open audits -6. Regular updates - -## π§° Requirements - -- [Cloudflare account](http://nfty.sh/kiUR9) -- [Domain name](http://nfty.sh/qnJji) - -[π Click here to buy a domain name](http://nfty.sh/qnJji) and [get a free Cloudflare account](http://nfty.sh/kiUR9). - -### Supported Platforms - -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) (optional) -- [Kubernetes](https://kubernetes.io/docs/tasks/tools/) (optional) -- [Python 3](https://www.python.org/downloads/) (optional) - -### Helpful links - -- [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) -- [Cloudflare zone ID](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-IP-address-) -- [Cloudflare zone DNS record ID](https://support.cloudflare.com/hc/en-us/articles/360019093151-Managing-DNS-records-in-Cloudflare) - -## βοΈ Equipment - -Here is a list of equipment I use to run this script on a Raspberry Pi 4. - -### Networking - -- [Unifi Dream Machine Pro](https://amzn.to/3Fvq8OM) -- [Unifi AC Pro](https://amzn.to/3FzhdM6) - -#### Network Switches - -- [TPLink 8 Port Gigabit Switch](https://amzn.to/3h08Vm4) - -#### Network Cables - -- [2 Foot Cat 6 Ethernet Cable](https://amzn.to/3NkxyGo) -- [25 Foot Cat 6 Ethernet Cable](https://amzn.to/3FuFEu9) - -#### Storage - -- [2TB Samsung 980 PRO PCIe NVMe Gen 4 Gaming SSD](https://amzn.to/3U6tFag) -- [WD 16TB Elements Desktop External Hard Drive](https://amzn.to/3zplpdB) - -#### Battery Backup - -- [APC UPS Battery Backup Surge Protector, 425VA Backup Battery Power Supply](https://amzn.to/3U5eVZ2) - -### Recommended Setups - -Rest assured your money is well spent on any of the following platforms. Each platform provides a robust virtualization solution for running Docker containers. - -#### ARM64 - Raspberry Pi 4 - -- [Raspberry Pi 4](https://amzn.to/3Uc8gg0) -- [Raspberry Pi 4 case](https://amzn.to/3fooUtN) -- [Raspberry Pi 4 power supply](https://amzn.to/3fixzxV) -- [Raspberry Pi 4 128gb microSD card](https://amzn.to/3ztuNwL) -- [Raspberry Pi 4 heatsink](https://amzn.to/3SOK8Ps) -- [Raspberry Pi 4 cooling fan](https://amzn.to/3Doa1Qa) -- [Raspberry Pi 4 USB-C to HDMI adapter](https://amzn.to/3gVks6u) - -#### x86_64 - Intel NUC - -- [Intel NUC](https://amzn.to/3STMYT4) - ## π¦ Getting Started First copy the example configuration file into the real one. @@ -153,7 +61,7 @@ Alternatively, you can use the traditional API keys by setting appropriate value "account_email": "The email address you use to sign in to cloudflare", ``` -### Enable or disable IPv4 or IPv6 +### π Enable or disable IPv4 or IPv6 Some ISP provided modems only allow port forwarding over IPv4 or IPv6. In this case, you would want to disable any interface not accessible via port forward. @@ -162,7 +70,7 @@ Some ISP provided modems only allow port forwarding over IPv4 or IPv6. In this c "aaaa": true ``` -### Other values explained +### ποΈ Other values explained ```json "zone_id": "The ID of the zone that will get the records. From your dashboard click into the zone. Under the overview tab, scroll down and the zone ID is listed in the right rail", @@ -173,17 +81,118 @@ Some ISP provided modems only allow port forwarding over IPv4 or IPv6. In this c ## π Hosting multiple subdomains on the same IP? -You can save yourself some trouble when hosting multiple domains pointing to the same IP address (in the case of Traefik) by defining one A & AAAA record 'ddns.example.com' pointing to the IP of the server that will be updated by this DDNS script. For each subdomain, create a CNAME record pointing to 'ddns.example.com'. Now you don't have to manually modify the script config every time you add a new subdomain to your site! +This script can be used to update multiple subdomains on the same IP address. -## π Hosting multiple domains (zones) on the same IP? - -You can handle ddns for multiple domains (cloudflare zones) using the same docker container by separating your configs inside `config.json` like below: +For example, if you have a domain `example.com` and you want to host additional subdomains at `foo.example.com` and `bar.example.com` on the same IP address, you can use this script to update the DNS records for all subdomains. ### β οΈ Note +Please remove the comments after `//` in the below example. They are only there to explain the config. + Do not include the base domain name in your `subdomains` config. Do not use the [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name). +### π Example π + ```bash +{ + "cloudflare": [ + { + "authentication": { + "api_token": "api_token_here", // Either api_token or api_key + "api_key": { + "api_key": "api_key_here", + "account_email": "your_email_here" + } + }, + "zone_id": "your_zone_id_here", + "subdomains": [ + { + "name": "", // Root domain (example.com) + "proxied": true + }, + { + "name": "foo", // (foo.example.com) + "proxied": true + }, + { + "name": "bar", // (bar.example.com) + "proxied": true + } + ] + } + ], + "a": true, + "aaaa": true, + "purgeUnknownRecords": false, + "ttl": 300 +} +``` + +## π Hosting multiple domains (zones) on the same IP? + +You can handle ddns for multiple domains (cloudflare zones) using the same docker container by duplicating your configs inside the `cloudflare: []` key within `config.json` like below: + +### β οΈ Note: + +If you are using API Tokens, make sure the token used supports editing your zone ID. + +```bash +{ + "cloudflare": [ + { + "authentication": { + "api_token": "api_token_here", + "api_key": { + "api_key": "api_key_here", + "account_email": "your_email_here" + } + }, + "zone_id": "your_first_zone_id_here", + "subdomains": [ + { + "name": "", + "proxied": false + }, + { + "name": "remove_or_replace_with_your_subdomain", + "proxied": false + } + ] + }, + { + "authentication": { + "api_token": "api_token_here", + "api_key": { + "api_key": "api_key_here", + "account_email": "your_email_here" + } + }, + "zone_id": "your_second_zone_id_here", + "subdomains": [ + { + "name": "", + "proxied": false + }, + { + "name": "remove_or_replace_with_your_subdomain", + "proxied": false + } + ] + } + ], + "a": true, + "aaaa": true, + "purgeUnknownRecords": false +} +``` + +## βοΈ Load Balancing + +If you have multiple IP addresses and want to load balance between them, you can use the `loadBalancing` option. This will create a CNAME record for each subdomain that points to the subdomain with the lowest IP address. + +### π Example config to support load balancing + +```json { "cloudflare": [ { @@ -206,10 +215,51 @@ Do not include the base domain name in your `subdomains` config. Do not use the } ] } + ],{ + "cloudflare": [ + { + "authentication": { + "api_token": "api_token_here", + "api_key": { + "api_key": "api_key_here", + "account_email": "your_email_here" + } + }, + "zone_id": "your_zone_id_here", + "subdomains": [ + { + "name": "", + "proxied": false + }, + { + "name": "remove_or_replace_with_your_subdomain", + "proxied": false + } + ] + } + ], + "load_balancer": [ + { + "authentication": { + "api_token": "api_token_here", + "api_key": { + "api_key": "api_key_here", + "account_email": "your_email_here" + } + }, + "pool_id": "your_pool_id_here", + "origin": "your_origin_name_here" + } ], "a": true, "aaaa": true, - "purgeUnknownRecords": false + "purgeUnknownRecords": false, + "ttl": 300 +} + "a": true, + "aaaa": true, + "purgeUnknownRecords": false, + "ttl": 300 } ``` @@ -227,6 +277,10 @@ For ex: "api_token": "${CF_DDNS_API_TOKEN}", ``` +### π§Ή Optional features + +`purgeUnknownRecords` removes stale DNS records from Cloudflare. This is useful if you have a dynamic DNS record that you no longer want to use. If you have a dynamic DNS record that you no longer want to use, you can set `purgeUnknownRecords` to `true` and the script will remove the stale DNS record from Cloudflare. + ## π³ Deploy with Docker Compose Pre-compiled images are available via [the official docker container on DockerHub](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns). @@ -234,7 +288,7 @@ Pre-compiled images are available via [the official docker container on DockerHu Modify the host file path of config.json inside the volumes section of docker-compose.yml. ```yml -version: '3.7' +version: '3.9' services: cloudflare-ddns: image: timothyjmiller/cloudflare-ddns:latest @@ -361,6 +415,20 @@ Recommended for production docker run -d timothyjmiller/cloudflare_ddns:latest ``` +## Supported Platforms + +- [Docker](https://docs.docker.com/get-docker/) +- [Docker Compose](https://docs.docker.com/compose/install/) +- [Kubernetes](https://kubernetes.io/docs/tasks/tools/) +- [Python 3](https://www.python.org/downloads/) +- [Systemd](https://www.freedesktop.org/wiki/Software/systemd/) + +## π Helpful links + +- [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens) +- [Cloudflare zone ID](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-IP-address-) +- [Cloudflare zone DNS record ID](https://support.cloudflare.com/hc/en-us/articles/360019093151-Managing-DNS-records-in-Cloudflare) + ## License This Template is licensed under the GNU General Public License, version 3 (GPLv3). diff --git a/cloudflare-ddns.py b/cloudflare-ddns.py index 9b19393..ca169c1 100755 --- a/cloudflare-ddns.py +++ b/cloudflare-ddns.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # cloudflare-ddns.py # Summary: Access your home network remotely via a custom domain name without a static IP! # Description: Access your home network remotely via a custom domain @@ -69,9 +69,20 @@ def getIPs(): global shown_ipv4_warning if not shown_ipv4_warning: shown_ipv4_warning = True - print("π§© IPv4 not detected") - if purgeUnknownRecords: - deleteEntries("A") + print("π§© IPv4 not detected via 1.1.1.1, trying 1.0.0.1") + # Try secondary IP check + try: + a = requests.get( + "https://1.0.0.1/cdn-cgi/trace").text.split("\n") + a.pop() + a = dict(s.split("=") for s in a)["ip"] + except Exception: + global shown_ipv4_warning_secondary + if not shown_ipv4_warning_secondary: + shown_ipv4_warning_secondary = True + print("π§© IPv4 not detected via 1.0.0.1. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.") + if purgeUnknownRecords: + deleteEntries("A") if ipv6_enabled: try: aaaa = requests.get( @@ -82,16 +93,26 @@ def getIPs(): global shown_ipv6_warning if not shown_ipv6_warning: shown_ipv6_warning = True - print("π§© IPv6 not detected") - if purgeUnknownRecords: - deleteEntries("AAAA") + print("π§© IPv6 not detected via 1.1.1.1, trying 1.0.0.1") + try: + aaaa = requests.get( + "https://[2606:4700:4700::1001]/cdn-cgi/trace").text.split("\n") + aaaa.pop() + aaaa = dict(s.split("=") for s in aaaa)["ip"] + except Exception: + global shown_ipv6_warning_secondary + if not shown_ipv6_warning_secondary: + shown_ipv6_warning_secondary = True + print("π§© IPv6 not detected via 1.0.0.1. Verify your ISP or DNS provider isn't blocking Cloudflare's IPs.") + if purgeUnknownRecords: + deleteEntries("AAAA") ips = {} - if(a is not None): + if (a is not None): ips["ipv4"] = { "type": "A", "ip": a } - if(aaaa is not None): + if (aaaa is not None): ips["ipv6"] = { "type": "AAAA", "ip": aaaa @@ -117,7 +138,7 @@ def commitRecord(ip): proxied = option["proxied"] fqdn = base_domain_name # Check if name provided is a reference to the root domain - if name != '' and name != '*' and name != '@': + if name != '' and name != '@': fqdn = name + "." + base_domain_name record = { "type": ip["type"], @@ -168,6 +189,26 @@ def commitRecord(ip): return True +def updateLoadBalancer(ip): + + for option in config["load_balancer"]: + pools = cf_api('user/load_balancers/pools', 'GET', option) + + if pools: + idxr = dict((p['id'], i) for i, p in enumerate(pools['result'])) + idx = idxr.get(option['pool_id']) + + origins = pools['result'][idx]['origins'] + + idxr = dict((o['name'], i) for i, o in enumerate(origins)) + idx = idxr.get(option['origin']) + + origins[idx]['address'] = ip['ip'] + data = {'origins': origins} + + response = cf_api(f'user/load_balancers/pools/{option["pool_id"]}', 'PATCH', option, {}, data) + + def cf_api(endpoint, method, config, headers={}, data=False): api_token = config['authentication']['api_token'] if api_token != '' and api_token != 'api_token_here': @@ -180,7 +221,7 @@ def cf_api(endpoint, method, config, headers={}, data=False): "X-Auth-Key": config['authentication']['api_key']['api_key'], } try: - if(data == False): + if (data == False): response = requests.request( method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers) else: @@ -204,11 +245,14 @@ def cf_api(endpoint, method, config, headers={}, data=False): def updateIPs(ips): for ip in ips.values(): commitRecord(ip) + #updateLoadBalancer(ip) if __name__ == '__main__': shown_ipv4_warning = False + shown_ipv4_warning_secondary = False shown_ipv6_warning = False + shown_ipv6_warning_secondary = False ipv4_enabled = True ipv6_enabled = True purgeUnknownRecords = False @@ -247,13 +291,14 @@ if __name__ == '__main__': if ttl < 30: ttl = 30 # print("βοΈ TTL is too low - defaulting to 30 seconds") - if(len(sys.argv) > 1): - if(sys.argv[1] == "--repeat"): + if (len(sys.argv) > 1): + if (sys.argv[1] == "--repeat"): if ipv4_enabled and ipv6_enabled: print( "π°οΈ Updating IPv4 (A) & IPv6 (AAAA) records every " + str(ttl) + " seconds") elif ipv4_enabled and not ipv6_enabled: - print("π°οΈ Updating IPv4 (A) records every " + str(ttl) + " seconds") + print("π°οΈ Updating IPv4 (A) records every " + + str(ttl) + " seconds") elif ipv6_enabled and not ipv4_enabled: print("π°οΈ Updating IPv6 (AAAA) records every " + str(ttl) + " seconds") @@ -268,4 +313,4 @@ if __name__ == '__main__': print("β Unrecognized parameter '" + sys.argv[1] + "'. Stopping now.") else: - updateIPs(getIPs()) + updateIPs(getIPs()) \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5721809..bb8b21c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,11 +1,11 @@ -version: "3.7" +version: '3.9' services: cloudflare-ddns: image: timothyjmiller/cloudflare-ddns:latest container_name: cloudflare-ddns security_opt: - no-new-privileges:true - network_mode: "host" + network_mode: 'host' environment: - PUID=1000 - PGID=1000 diff --git a/requirements.txt b/requirements.txt index 5e77405..bf0d9d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests==2.28.1 \ No newline at end of file +requests==2.28.2 \ No newline at end of file diff --git a/systemd/cloudflare-ddns.service b/systemd/cloudflare-ddns.service new file mode 100644 index 0000000..e5c0692 --- /dev/null +++ b/systemd/cloudflare-ddns.service @@ -0,0 +1,13 @@ +[Unit] +Description=Update DDNS on Cloudflare +ConditionPathExists=/etc/cloudflare-ddns/config.json +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +Environment=CONFIG_PATH=/etc/cloudflare-ddns +ExecStart=cloudflare-ddns + +[Install] +WantedBy=multi-user.target diff --git a/systemd/cloudflare-ddns.timer b/systemd/cloudflare-ddns.timer new file mode 100644 index 0000000..cc9cd61 --- /dev/null +++ b/systemd/cloudflare-ddns.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Update DDNS on Cloudflare every 15 minutes + +[Timer] +OnBootSec=2min +OnUnitActiveSec=15m + +[Install] +WantedBy=timers.target