Dynamic DNS via shell script and IBM NS1 (nsone)

After trying dynamic DNS with Cloudflare but being unhappy with their provision that I could not use any other DNS servers than Cloudflare in order for my domains to be ‘active’ on there, I started looking for alternatives.

I chose to try IBM NS1 (also called nsone) to host domain records. It seems to have very good features, the most unique of which is a ‘linked record’. A linked record (for example a linked A record) behaves the same as a manually configured entry in terms of lookup. But, if the link target is updated then the linked record is updated too. While similar to CNAME it actually saves a nameserver lookup since the A record is returned immediately instead of the resolver having to look up the CNAME target. Kinda cool, I thought.

Make sure you have the requisites installed by doing the following:

apt -y install curl jq

I have a Unifi based home network and wanted to be able to update some domains dynamically when my fiber Internet connection changes. Fortunately the UDM Pro can call a URL to pass along the new IP address when it changes.

The updater shell script

First of all I needed a shell script to update the DNS entry on my domains with NS1. The API is quite simple to use. I knocked up the following BASH script to be able to update a record on the NS1 side easily. It will also. Save this as ‘updatensonerecord‘, for example.

#!/bin/bash

APIKEY=<your IBM NS1 API Key>

DOMAIN=$1
HOST=$2
RECORDTYPE=$3
DATA=$4
MODE=$5

if [ "$HOST" == "$DOMAIN" ]; then
  RECORDNAME=$DOMAIN
else
  RECORDNAME=${HOST}.${DOMAIN}
fi

if [ "$MODE" == "" ]; then
  MODE='auto'
fi

if [ "$DATA" != "" ]; then
  RCVDATA=`curl --silent --request GET --url https://api.nsone.net/v1/zones/${DOMAIN}/${RECORDNAME}/${RECORDTYPE} --header "X-NSONE-Key: $APIKEY" --header 'Accept: application/json'`
  RESPONSE=`echo $RCVDATA | jq -r '.message'`
  if [ "$RESPONSE" == "record not found" ]; then
    OP='PUT'
  else
    if [ "$MODE" != "overwrite" ]; then
      for PREVANS in `echo $RCVDATA | jq -r '.answers[].answer[]'`
      do
        PREVIOUSDATA="${PREVIOUSDATA}, \"${PREVANS}\""
      done
    fi
    OP='POST'
  fi

  NEWDATA="{\"answers\": [ { \"answer\": [ \"${DATA}\" ${PREVIOUSDATA} ] } ], \"domain\": \"${RECORDNAME}\", \"zone\":\"${DOMAIN}\", \"type\":\"${RECORDTYPE}\"}"
  
  RESPONSE=`curl --silent --request $OP --data "$NEWDATA" --url https://api.nsone.net/v1/zones/${DOMAIN}/${RECORDNAME}/${RECORDTYPE} --header "X-NSONE-Key: $APIKEY" --header 'Accept: application/json' | jq -r '.id'`
  if [ "$RESPONSE" != "" ]; then
    echo "Set $RECORDTYPE for $RECORDNAME to $DATA"
  fi
fi

Now we can use that in other things, one of which is to update the IP address for a list of our domains from the command line. Call this script ‘updatedynamicip', for example.

#!/bin/bash

NEWIP=`echo $1 | sed -e "s/ip=//g"`

DOMAINS='mydomain1.com mydomain2.com'

if [ "$NEWIP" != "" ]; then
  for DOMAIN in $DOMAINS
  do
    ./updatensonerecord $DOMAIN $DOMAIN A $NEWIP
  done
else
  echo "No IP passed"
fi

Usage is then like this:

./updatedynamicip <new IP>

If you have more complicated DNS zones that you are wanting to update you may consider using CNAMEs and/or linked records to be able to keep the updating side simple.

Updating via the UDM Pro

That’s all great to be able to update from a command line but we want this to be automatic and seamless.

I decided to just use a very simple CGI-bin handler via nginx on a VM within my home network. However, given how the UDM Pro works it needs to support SSL. You will need to install the fcgiwrap package first of all.

CGI-bin config:

server {
  listen 443 ssl http2;

  server_name <your hostname>;

  ssl_certificate <your ssl cert>;
  ssl_certificate_key <your ssl key>;
  include ssl_params;

  root /var/www/html;

  index index.html index.htm index.nginx-debian.html;

  location ~ ^/cgi {
    rewrite ^/cgi/(.*) /$1 break;

    include fastcgi_params;
    fastcgi_pass unix:/var/run/fcgiwrap.socket;
    fastcgi_param SCRIPT_FILENAME /root/bin/simplecgi;
  }
}

Once you enable that script you should be able to run a simple script, in this case I have one at /root/bin/simplecgi on this particular system. That script would then be the one to run your DNS updater script and respond appropriately. The script I have for that is:

#!/bin/bash

PROGRAM=/root/dns/updatensone

if [ -x $PROGRAM ]; then
  echo "HTTP/1.0 200 OK"

  OUTPUT=`$PROGRAM $QUERY_STRING`
  LENGTH=$((${#OUTPUT} + 4))

  echo $OUTPUT >>/tmp/simplecgi.log

  echo "Content-Length: $LENGTH"
  echo ""
  echo "OK"
  echo ""
  echo $OUTPUT
else
  echo "HTTP/1.0 404 NotFound"
  echo "Content-Length: 9"
  echo ""
  echo "Not found"
fi

All that script does is to pass the querystring to a predetermined script (in this case our DNS updater) and then output an HTTP/1.0 compatible response, along with the output from the script it ran.

Then from the UDM Pro, just set up a ‘custom’ DNS handler that has the URL of the form:

<your host machine IP>/cgi-bin/?ip=%i

Then when your network updates its IP address, your domains will follow suit! Note: do not prepend the protocol, i.e, https:// to that URL.

If you want to test it immediately, SSH to your UDM Pro.

ps aux | grep inadyn

Then run the following using the configuration file you saw in the output from above. e.g.

inadyn -n -1 --force -f /run/ddns-eth8-inadyn.conf