Skip to content

Instantly share code, notes, and snippets.

@zsimic
Last active November 20, 2023 02:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zsimic/c39dd9686c6d6b0d149a67ff23286b99 to your computer and use it in GitHub Desktop.
Save zsimic/c39dd9686c6d6b0d149a67ff23286b99 to your computer and use it in GitHub Desktop.
Linode dynamic dns on ubiquiti edge router
#!/bin/bash
# See https://gist.github.com/zsimic/c39dd9686c6d6b0d149a67ff23286b99 for docs on how to use
# Note: you can invoke with LOGFILE= for troubleshooting
[ -z "$LOGFILE" ] && LOGFILE=/var/log/messages
if [ -z "$1" -a -f $LOGFILE ]; then # Pass any command line arg to avoid the logging redirect
exec $0 run 1>> $LOGFILE 2>&1
fi
function do_log {
echo "$(date +'%h %e %T') $(hostname) linode-ddns: $@"
}
function usage {
do_log "Usage error: $@"
exit 1
}
# Note: you can invoke with CFGFILE= for troubleshooting
[ -z "$CFGFILE" ] && CFGFILE=~/.ssh/linode-ddns-cfg.sh
CFGFOLDER=$(dirname $CFGFILE)
IPFILE="$CFGFOLDER/.last-ip" # Allows to do nothing when IP didn't change, remove file to force re-run
[ ! -f $CFGFILE ] && usage "Config file $CFGFILE does not exist"
source $CFGFILE
[ -z "$TOKEN" ] && usage "TOKEN not defined in $CFGFILE"
[ -z "$RECORDS" ] && usage "RECORDS not defined in $CFGFILE"
URL="https://api.linode.com/v4"
H1="Content-type: application/json"
H2="Authorization: Bearer $TOKEN"
CURRENT_IP=$(ip address show eth0 | grep -Eo 'inet [0-9\.]+' | cut -d " " -f2)
LAST_IP=$([ -f $IPFILE ] && head -1 $IPFILE)
[[ $LAST_IP == $CURRENT_IP ]] && exit 0
DBG=""
[ ! -d /config/scripts ] && DBG="echo" # Useful to debug, allows to perform actual REST call only on router
for record in $RECORDS; do
OUTPUT=$($DBG curl -s -H"$H1" -H"$H2" "$@" -XPUT $URL/domains/$record -d "{\"target\": \"$CURRENT_IP\"}")
if [ $? -ne 0 ]; then
do_log "Updating record $record failed: $OUTPUT"
exit 1
fi
done
do_log "Home IP updated to $CURRENT_IP"
[ ! -f $IPFILE ] && touch $IPFILE && chmod 0600 $IPFILE # chmod just because domainId is logged there...
echo $CURRENT_IP > $IPFILE
echo "# Updated on $(date) for $RECORDS" >> $IPFILE
@zsimic
Copy link
Author

zsimic commented Feb 9, 2021

Linode dynamic DNS update

If you use linode to manage your DNS, you can easily keep your home IP correct using the linode REST API.

You'll need to create a DNS record on linode first, then find the {domainId}/records/{recordId} corresponding to that record.
You can do so by running /v4/domains on the REST API (using your token), then /v4/domains/{domainId}/records

Main features of this script:

  • Designed to run unattended (as a cron job), on ubiquiti Edge Router
  • Hits the REST API only when your home IP actually changes
    (last IP remembered in /root/.ssh/.last-ip),
    most scripts out there keep hitting remote endpoint even if IP didn't change...
  • Can be ran ad-hoc to check that everything's OK
  • Logs to /var/log/messages when unattended (keeps quiet when IP didn't change)
  • Runs in 0.06s when IP didn't change :)
  • Can update multiple records at once, if for some reason you like to have home.foobar.com and home.foobaz.com...

How to use:

Put script in /config/scripts/ to avoid it getting clobbered by router OS updates.
Use /root/.ssh/ to hold linode token and info, since that is a pretty secure place (also not modified by OS updates).

You can easily change these locations by editing the script if need be.

1. Copy the script to the router

scp linode-ddns.sh ROUTER:~
ssh ROUTER
sudo cp linode-ddns.sh /config/scripts/linode-ddns.sh
sudo chmod 0755 /config/scripts/linode-ddns.sh

2. Configure TOKEN and RECORDS

sudo mkdir -p /root/.ssh
sudo chmod 0700 /root/.ssh
sudo vi /root/.ssh/linode-ddns-cfg.sh

# The file gets sourced by the script, define TOKEN and RECORDS like so:
TOKEN="(linode API token)"                  # your API token
RECORDS="123/records/234 124/records/345"   # space-separated list of "{domainId}/records/{recordId}"

The contents of that file should look roughly like so, if you have one record:

user@ROUTER:~$ sudo cat /root/.ssh/linode-ddns-cfg.sh
TOEKN="1234567890123456789012345678901234567890123456789012345678901234"
RECORDS="1234567/records/87654321"

3. Test that the script works

(Note: passing any argument to the script makes it log to stdout instead of /var/log/messages)

sudo /config/scripts/linode-ddns.sh run
sudo cat /root/.ssh/.last-ip

# 2nd run is a no-op (logs nothing):
time sudo /config/scripts/linode-ddns.sh run

# You can delete the file /root/.ssh/.last-ip to make it run again

If you see output that looks like this, then you're good to go to next section:

Feb  9 21:02:02 ROUTER linode-ddns: Home IP updated to 1.2.3.4

1.2.3.4
# Updated on Tue Feb  9 21:02:02 PST 2021 for 1234567/records/87654321

4. Schedule a job to run this script periodically

For example every 30 minutes:

configure
set system task-scheduler task linode-ddns interval 30m
set system task-scheduler task linode-ddns executable path /config/scripts/linode-ddns.sh
commit
save

And you're done!

5. Double-check that it works unattended

To double-check that the task is getting triggered, you can do this:

# Force script to re-run by deleting the file where it remembers which IP it last saw
sudo rm /root/.ssh/.last-ip

# Wait 30 minutes (or whatever time you scheduled)

# You should see evidence that the script ran:
sudo cat /root/.ssh/.last-ip

# The logs should have a line saying: "... linode-ddns: Home IP updated to ..."
tail -f /var/log/messages

@dgaidula
Copy link

This is awesome, thanks! I just added ipv6 support to this bc comcast SLAAC can supposedly change.


# See https://gist.github.com/zsimic/c39dd9686c6d6b0d149a67ff23286b99 for docs on how to use

# Note: you can invoke with LOGFILE= for troubleshooting
[ -z "$LOGFILE" ] && LOGFILE=/var/log/messages
if [ -z "$1" -a -f $LOGFILE ]; then  # Pass any command line arg to avoid the logging redirect
    exec $0 run 1>> $LOGFILE 2>&1
fi

function do_log {
    echo "$(date +'%h %e %T') $(hostname) linode-ddns: $@"
}

function usage {
    do_log "Usage error: $@"
    exit 1
}

# Note: you can invoke with CFGFILE= for troubleshooting
[ -z "$CFGFILE" ] && CFGFILE=~/.ssh/linode-ddns-cfg.sh
CFGFOLDER=$(dirname $CFGFILE)
IPFILE="$CFGFOLDER/.last-ip"  # Allows to do nothing when IP didn't change, remove file to force re-run
IPFILE6="$CFGFOLDER/.last-ip6"  # Allows to do nothing when IP didn't change, remove file to force re-run

[ ! -f $CFGFILE ] && usage "Config file $CFGFILE does not exist"
source $CFGFILE
[ -z "$TOKEN" ] && usage "TOKEN not defined in $CFGFILE"
[ -z "$RECORDS" ] && usage "RECORDS not defined in $CFGFILE"
[ -z "$RECORDS6" ] && usage "RECORDS6 not defined in $CFGFILE"

URL="https://api.linode.com/v4"
H1="Content-type: application/json"
H2="Authorization: Bearer $TOKEN"
CURRENT_IP=$(ip address show eth0 | grep -Eo 'inet [0-9\.]+' | cut -d " " -f2)
CURRENT_IP6=$(ip address show eth0 | grep -Eo 'inet6 [0-9a-f\:]+' | cut -d " " -f2 | head -1)

LAST_IP=$([ -f $IPFILE ] && head -1 $IPFILE)
LAST_IP6=$([ -f $IPFILE6 ] && head -1 $IPFILE6)

[[ $LAST_IP == $CURRENT_IP && $LAST_IP6 == $CURRENT_IP6 ]] && exit 0

DBG=""
[ ! -d /config/scripts ] && DBG="echo"  # Useful to debug, allows to perform actual REST call only on router
if [[ $LAST_IP != $CURRENT_IP ]]; then
	for record in $RECORDS; do
		OUTPUT=$($DBG curl -s -H"$H1" -H"$H2" "$@" -XPUT $URL/domains/$record -d "{\"target\": \"$CURRENT_IP\"}")
		if [ $? -ne 0 ]; then
			do_log "Updating record $record failed: $OUTPUT"
			exit 1
		fi
	done
fi

if [[ $LAST_IP6 != $CURRENT_IP6 ]]; then
	for record in $RECORDS6; do
		OUTPUT=$($DBG curl -s -H"$H1" -H"$H2" "$@" -XPUT $URL/domains/$record -d "{\"target\": \"$CURRENT_IP6\"}")
		if [ $? -ne 0 ]; then
			do_log "Updating record $record failed: $OUTPUT"
			exit 1
		fi
	done
fi


do_log "Home IP updated to $CURRENT_IP $CURRENT_IP6"
[ ! -f $IPFILE ] && touch $IPFILE && chmod 0600 $IPFILE  # chmod just because domainId is logged there...
echo $CURRENT_IP > $IPFILE
echo "# Updated on $(date) for $RECORDS" >> $IPFILE

#IP6 Version
[ ! -f $IPFILE6 ] && touch $IPFILE6 && chmod 0600 $IPFILE6  # chmod just because domainId is logged there...
echo $CURRENT_IP6 > $IPFILE6
echo "# Updated on $(date) for $RECORDS6" >> $IPFILE6

@zsimic
Copy link
Author

zsimic commented Nov 20, 2023

Awesome! Thank you :)
I'm not super familiar with ipv6 yet, I thought this wasn't needed with ipv6 :)
Ie, addresses are so plentiful that they can be trivially static...

Routers may be intentionally configured to change their ipv6 every now and then, but that's more to avoid tracking (ie: for privacy reasons), so this addition could play well into that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment