-
-
Save zsimic/c39dd9686c6d6b0d149a67ff23286b99 to your computer and use it in GitHub Desktop.
| #!/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 |
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
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.
I recently upgraded to a UCG-MAX to get 2.5GB internet and had to make a few tweaks to get the script to work on it. As Linode is not supported on these 'Dream' machines as a DDNS provider. The big tweaks are WAN_PORT & SCRIPT_DIR variables since the edge router paths are not there. Also note that the path to the cfg file had to be absolute. /root/.ssh/linode-ddns-cfg.sh.
A critical tweak was adding a space after the -H for the curl headers: curl -s -H "$H1" -H "$H2" "$@" ... not sure why this was needed but this was part of way got it all working.
Verified on UGC-MAX running UNIFI OS 4.4.9
#!/bin/bash
# See https://gist.github.com/zsimic/c39dd9686c6d6b0d149a67ff23286b99 for docs on how to use
#2026-01-02 update variable for WAN port bc some dream devices have flexible wans with default of last port instead of port zero
# other updates for dream devices as tested on ucg-max
WAN_PORT='eth4';
SCRIPT_DIR='/opt/scripts/linode-ddns';
# edge routers usually used port zero for WAN
# uncomment these as needed
#WAN_PORT='eth0';
#SCRIPT_DIR='/config/scripts';
# 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=/root/.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 "$WAN_PORT" | grep -Eo 'inet [0-9\.]+' | cut -d " " -f2)
CURRENT_IP6=$(ip address show "$WAN_PORT" | 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 "$SCRIPT_DIR" ] && 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
I've moved away from ubiquiti edge routers 😊 I'm using their "dream machine" now and don't want to publicize my IP anymore (reverse tunnel is way better😬)
interesting. Do you have any tutorials you can point me towards?
Yes, like https://www.youtube.com/watch?v=nmE28_BA83w or https://www.youtube.com/watch?v=B4pnGyvj2sM or https://www.youtube.com/watch?v=Cs8yOmTJNYQ - all very good channels, Techno Tim (that last one) probably explains things more in depth
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/domainson the REST API (using your token), then/v4/domains/{domainId}/recordsMain features of this script:
(last IP remembered in
/root/.ssh/.last-ip),most scripts out there keep hitting remote endpoint even if IP didn't change...
/var/log/messageswhen unattended (keeps quiet when IP didn't change)home.foobar.comandhome.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.sh2. Configure
TOKENandRECORDSThe contents of that file should look roughly like so, if you have one record:
3. Test that the script works
(Note: passing any argument to the script makes it log to stdout instead of
/var/log/messages)If you see output that looks like this, then you're good to go to next section:
4. Schedule a job to run this script periodically
For example every 30 minutes:
And you're done!
5. Double-check that it works unattended
To double-check that the task is getting triggered, you can do this: