├── .gitignore ├── LICENSE ├── .cloudflare.example ├── CONTRIBUTING.md ├── cloudflare_ddns └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .cloudflare 2 | 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ulygit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.cloudflare.example: -------------------------------------------------------------------------------- 1 | # The Cloudlflare credentials (i.e. api token and zone id) can be 2 | # obtained from your Cloudflare account online. The record information 3 | # (i.e. id, name, type) can be obtained by first specifying the 4 | # Cloudflare credentials and zone id here and then running 5 | # 'cloudflare_ddns list'. This will query your Cloudflare account and download 6 | # record information for the specified zone into a log file in the directory 7 | # the script is executed from. 8 | # 9 | # KNOWN ISSUE: Zones with a large number of records may not download 10 | # completely because Cloudflare paginates the results but the cloudflare_ddns 11 | # script doesn't support pagination. You will have to manually query if this 12 | # becomes a problem. It may also be possible to obtain record information 13 | # directly from the web portal, but it wasn't obvious to me where. 14 | 15 | # account credentials 16 | CLOUDFLARE_API_TOKEN='' 17 | 18 | # DEPRECATION WARNING 19 | # Cloudflare continues to support auth via email and api key, but 20 | # they are encouraging the use of "tokens" for API access. These can be 21 | # created through your Cloudflare account online. Once created, you can specify 22 | # your token via the CLOUDFLARE_API_TOKEN setting. 23 | CLOUDFLARE_EMAIL='' 24 | CLOUDFLARE_API_KEY='' 25 | 26 | # zone information 27 | DNS_ZONE_ID='' 28 | 29 | # record information 30 | DNS_RECORD_ID='' 31 | DNS_RECORD_NAME='' 32 | DNS_RECORD_TYPE='A' 33 | # Determines whether the zone will be proxied by Cloudflare. 34 | DNS_RECORD_PROXIED='false' 35 | 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | I'm really glad you're reading this, because working together is better than working alone! 4 | 5 | Here are some important resources: 6 | 7 | * [Cloudflare API](http://opengovernment.org/pages/developer) has all the technical details for interfacing with Cloudflare services 8 | * [Cloudflare "Tokens" support](https://support.cloudflare.com/hc/en-us/articles/200167836-Managing-API-Tokens-and-Keys) has details on the new API Token approach to access control over your Cloudflare account 9 | 10 | ## Testing 11 | 12 | Please test your code thoroughly including failure modes (e.g. connection down, invalid credentials, conflicting inputs, etc.) 13 | 14 | ## Submitting changes 15 | 16 | Please send a [GitHub Pull Request to opengovernment](https://github.com/opengovernment/opengovernment/pull/new/master) with a clear list of what you've done (read more about [pull requests](http://help.github.com/pull-requests/)). Please follow our coding conventions (below) and make sure all of your commits are atomic (one feature per commit). 17 | 18 | Always write a clear log message for your commits. One-line messages are fine for small changes, but bigger changes should look like this: 19 | 20 | $ git commit -m "A brief summary of the commit 21 | > 22 | > A paragraph describing what changed and its impact." 23 | 24 | Please include any necessary changes to documentation within your same, single commit. Also include your name in the Contributors sections of the README with a one-line description of your contribution. 25 | 26 | ## Coding conventions 27 | 28 | Review existing code and you'll get the hang of it. Strive for readability: 29 | 30 | * Use identation consist with existing code 31 | * Use if-then-else and similar block structures consistent with existing code 32 | * This is open source software. Consider the people who will read your code, and make it look nice for them. It's sort of like driving a car: Perhaps you love doing donuts when you're alone, but with passengers the goal is to make the ride as smooth as possible. 33 | 34 | Thanks! 35 | -------------------------------------------------------------------------------- /cloudflare_ddns: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CONFIG=$(dirname $0)/cloudflare_config 4 | 5 | if [ -r $CONFIG ]; then 6 | . $CONFIG 7 | else 8 | ( >&2 echo "Missing or unreadable $CONFIG" ) 9 | fi 10 | 11 | # script settings 12 | LOGFILE=$(basename $0).log 13 | THROTTLE_SECONDS=300 # terminate prematurely if run too often 14 | CLOUDFLARE_AUTH_HEADERS="X-Auth-Email: $CLOUDFLARE_EMAIL 15 | X-Auth-Key: $CLOUDFLARE_API_KEY" 16 | 17 | # cloudflare prefers token-based auth, so use that if available 18 | if [ -n $CLOUDFLARE_API_TOKEN ]; then 19 | CLOUDFLARE_AUTH_HEADERS="Authorization: Bearer $CLOUDFLARE_API_TOKEN" 20 | fi 21 | 22 | # user input IP address 23 | NEW_IP=$1 24 | 25 | log_output() 26 | { 27 | timestamp=$(date +"%Y-%m-%d %H:%M:%S") 28 | echo "$timestamp $1 $2" 29 | } >> $LOGFILE 30 | 31 | list_dns_records() 32 | { 33 | curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$DNS_ZONE_ID/dns_records" \ 34 | -H @- <<- HEADERS 35 | $CLOUDFLARE_AUTH_HEADERS 36 | HEADERS 37 | } 38 | 39 | list_dns_records_onsuccess() 40 | { 41 | log_output LIST_SUCCESS "$1" 42 | } 43 | 44 | list_dns_records_onfailure() 45 | { 46 | log_output LIST_FAILED "$1" 47 | } 48 | 49 | list_dns_records_onthrottled() 50 | { 51 | log_output LIST_THROTTLED 52 | } 53 | 54 | update_record() 55 | { 56 | curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$DNS_ZONE_ID/dns_records/$DNS_RECORD_ID" \ 57 | --data "{\"type\":\"$DNS_RECORD_TYPE\",\"name\":\"$DNS_RECORD_NAME\",\"content\":\"$NEW_IP\",\"ttl\":1,\"proxied\":$DNS_RECORD_PROXIED}" \ 58 | -H @- <<- HEADERS 59 | $CLOUDFLARE_AUTH_HEADERS 60 | Content-Type: application/json 61 | HEADERS 62 | } 63 | 64 | update_record_onsuccess() 65 | { 66 | /sbin/ddns_custom_updated 1 67 | log_output UPDATE_SUCCESS "$1" 68 | } 69 | 70 | update_record_onfailure() 71 | { 72 | /sbin/ddns_custom_updated 0 73 | log_output UPDATE_FAILED "$1" 74 | } 75 | 76 | update_record_onthrottled() 77 | { 78 | /sbin/ddns_custom_updated 0 79 | log_output UPDATE_THROTTLED 80 | } 81 | 82 | throttle() 83 | { 84 | # if we can't read the log file, assume it's a first run 85 | if [ ! -r $LOGFILE ]; then 86 | return 87 | fi 88 | 89 | last_run_time=$(date -d "$(sort -r $LOGFILE | \ 90 | grep '^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]' | \ 91 | grep -vm1 THROTTLED | cut -f 1)" +%s) 92 | current_time=$(date +%s) 93 | if [ $(expr $last_run_time + $THROTTLE_SECONDS) -gt $current_time ]; then 94 | $throttled 95 | exit 1 96 | fi 97 | } 98 | 99 | command=update_record 100 | success=update_record_onsuccess 101 | failure=update_record_onfailure 102 | throttled=update_record_onthrottled 103 | 104 | # if argument is the word 'list', query for a listing of all zone records 105 | if [ "$NEW_IP" = "list" ]; then 106 | command=list_dns_records 107 | success=list_dns_records_onsuccess 108 | failure=list_dns_records_onfailure 109 | throttled=list_dns_records_onthrottled 110 | fi 111 | 112 | throttle 113 | 114 | OUTPUT=$($command) 115 | 116 | if echo $OUTPUT | grep -q '"success"\s*:\s*true'; then 117 | $success "$OUTPUT" 118 | else 119 | $failure "$OUTPUT" 120 | fi 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Dynamic DNS Update Script for Asuswrt-Merlin (on supportd ASUS routers) 2 | 3 | The Asuswrt-Merlin custom firmware adds support for custom dynamic DNS providers to various ASUS routers. This is great for Cloudflare users because, although Cloudflare is not one of the built-in providers, we can add support for it. This guide and accompanying script do exactly that. Confirmed works for the RT-AC68U, RT-AC66U, RT-AC87U and RT-AX88U model routers. 4 | 5 | Features include: 6 | - Support for querying your Cloudflare DNS zone to determine record IDs 7 | - Handling of Merlin firmware success/failure callbacks 8 | - Logging of JSON responses for later inspection, and 9 | - Configurable rate-limiting to comply with Cloudflare API TOS 10 | 11 | 12 | ## Contributors 13 | 14 | - [@epicylon](https://github.com/epicylon) - Verified works on RT-AC66U 15 | - [@bengalih](https://github.com/bengalih) - Several contributions toward support for API Tokens, proxying, and other enhancements. 16 | - [@gumanov](https://github.com/gumanov) - Verified works on RT-AX88U 17 | - [@clayauld](https://github.com/clayauld) - Verified works on RT-AC87U 18 | - [@ttgapers](https://github.com/ttgapers) - Verified works on RT-AC86U & RT-AC68U. Updated script to run from /jffs/addons/ddns/cloudflare/ directory. 19 | 20 | ## How to Configure 21 | 22 | ### Prerequisites 23 | You should have your Merlin-enabled ASUS router configured for your network with Internet access. Since you've found this guide, it's also assumed you have a Cloudflare account managing your own domain, and you've already created a subdomain you will use for dynamic DNS. 24 | 25 | ### Configuration Overview 26 | Configuration of Cloudflare DDNS involves changes through the router web portal as well as changes made through the router shell. 27 | 1. [Enable shell access and JFFS partition](#enable-shell-access-and-jffs-partition) 28 | 2. [Install Cloudflare DDNS script](#install-cloudflare-ddns-script) 29 | 3. [Enable custom DDNS](#enable-custom-ddns) 30 | 4. [Verification](#verification) 31 | 5. [Clean up](#clean-up) 32 | 33 | Directions for disabling dynamic DNS and removal of the script and related files are at bottom. 34 | 35 | ##### Enable Shell Access and JFFS Partition 36 | In the router portal, under Administration -> System, 37 | - Basic Config -> Enable JFFS custom scripts and configs: Yes 38 | - Service -> Enable SSH: LAN only 39 | 40 | Save the configuration. Ensure you are able to SSH into your router using your router portal credentials (or via public key crypto, depending on configuration) before continuing. 41 | 42 | > Note: If SSH will be left enabled after installation, disallow password login, enable brute force protection, and use public keys for login to enhance security. 43 | 44 | ##### Install Cloudflare DDNS Script 45 | 1. Log into your router via SSH, and navigate to `/jffs/addons/ddns/cloudflare`. 46 | 2. Copy the `cloudflare_ddns` and `.cloudflare.example` files to that directory. 47 | 3. Rename `.cloudflare.example` to `.cloudflare`. 48 | 4. Edit `.cloudflare` with your [Cloudflare API token](https://blog.cloudflare.com/api-tokens-general-availability/) and zone ID from your Cloudflare portal. The script also supports the legacy "API Key plus account e-mail" method of authentication, but this method appears likely to be eliminated in future. 49 | 5. Run `chmod 700 cloudflare_ddns`. 50 | 6. Run `chmod 600 .cloudflare`. 51 | 7. Run `./cloudflare_ddns list`. 52 | 8. Step 7 should have resulted in the creation of a log file named `cloudflare_ddns.log`. Open the log file and review the JSON response object, which should be a listing of your Cloudflare DNS records for the zone ID specified in Step 4. 53 | > Note: If there is an error in the log file or no log file is present, ensure permissions are correct and that the text of the script is copied accurately. Double-check your Cloudflare credentials. If the error is from Cloudflare, you can review the text of the error in the JSON response and look for any error code online. 54 | 9. Edit `.cloudflare` with the DNS record information (i.e. ID, name and type) obtained from Step 8. Ensure your text matches exactly. 55 | 10. Run `./cloudflare_ddns 1.1.1.1`. 56 | 11. Review the log file for the result of the last execution. If you see a successful response, verify against the Cloudflare portal. Otherwise, review the errors and correct as necessary. 57 | > Note: You may get a throttled response if you have queried too quickly after Step 7. The script rate-limits to one query every 5 minutes. This is configurable in the `cloudflare_ddns` script or you can simply wait. 58 | 12. Ensure rate-limiting is working as expected by re-issuing the command in Step 10 a couple of times in quick succession and verifying that the log file shows frequent invocations are throttled. 59 | 13. If there is an existing /jffs/scripts/ddns-start script rename to ddns-start.bak (rollback if changing DNS providers) 60 | 14. If Steps 11 and 12 were successful, run `ln -s /jffs/addons/ddns/cloudflare/cloudflare_ddns /jffs/scripts/ddns-start`. This creates a symbolic link with the name expected by the router firmware. 61 | ##### Enable Custom DDNS 62 | In the router portal, under WAN -> DDNS, 63 | - Enable DDNS client: Yes 64 | - Server: Custom 65 | - Host name: `the DNS host name you're using in Cloudflare` 66 | - HTTPS/SSL Certificate: None 67 | 68 | Save the configuration. 69 | 70 | ##### Verification 71 | If all is configured correctly, you should see: 72 | 1. A "successful" message on the router portal on saving the configuration. I believe this is determined by the `/sbin/ddns_custom_updated` commands being called properly within the `cloudflare_ddns` script. 73 | 2. In the router portal, under System Log, you should see entries as below. 74 | ``` 75 | Nov 5 6:57 start_ddns: update CUSTOM , wan_unit 0 76 | Nov 5 6:57 custom_script: Running /jffs/scripts/ddns-start (args: x.x.x.x ) - max timeout = 120s 77 | Nov 5 6:57 ddns: Completed custom ddns update 78 | ``` 79 | 3. A new log file for the script should have been created in a /tmp folder and it should contain a successful log entry. Find the log file by running `find / -name ddns-start.log 2>&1`. 80 | 4. The Cloudflare portal should reflect the updated public IP address of your router. 81 | 82 | > Note: If any errors occur, review the router log file and the script log file for an indication of the error or manually re-run `./cloudflare_ddns list` and `./cloudflare_ddns 1.1.1.1` to identify and troubleshoot. 83 | 84 | ##### Clean Up 85 | Once everything is configured and working properly, you may delete the `cloudflare_ddns.log` file from the `/jffs/addons/ddns/cloudflare` directory on the router. If SSH access is no longer needed, disable SSH on the router portal for security (especially if password authentication was used). 86 | 87 | ## Script Removal 88 | To remove the script, the process is essentially reversed. 89 | 1. In the router portal, disable DDNS client and save. It may be worthwhile to restart your router to ensure any in-memory settings are cleared. 90 | 2. Log into the router via SSH and delete (in order): a) ddns-start, b) cloudflare_ddns, c) .cloudflare, d) cloudflare_ddns.log, and e) ddns-start.log. 91 | --------------------------------------------------------------------------------