Skip to content

Instantly share code, notes, and snippets.

@akhoury6
Created March 1, 2024 20:12
Show Gist options
  • Save akhoury6/b67d96973c37b4b7bb39767ba38e220e to your computer and use it in GitHub Desktop.
Save akhoury6/b67d96973c37b4b7bb39767ba38e220e to your computer and use it in GitHub Desktop.
DigitalOcean DDNS Script
#!/usr/bin/env bash
##########################
## This script dynamically updates a DNS record on DigitalOcean DNS
## Make sure that the 'jq' utility is installed before using this script
## https://jqlang.github.io/jq/
##
## LICENSE: GPLv3
##
## (c) 2020 Andrew Khoury
## akhoury@live.com
##
###### BEGIN CONFIG ######
# Digital Ocean API Token
do_token="$(cat ${HOME}/.do_token)"
# Hostname for DNS server. This is the 'www' in 'www.example.com', and can be set to '@' for the root domain
do_dns_hostname="$1"
# Domain name for DNS server. This is 'example.com' in 'www.example.com'
do_dns_domain="$2"
# Either set this variable to an IP address directly, or use one of the following keywords
# <ip> : Set an IPv4 address directly, in the format 255.255.255.255
# public : Uses checkip.dyndns.com to determine the public IP for this computer
# gateway : Uses the IP address of the default gateway of this computer
# <iface> : Uses the specified address for a local interface (i.e. eth0)
ip_addr_or_src="$3"
# TTL for the DNS record
record_ttl="3600"
# Logfile Location
logging="true"
logfile="/var/log/ddns"
# Log Rotation
logrotate="true"
logrotate_config_file="/etc/logrotate.d/ddns"
logrotate_file_content=$(cat <<EOF
${logfile} {
monthly
create 0660 root odroid
rotate 4
dateext
}
EOF
)
# Define a callback function upon success/failure of the script. This is useful when
# running the script on a router, as some routers provide scripts to interface with
# the UI.
# The variable $1 will be provided as a 0 on failure and a 1 on success
function callback {
which /sbin/ddns_custom_updated && /sbin/ddns_custom_updated $1
}
###### END CONFIG ######
###### BEGIN LOGGING ######
_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
## Set up logging
if [ "${logging}" == "true" ] && [ ! -f "${logfile}" ]; then
if [ ! "$(id -u)" == "0" ]; then
echo "Can't create logfile; this script must be run as root to create the logfile."
exit 1
elif [ -w "$(dirname "${logfile}")" ]; then
touch "${logfile}"
chmod 0660 "${logfile}"
chown root:odroid "${logfile}"
echo "Created logfile."
else
echo "Logging configured but the logfile could not be created. Please disable logging or create the file manually at: ${logfile}"
exit 1
fi
fi
## Ensure the logfile is writeable if logging is enabled
if [ "${logging}" == "true" ] && [ ! -w "${logfile}" ]; then
echo "Logging is enabled but the logfile is not writable. Please disable logging or fix permissions for the file: ${logfile}"
exit 1
fi
# Starting from here, everything can be output to logs, so this is the output function
function logged_output {
output="$(date +"%m-%d-%y -- %T") -- $1"
[ "${logging}" == "true" ] && [ -f "${logfile}" ] && [ -w "${logfile}" ] && echo "${output}" | tee -a "${logfile}"
[ "${logging}" != "true" ] && echo "${output}"
}
# If log rotation is enabled, ensure that it is configured
if [ "${logrotate}" == "true" ] && [ ! -f "${logrotate_config_file}" ]; then
if [ ! "$(id -u)" == "0" ]; then
logged_output "Can't enable log rotation; this script must be run as root to configure it. Continuing without log rotation."
logrotate="false"
elif [ -w "$(dirname "${logrotate_config_file}")" ]; then
echo "${logrotate_file_content}" > "${logrotate_config_file}"
chmod 0440 "${logrotate_config_file}"
chown root:root "${logrotate_config_file}"
service logrotate restart
logged_output "Enabled log rotation."
else
logged_output "Log rotation configured but could not create the config: ${logrotate_config_file}"
logged_output "Continuing without log rotation."
fi
# Disable log rotation automatically
elif [ "${logrotate}" != "true" ] && [ -f "${logrotate_config_file}" ]; then
if [ ! "$(id -u)" == "0" ]; then
logged_output "Can't remove log rotation automatically. Please re-run this script as root."
exit 1
else
rm -f "${logrotate_config_file}"
service logrotate restart
logged_output "Removed log rotation."
fi
fi
###### END LOGGING ######
###### BEGIN VALIDATION ######
local_interfaces="$(ip link show | awk '/[0-9]+: / {print $2}' | tr -d ':')"
function valid_ip() {
local ip=$1
local stat=1
if [[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
OIFS=$IFS
IFS='.'
ip=($ip)
IFS=$OIFS
[[ ${ip[0]} -le 255 && ${ip[1]} -le 255 && ${ip[2]} -le 255 && ${ip[3]} -le 255 ]]
stat=$?
fi
return $stat
}
exit_with_errors="false"
[[ ! "${do_token}" =~ ^dop_v1_[A-Fa-f0-9]+$ ]] && exit_with_errors="true" && logged_output "DigitalOcean API Token is not in a valid format."
[[ ! "${do_dns_hostname}" =~ ^([A-Za-z0-9-]+|@)$ ]] && exit_with_errors="true" && logged_output "Hostnames can only contain letters, numbers, and hyphens, or be the '@' symbol."
[[ ! "${do_dns_domain}" =~ ^[A-Za-z0-9-]+\.[A-Za-z0-9-]+$ ]] && exit_with_errors="true" && logged_output "Domain names can only contain letters, numbers, and hyphens."
[[ ! "${record_ttl}" =~ ^[0-9]+$ ]] && exit_with_errors="true" && logged_output "Record TTL must be set to a number, in seconds"
if [[ ! "${ip_addr_or_src}" =~ ^(public|gateway)$ ]]; then
if ! echo $local_interfaces | grep "${ip_addr_or_src}" > /dev/null; then
if ! valid_ip "${ip_addr_or_src}"; then
exit_with_errors="true"
logged_output "Specified IP address or source is invalid: ${ip_addr_or_src}"
fi
fi
fi
[ "${exit_with_errors}" == "true" ] && exit 1
###### END VALIDATION ######
###### BEGIN PREPARE DATA ######
#city_name="$(curl -s https://ipvigilante.com/$(curl -s https://ipinfo.io/ip) | tr ',' "\n" | head -n 7 | tail -n 1 | cut -d':' -f 2 | sed s%\"%%g | tr '[:upper:]' '[:lower:]')"
#city_name="$(curl -s https://ipvigilante.com/$(curl -s https://ipinfo.io/ip) | jq '.data.city_name' | sed s%\"%%g | tr '[:upper:]' '[:lower:]')"
## Get IP address
if [ "${ip_addr_or_src}" == "public" ]; then
logged_output "Using Public IP Address"
my_ip="$(curl -s http://checkip.dyndns.com/ | cut -d' ' -f 6 | cut -d'<' -f 1)"
# my_ip="$(dig +short myip.opendns.com @resolver1.opendns.com)"
# my_ip="$(dig TXT +short o-o.myaddr.l.google.com @ns1.google.com)"
# my_ip="$(dig +short txt ch whoami.cloudflare @1.0.0.1)"
# my_ip="$(dig -6 TXT +short o-o.myaddr.l.google.com @ns1.google.com)"
elif [ "${ip_addr_or_src}" == "gateway" ]; then
logged_output "Using Gateway IP Address"
my_ip="$(ip route | awk '/default / {print $3}')"
elif echo $local_interfaces | grep "${ip_addr_or_src}" > /dev/null; then
logged_output "Using IP Address for interface ${ip_addr_or_src}"
my_ip="$(ip -f inet addr show ${ip_addr_or_src} | awk '/inet / {print $2}' | cut -d'/' -f1)"
elif valid_ip "${ip_addr_or_src}"; then
logged_output "Using IP Address provided manually"
my_ip="${ip_addr_or_src}"
fi
if ! valid_ip "${my_ip}"; then
logged_output "IP address lookup failed. Aborting."
exit 1
fi
## Get DigitalOcean DNS record ID for hostname/domain
do_dns_domain_records=$(curl -s -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${do_token}" \
"https://api.digitalocean.com/v2/domains/${do_dns_domain}/records")
if command -v "jq" &> /dev/null; then
do_dns_record_id=$(echo "${do_dns_domain_records}" | jq ".domain_records[] | select(.name == \"${do_dns_hostname}\") .id")
else
do_dns_record_id=$(echo "${do_dns_domain_records}" | tr "," "\n" | grep "${do_dns_hostname}" -B2 | head -n1 | cut -d":" -f2)
fi
###### END PREPARE DATA ######
###### BEGIN UPDATE ######
## If DNS record does not exist, create it
if [ -z "${do_dns_record_id}" ]; then
req=$(curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${do_token}" \
-d "{\"type\":\"A\",\"name\":\"${do_dns_hostname}\",\"data\":\"${my_ip}\",\"priority\":null,\"port\":null,\"ttl\":${record_ttl},\"weight\":null,\"flags\":null,\"tag\":null}" \
"https://api.digitalocean.com/v2/domains/${do_dns_domain}/records")
else # Update if it exists
req=$(curl -s -X PUT \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${do_token}" \
-d "{\"data\": \"${my_ip}\"}" \
"https://api.digitalocean.com/v2/domains/${do_dns_domain}/records/${do_dns_record_id}")
fi
if command -v "jq" &> /dev/null; then
res=$(echo "$req" | jq -r .domain_record.data)
else
res=$(echo "$req" | cut -d':' -f 6 | cut -d'"' -f 2)
fi
###### END UPDATE ######
###### BEGIN OUTPUT ######
## Output results
if [ "$my_ip" == "$res" ]; then
logged_output "Successfully updated ${do_dns_hostname}.${do_dns_domain} to ${my_ip} using ${ip_addr_or_src} address"
callback 1
else
logged_output "Failed updating ${do_dns_hostname}.${do_dns_domain} to ${my_ip} using ${ip_addr_or_src} address"
callback 0
fi
###### END OUTPUT ######
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment