Namecheap offers a free dynamic DNS service with the purchase of a domain. They also offer a Windows client to update DDNS records, but if you are in a Linux or macOS environment, you're out of luck.

To improve upon my Bash scripting skills, I decided to write my own DDNS update script. The script was written for one of my CentOS 7 servers, but it should work on most versions of Linux and macOS, provided the dependencies are installed.

Installation

First, we need to install the dependencies. In CentOS 7, everything is installed in a Minimal installation except for dig, which is included in bind-utils.

The following commands assume this is a fresh installation of CentOS. You do not, however, need to dedicate an entire VM to this script, and if you are using an existing VM that is already up to date, you can skip the second and third commands.

yum install -y bind-utils
yum update -y
reboot

Next, we are going to create a service account without root privileges for running the script.

adduser ddns -s /sbin/nologin

This command creates a directory for the script.

mkdir /opt/ddns

This command creates the script itself. Be sure to modify the variables near the beginning of the script.

cat >/opt/ddns/ddns.sh <<"EOL"
#!/bin/bash

## Namecheap DDNS update client for CentOS 7
## Written by Ian Harrier

###
# Variables
# These need to be customized to your environment
#
# FQDN
#   The FQDN of the (A) record to be updated
#   Supports either 'domain.tld' or 'subdomain.domain.tld'
# PASSWORD
#   The DDNS update password from Namecheap
# NAME_SERVER
#   The DNS server used to resolve the DDNS (A) record
#   Recommended to use one of the domain's name servers
###

FQDN='subdomain.domain.tld'
PASSWORD='<secret password from Namecheap admin console>'
NAME_SERVER='dns1.registrar-servers.com'

###
# Start
###

echo -n "Starting at "
date +%Y-%m-%d\ %H:%M:%S

###
# Retrieve the current public IPv4 address from ident.me
# For more information: http://api.ident.me/
###

IPv4_CURRENT=$(curl -s v4.ident.me)
echo "The current IPv4 address is: $IPv4_CURRENT"

###
# Retrieve the current public IPv4 address from the DDNS entry
#
# The query is built as follows:
#   1. resolve using this external DNS server ( '@$NAME_SERVER' )
#   2. name to resolve ( '$FQDN' )
#   3. get only IPv4 address(es) ( 'A' )
#   4. get only the IP address ( '+short' )
###

IPv4_DDNS=$(dig @$NAME_SERVER $FQDN A +short)
echo "The DDNS (A) record reports: $IPv4_DDNS"

###
# Compare the results
###

if [[ "$IPv4_CURRENT" == "$IPv4_DDNS" ]]; then
	echo -n "IPv4 addresses match. No need to update. Exiting at "
	date +%Y-%m-%d\ %H:%M:%S
	echo
	exit 0
else
	echo "IPv4 addresses DO NOT match. Need to update."
fi

###
# Generate $HOST and $DOMAIN
###

# If $FQDN is in the format 'domain.tld' (i.e. one occurrence of '.')
if [[ $(echo $FQDN | awk 'BEGIN{FS="."} {print NF?NF-1:0}') == 1 ]]; then
	HOST='@'
	DOMAIN=$FQDN
fi

# If $FQDN is in the format 'subdomain.domain.tld' (i.e. two occurrences of '.')
if [[ $(echo $FQDN | awk 'BEGIN{FS="."} {print NF?NF-1:0}') == 2 ]]; then
	HOST=$(echo $FQDN | awk -F "." '{print $1}')
	DOMAIN=$(echo $FQDN | awk -F "." '{print $2 "." $3}')
fi

###
# Update DDNS
###

echo "Updating $FQDN to $IPv4_CURRENT . . ."
UPDATE_RESULT=$(curl -s "https://dynamicdns.park-your-domain.com/update?host=$HOST&domain=$DOMAIN&password=$PASSWORD&ip=$IPv4_CURRENT")

###
# Report error(s)
###

UPDATE_ERRORS=$(xmllint --xpath "string(//ErrCount)" - <<<"$UPDATE_RESULT")
echo "There were $UPDATE_ERRORS error(s)."

if [[ $UPDATE_ERRORS -gt 0 ]]; then
	echo "Error(s):"
	for (( i = 1; i <= $UPDATE_ERRORS; i++ )); do
		echo -n " $i. "
		xmllint --xpath "string(//Err$i)" - <<<"$UPDATE_RESULT"
	done
	echo
fi

###
# Exit
###

echo -n "Finished. Exiting at "
date +%Y-%m-%d\ %H:%M:%S
echo

exit 0
EOL

The following two commands have to do with permissions. The first makes the script executable. The second gives ownership of the /opt/ddns directory and its contents to the ddns service account.

chmod +x /opt/ddns/ddns.sh
chown -R ddns:ddns /opt/ddns

This script also has some basic output built in, so we will create a directory for the log and give ownership of the directory to the ddns service account.

mkdir /var/log/ddns
chown ddns:ddns /var/log/ddns

To allow this script to run on a schedule, we will create a cron job under the ddns service account. In this example, the script will run every five minutes, indicated by */5. There are many articles on the Internet documenting the functionality of cron, so instead of re-describing it here, I would recommend taking a look at this article if you have further questions about creating cron jobs.

crontab -u ddns -e
*/5 * * * * /opt/ddns/ddns.sh >> /var/log/ddns/ddns.log

In order to verify the creation of the job, the following command will list all cron jobs under the ddns service account.

crontab -u ddns -l

Conclusion

That's about it. Whenever your public IP address changes, this script will update your Namecheap DDNS record. If you want to see what the script is doing, you can run the following command to look at the log.

cat /var/log/ddns/ddns.log