Compare commits

..

14 Commits

Author SHA1 Message Date
root 3bed4c4bbc always uses @ for subdomain when on root 2026-06-29 12:18:13 +01:00
joeac 2060f8379f configures cache dir, config dir 2026-06-29 11:33:58 +01:00
joeac d0b389a4e6 CONN_DEVICE_NAME is configurable by env var 2026-06-29 11:19:01 +01:00
joeac d2716eda75 uses dig myip.opendns.com instead of curl icanhazip.com for ipv4 2026-06-29 11:18:34 +01:00
joeac 7c1d05b5ed works for subdomains I think? 2026-05-05 07:50:15 +01:00
joeac af72435df2 gitignores all REPEAT_CHECK_* 2026-05-05 07:49:37 +01:00
joeac 532e63e038 allows multiple domain names using second arg 2026-02-09 10:27:20 +00:00
joeac 3d2a0602e4 add README 2025-12-15 13:54:30 +00:00
joeac a7b5490ea9 gitignore REPEAT_CHECKs 2025-12-15 13:42:09 +00:00
joeac 00dfd4bc02 fixes 2025-12-15 13:41:01 +00:00
joeac 6722b2b23e rework 2025-12-15 12:03:22 +00:00
joeac 5fd6d6996e revert curl DIGITALOCEAN_DOMAIN 2025-12-13 19:35:49 +00:00
joeac 472b2ea2c8 store REPEAT_CHECK in the same folder 2025-12-13 19:35:10 +00:00
joeac 0d4e612433 curl DIGITALOCEAN_DOMAIN 2025-12-13 18:16:05 +00:00
5 changed files with 252 additions and 93 deletions
+1
View File
@@ -1,2 +1,3 @@
DIGITALOCEAN_TOKEN DIGITALOCEAN_TOKEN
DIGITALOCEAN_DOMAIN DIGITALOCEAN_DOMAIN
REPEAT_CHECK_*
+50
View File
@@ -0,0 +1,50 @@
# digitalocean_dyndns
Forked from [dc25/digitalocean_dyndns](https://github.com/dc25/digitalocean_dyndns).
This script automatically keeps your DNS records for a HTTPS website working
when you're hosting from a network with a dynamically allocated IP address, such
as a typical home network.
To use this script, first mark the shell files as runnable:
```sh
chmod +x ./dyndns.sh
chmod +x ./get_ip_addr.sh
```
Then to set things up, first write your domain name, e.g. `example.com`, to
`DIGITALOCEAN_DOMAIN`. Secondly, go to your DigitalOcean account and generate a
Personal Access Token with scopes for creating, reading, and deleting domains.
Write this to `DIGITALOCEAN_TOKEN`.
The script should now run. Try it out:
```sh
./dyndns.sh 4 # for IPv$
./dyndns.sh 6 # for IPv$
```
Go to the control panel in DigitalOcean for your domain, and check the A and
AAAA records are what you expect. If they are, the next step is to get this
script to run regularly.
You could do this in a number of ways. Probably the easiest is with crontab.
Run `crontab -e` to open your crontab for editing, and add these two lines:
```cron
* * * * * /home/.../dyndns.sh 4 > /tmp/dyndns.sh.4.log 2>&1
* * * * * /home/.../dyndns.sh 6 > /tmp/dyndns.sh.6.log 2>&1
```
Wait one minute, and check the logs. If you want to see that it works, delete
the REPEAT_CHECK_A and REPEAT_CHECK_AAAA files and wait for the script to run
again. You may also want to have an MTA set up, as cron will try to send any
unhandled output to your inbox.
If you want to use this script for more than one domain, you can supply a domain
name as a second argument. For example:
```sh
./dyndns.sh 4 example.com
```
-93
View File
@@ -1,93 +0,0 @@
#! /bin/bash
# Work with DigitalOcean api to update "A" record for a domain.
# Store DigitalOcean API token in file : DIGITALOCEAN_TOKEN
# Store domain to be updated in file : DIGITALOCEAN_DOMAIN
SCRIPT="${BASH_SOURCE[0]}"
SCRIPT_DIR=$(builtin cd "${SCRIPT%/*}" && pwd )
SCRIPT_NAME=${SCRIPT##*/}
REPEAT_CHECK_DIR=$HOME/repeat_check
REPEAT_CHECK=$REPEAT_CHECK_DIR/$SCRIPT_NAME # file to save argument in if successful
DIGITALOCEAN_DOMAIN=`cat $SCRIPT_DIR/DIGITALOCEAN_DOMAIN`
DIGITALOCEAN_TOKEN=`cat $SCRIPT_DIR/DIGITALOCEAN_TOKEN`
# Get IP from command line if available. Otherwise use curl to get public IP.
IP=""
if [ "$#" == "1" ]; then
IP=$1
else
IP=`curl -4 icanhazip.com 2> /dev/null`
fi
# If $IP does not look like a valid address then complain and exit.
# For regular expression, thank you to : https://tecadmin.net/shell-script-validate-ipv4-addresses/
if ! [[ $IP =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo Invalid IP address format: $IP
exit
fi
get_a_record_ids() {
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/domains/$DIGITALOCEAN_DOMAIN/records" 2> /dev/null | jq '.domain_records | map(select(.type == "A" and .name == "@")) | map(.id) | .[]'
}
add_a_record() {
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-d "{\"type\":\"A\" \
,\"name\":\"@\" \
,\"data\":\"$IP\" \
,\"priority\":null \
,\"port\":null \
,\"ttl\":1800 \
,\"weight\":null \
,\"flags\":null \
,\"tag\":null \
}" \
"https://api.digitalocean.com/v2/domains/$DIGITALOCEAN_DOMAIN/records" 2> /dev/null | jq '.domain_record.id'
}
delete_record_by_id() {
echo deleting existing A record for $DIGITALOCEAN_DOMAIN
curl -X DELETE \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/domains/$DIGITALOCEAN_DOMAIN/records/$1" > /dev/null 2>&1
}
replace_a_record() {
for id in `get_a_record_ids`; do
delete_record_by_id $id
done
new_record_id=`add_a_record`
if [ "$new_record_id" == "null" ] ; then
echo unable to add new A record for $DIGITALOCEAN_DOMAIN with value : $1
return 1
fi
echo added new A record for $DIGITALOCEAN_DOMAIN with value : $1
}
# exit if previous successful call was with same argument
if [[ -e $REPEAT_CHECK ]]; then
PREVIOUS_IP=`cat $REPEAT_CHECK`
if [[ "$IP" == "$PREVIOUS_IP" ]]; then
echo previous update was also with argument: \"$IP\" so exiting now to avoid duplicate work.
exit
fi
fi
# attempt to replace A record and, if successful, save arguments to check against next time
if replace_a_record $IP; then
# if successful then save arguments.
echo update worked so saving arguments \"$IP\" in $REPEAT_CHECK
mkdir -p $REPEAT_CHECK_DIR
echo "$IP" > $REPEAT_CHECK
fi
Executable
+149
View File
@@ -0,0 +1,149 @@
#!/bin/sh
# Work with DigitalOcean api to update "A" and "AAAA" records for a domain.
# Store DigitalOcean API token in file : DIGITALOCEAN_TOKEN
# Store domain to be updated in file : DIGITALOCEAN_DOMAIN
ip_version="$1"
if [ -z "${ip_version:-}" ] || ([ "$ip_version" != 4 ] && [ "$ip_version" != 6 ]); then
echo "Error: no valid IP version provided" >&2
echo "Usage: dyndns.bash <4|6> [domain]" >&2
exit 1
fi
SCRIPT="$0"
SCRIPT_DIR=$(cd "${SCRIPT%/*}" && pwd )
SCRIPT_NAME=${SCRIPT##*/}
CACHE_DIR=${CACHE_DIR:-~/.cache/dyndns}
CONFIG_DIR=${CONFIG_DIR:-~/.config/dyndns}
mkdir -p ${CACHE_DIR}
mkdir -p ${CONFIG_DIR}
IP="$($SCRIPT_DIR/get_ip_addr.sh $ip_version)"
if [ -z "$IP" ]; then
echo "Error: could not find IP address" >&2
exit 1
fi
DIGITALOCEAN_TOKEN=`cat ${CONFIG_DIR}/DIGITALOCEAN_TOKEN`
DOMAIN=${2:-`cat ${CONFIG_DIR}/DIGITALOCEAN_DOMAIN`}
SUBDOMAIN=`echo $DOMAIN | sed "s/[^.]\+\.[^.]\+$//g"`
if [ -z "$SUBDOMAIN" ]; then
SUBDOMAIN="@"
ROOT_DOMAIN=$DOMAIN
elif [ "$SUBDOMAIN" = "@." ]; then
SUBDOMAIN="@"
ROOT_DOMAIN=`echo $DOMAIN | sed "s/^@.//"`
else
SUBDOMAIN=`echo $SUBDOMAIN | sed "s/\.$//g"`
ROOT_DOMAIN=`echo $DOMAIN | sed "s/^${SUBDOMAIN}\.//g"`
fi
REPEAT_CHECK_DIR=${CACHE_DIR}
REPEAT_CHECK_A=$REPEAT_CHECK_DIR/REPEAT_CHECK_${SUBDOMAIN}.${ROOT_DOMAIN}_A
REPEAT_CHECK_AAAA=$REPEAT_CHECK_DIR/REPEAT_CHECK_${SUBDOMAIN}.${ROOT_DOMAIN}_AAAA
REPEAT_CHECK="$([ $ip_version = 4 ] && echo $REPEAT_CHECK_A || echo $REPEAT_CHECK_AAAA)"
get_a_record_ids() {
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/domains/$ROOT_DOMAIN/records" 2> /dev/null | jq '.domain_records | map(select(.type == "A" and .name == "$SUBDOMAIN")) | map(.id) | .[]'
}
get_aaaa_record_ids() {
curl -X GET \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/domains/$ROOT_DOMAIN/records" 2> /dev/null | jq '.domain_records | map(select(.type == "AAAA" and .name == "$SUBDOMAIN")) | map(.id) | .[]'
}
add_a_record() {
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-d "{\"type\":\"A\" \
,\"name\":\"$SUBDOMAIN\" \
,\"data\":\"$IP\" \
,\"priority\":null \
,\"port\":null \
,\"ttl\":1800 \
,\"weight\":null \
,\"flags\":null \
,\"tag\":null \
}" \
"https://api.digitalocean.com/v2/domains/$ROOT_DOMAIN/records" 2> /dev/null | jq '.domain_record.id'
}
add_aaaa_record() {
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-d "{\"type\":\"AAAA\" \
,\"name\":\"$SUBDOMAIN\" \
,\"data\":\"$IP\" \
,\"priority\":null \
,\"port\":null \
,\"ttl\":1800 \
,\"weight\":null \
,\"flags\":null \
,\"tag\":null \
}" \
"https://api.digitalocean.com/v2/domains/$ROOT_DOMAIN/records" 2> /dev/null | jq '.domain_record.id'
}
delete_record_by_id() {
echo deleting existing A/AAAA record for ${SUBDOMAIN}.${ROOT_DOMAIN}
curl -X DELETE \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
"https://api.digitalocean.com/v2/domains/$ROOT_DOMAIN/records/$1" > /dev/null 2>&1
}
replace_a_records() {
for id in `get_a_record_ids`; do
delete_record_by_id $id
done
new_record_id=`add_a_record`
if [ "$new_record_id" = "null" ] ; then
echo unable to add new A record for ${SUBDOMAIN}.${ROOT_DOMAIN} with value: $IP
return 1
fi
echo added new A record for ${SUBDOMAIN}.${ROOT_DOMAIN} with value: $IP
}
replace_aaaa_records() {
for id in `get_aaaa_record_ids`; do
delete_record_by_id $id
done
new_record_id=`add_aaaa_record`
if [ "$new_record_id" = "null" ] ; then
echo unable to add new AAAA record for ${SUBDOMAIN}.${ROOT_DOMAIN} with value: $IP
return 1
fi
echo added new AAAA record for ${SUBDOMAIN}.${ROOT_DOMAIN} with value: $IP
}
replace_records() {
[ $ip_version = 4 ] && replace_a_records || replace_aaaa_records
}
# exit if previous successful call was with same argument
if [ -e $REPEAT_CHECK ]; then
PREVIOUS_IP=`cat $REPEAT_CHECK`
if [ "$IP" = "$PREVIOUS_IP" ]; then
echo previous update was also with argument: \"$IP\" so exiting now to avoid duplicate work.
exit
fi
fi
# attempt to replace records and, if successful, save arguments to check against next time
if replace_records $IP; then
# if successful then save arguments.
echo update worked so saving arguments \"$IP\" in $REPEAT_CHECK
mkdir -p $REPEAT_CHECK_DIR
echo "$IP" > $REPEAT_CHECK
fi
Executable
+52
View File
@@ -0,0 +1,52 @@
#!/bin/sh
ip_version="$1"
if [ -z "${ip_version:-}" ] || ([ "$ip_version" != 4 ] && [ "$ip_version" != 6 ]); then
echo "Usage: get_ip_addr <4|6>" >&2
exit 1
fi
if [ $ip_version = 4 ]; then
dig +short myip.opendns.com @resolver1.opendns.com \
&& exit 0 \
|| exit 1
fi
set -eu
CONN_NAME=${CONN_NAME:-nevis}
CONN_DEVICE_NAME="${CONN_DEVICE_NAME:-$(nmcli --fields NAME,DEVICE con show | grep ^$CONN_NAME | sed 's/\s\+/ /g' | cut -d' ' -f 2)}"
if [ -z "${CONN_DEVICE_NAME:-}" ]; then
echo "Could not find the connection device name for connection: \"$CONN_NAME\"" >&2
exit 1
fi
device_line_no="$(ip addr | grep -En ^[0-9]+:\ $CONN_DEVICE_NAME | cut -d':' -f 1)"
if [ -z "${device_line_no:-}" ]; then
echo "Could not find device: \"$CONN_DEVICE_NAME\" in output of ip addr" >&2
exit 1
fi
ip_addr_output_for_device="$(ip addr | tail -n +$(($device_line_no + 1)))"
device_no="$(ip addr | tail -n +$device_line_no | head -n 1 | cut -d':' -f 1)"
next_device_no=$(($device_no + 1))
next_device_line_no="$(ip addr | grep -En ^${next_device_no}:\ | cut -d':' -f 1)"
if [ -n "${next_device_line_no:-}" ]; then
len=$(($next_device_line_no - ($device_line_no + 1)))
ip_addr_output_for_device="$(echo $ip_addr_output_for_device | head -n $len)"
fi
ipv4_regex="([a-z0-9]{1,3}\.){3}[a-z0-9]{1,3}"
ipv6_regex="([a-z0-9]{1,4}:){7}[a-z0-9]{1,4}"
if [ "$ip_version" = "4" ]; then
ip_regex="$ipv4_regex"
else
ip_regex="$ipv6_regex"
fi
ip="$(echo "$ip_addr_output_for_device" | grep "scope global" | grep -oE $ip_regex | head -n 1)"
if [ -z "${ip:-}" ]; then
echo "Could not find global IP in output of ip addr under device: $CONN_DEVICE_NAME ($CONN_NAME)"
exit 1
fi
echo "$ip"