Long story short, I need to be able to access my home machine(s) from the Internet. Unfortunately my ISP provides me with a dynamic IP address so I need to jump to another hoop to get where I want. Luckily there’s a lot of Dynamic DNS providers out there, for reason(s) I opted to use Duck DNS.

So go over to Duck DNS and sign-in to create an account, claim your subdomain and grab the token.

I wrote a simple shell script that checks the external interface for any IP changes and when needed updates the Duck DNS service. The only external dependency is on curl which you can install quickly using pkg_add.

I tried to create a somewhat secure execution environment for this script:

cerberus# groupadd -g 5000 _duckdns
cerberus# useradd -g 5000 -u 5000 -s /sbin/nologin -d /nonexistent -c "DuckDNS Updater" _duckdns

The script expects a writable directory to maintain a state file, so that we only go out to the DuckDNS service if needed.

cerberus# mkdir /var/db/duckdns
cerberus# chown -R _duckdns:wheel /var/db/duckdns
cerberus# chmod 750 /var/db/duckdns

Copy the script and put store it in a sane location: /usr/local/sbin/duckdns-update Now you just need to change three variables in the scripts configuration section:

  • INTERFACE
  • DUCKDNS_TOKEN
  • DUCKDNS_DOMAIN

Setup the permissions so that only root can make changes to the code and the sandbox user _duckdns can execute it.

cerberus# chmod 750 /usr/local/sbin/duckdns-update
cerberus# chown root:_duckdns /usr/local/sbin/duckdns-update

The next step is to have the script run periodically from cron by issuing the crontab -e command. I run it every 5 minutes, make sure to change it to fit your needs.

cerberus# su -s /bin/sh _duckdns
cerberus$ crontab -e
# DuckDNS
1-59/5  *       *       *       *       /usr/local/sbin/duckdns-update > /dev/null 2>&1

Finally we want to make sure that the _duckdns user isn’t receiving mail by adding the following to /etc/mail/aliases:

cerberus# grep _duckdns /etc/mail/aliases
_duckdns:	/dev/null

After any modifications to the /etc/mail/aliases file you need to run the newaliases command.

The script:

#!/bin/sh
# Simple DuckDNS update script

# enable for debugging
#set -x

# default umask
umask 026

# configuration
INTERFACE="<<YOUR EXT INTERFACE>>"
DUCKDNS_TOKEN="<<YOUR TOKEN>>"
DUCKDNS_DOMAIN="<<YOUR DUCKDNS SUBDOMAIN>>"

# code - no need for changes here
CURL="/usr/local/bin/curl"
STATE_FILE="/var/db/duckdns/${INTERFACE}.ip"
NEW_IP=$(/sbin/ifconfig ${INTERFACE} | grep "inet " | awk '{print $2}')
PRV_IP=$(/bin/cat ${STATE_FILE})
DUCKDNS_UPDATE_URL="https:/www.duckdns.org/update?domains=${DUCKDNS_DOMAIN}&token=${DUCKDNS_TOKEN}"

LOGGER() {
     /usr/bin/logger -i -p local0.info -t $0 "$1"
}

if [ ! -x ${CURL} ]; then
    LOGGER "Fatal error; curl is not installed"
    exit 1
fi

if [ ! -w $(dirname ${STATE_FILE}) ]; then
    LOGGER "Fatal error; state directory not writable"
    exit 1
fi

if [ "${NEW_IP}" != "${PRV_IP}" ]; then
    LOGGER "Detected a new IP for ${INTERFACE}: ${NEW_IP}"

    STATUS=$(${CURL} --insecure --silent ${DUCKDNS_UPDATE_URL})
    if [ ${STATUS} == "OK" ]; then
        LOGGER "DuckDNS update successful"
        LOGGER "Updating state file: ${STATE_FILE}"
        echo ${NEW_IP} > ${STATE_FILE}
    else
        LOGGER "DuckDNS update failed"
        exit 1
    fi
fi

# EOF

I guess that’s all for today :).