I have had a Netgear WNDR3700v2 router for a few years already and I bought this model specifically because I could flash it with the open-source DD-WRT firmware. The reasons remain the same as the ones I had in 2006—the key ones being: the ability to run a Dnsmasq DNS/DHCP server for my local network; and the ability to update a single Dynamic DNS address so that I can access my home network when I’m on the go.

Back in the day, I used to use the free DynDNS for my dynamic DNS needs, and this service was perfectly supported by DD-WRT out of the box. (I think even the upstream firmware did too.) At some point, however, DynDNS became a paid service and, around that same time, I got my own domain via Google Domains. It wasn’t until later that I found out that Google Domains also provides dynamic addresses but, unfortunately, DD-WRT ships with a very old version of the inadyn client that does not support this provider.

I hadn’t bothered to fix this since then because, even though I was not paying for a static IP in NYC, Spectrum seemed to maintain my IP for years in a row. But I’ve just moved to a new city, I have had to sign up for a different provider, and I do not know what to expect. And, you know… regardless of expectations, I do not pay for a static IP, so even if it seems stable over time, it could change at any point—probably at the most inconvenient time.

As a result, I spent some time figuring out how to convince DD-WRT to update my Google Domains dynamic entry. The API to update a dynamic entry in Google Domains seems trivial: we just need to send a POST request with the correct username, password, domain name, and new IP… and voilà. And the firmware ships with curl and cron, so we should be able to tie these together and create our own updater.

There are actually hints on how to do this in random forum and blog posts through the Internet, but, well… many suffer from misleading directions as you might expect. It took me a little while to figure out the right incantation of settings, but I think I’ve finally got it right, so this post contains my lab notes for future reference.

Here is what we will do and why some of the guidelines elsewhere are broken:

  1. We will set up a periodic cron job to update our dynamic DNS record using a curl call. The crontab format is “different” than what you might be used to as it takes a username after the time specifications. Failing to provide root as the username will prevent the job from running without any traces as to what went wrong, and as you can imagine, not all advice out there tells you this.

  2. We have to interpret the Google Domains responses. In other words: we cannot blindly issue update requests to Google Domains every time we run our job, as we risk being banned due to abuse. For this reason, the update isn’t a simple curl call, so we’ll need an auxiliary script to hold our logic. We will use the “Custom commands” logic to hold our script. This script used to be written as /tmp/custom.sh in old DD-WRT versions but is now written to /tmp/.rc_custom (my build is DD-WRT v3.0-r44715 std (11/03/20)).

  3. We want our changes to persist across router reboots, which means all the instructions that tell us to change files in /tmp/ are insufficient: those changes will be discarded the next time we reboot the router. For context: the files under /tmp/ are automatically generated at boot time from the only non-volatile storage we have, the NVRAM, so any configuration we want to persist has to be stored there in some way or another.

Configuration steps

With all of that in mind, here are the specific steps that worked for me:

  1. Visit the Google Domains registrar, click on your domain, and then open the DNS panel. Under Synthetic records, look for your Dynamic DNS entry, expand it for details, and click on View credentials. Note the username and password, as we’ll need them soon.

  2. In DD-WRT, go to Setup > DDNS and ensure the DDNS Service option is set to Disable.

  3. In DD-WRT, Go to Administration > Commands, and type the following script in the text box:

    #! /bin/sh
    
    PATH=/bin:/usr/bin:/sbin:/usr/sbin
    
    GOOGLE_DOMAINS_CACHE=/tmp/cached-ddns.txt
    GOOGLE_DOMAINS_USERNAME=____YOUR_USERNAME_GOES_HERE____
    GOOGLE_DOMAINS_PASSWORD=____YOUR_PASSWORD_GOES_HERE____
    GOOGLE_DOMAINS_DOMAIN=____YOUR_DYNAMIC_FQDN_GOES_HERE____
    
    update_google_domains() {
        local my_ip="$(nvram get wan_ipaddr)"
        local cached_ip=
        if [ -f "${GOOGLE_DOMAINS_CACHE}" ]; then
            cached_ip="$(cat "${GOOGLE_DOMAINS_CACHE}")"
        fi
    
        if [ "${my_ip}" != "${cached_ip}" ]; then
            curl -X POST -A Curl -H "Content-length: 0" "https://${GOOGLE_DOMAINS_USERNAME}:${GOOGLE_DOMAINS_PASSWORD}@domains.google.com/nic/update?hostname=${GOOGLE_DOMAINS_DOMAIN}&myip=${my_ip}" >/tmp/curl.out
            cat /tmp/curl.out
            if grep -Eq "^(good|nochg)" /tmp/curl.out; then
                printf "%s" "${my_ip}" >"${GOOGLE_DOMAINS_CACHE}"
            fi
            rm -f /tmp/curl.out
        fi
    }
    
    case "${1}" in
        update-google-domains)
            update_google_domains
            ;;
    
        *)
            echo "Unknown command" 1>&2
            exit 1
            ;;
    esac
    

    WARNING: Make sure to set GOOGLE_DOMAINS_USERNAME, GOOGLE_DOMAINS_PASSWORD and GOOGLE_DOMAINS_DOMAIN to the right values.

    Then click Save Custom. This will cause DD-WRT to write your script to /tmp/custom.sh on old versions of the firmware and to /tmp/.rc_custom in more recent ones. Note that we can only have a single script—hence why I parameterized it.

  4. In DD-WRT, go to Administration > Management and, under the Cron section, enable the cron service and then type the following in the Additional Cron Jobs section:

    */5 * * * * root /tmp/.rc_custom update-google-domains
    

    WARNING: Note the root field value after the various time specifications. This must be present or else your command won’t be correctly parsed.

  5. In DD-WRT, click Apply Settings.

To confirm that things are working, SSH into the router and check if the /tmp/cached-ddns.txt file has been created after five minutes. Then go back to the Google Domains portal and verify that the domain entry was correctly updated. If not, something has gone wrong.

Troubleshooting

Here are some tips that helped me:

  1. Log into the router via SSH.

  2. Confirm that any settings you change in the UI appear in the output of nvram show. Remember that the firmware will use these settings to populate the contents of /tmp/ at boot time.

  3. Look for the location of the custom script in case it is not /tmp/.rc_custom in your build. Something like grep -R update-google-domains /tmp will reveal where it is.

  4. Check if cron is running (ps | grep cron) and that the custom job we set up appears in the crontab files (grep update-google-domains /tmp/cron.d/*).

  5. Run the update by hand and see if it complains: /tmp/.rc_custom update-google-domains.