Created
March 1, 2024 20:12
-
-
Save akhoury6/b67d96973c37b4b7bb39767ba38e220e to your computer and use it in GitHub Desktop.
DigitalOcean DDNS Script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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