Script example - instead of DynDNS

Hi.

I am new to desec, but already loves your DNS service!

I have a public dynamic IP, but it does not change that often, and I am not that fond of any of the DynDNS services out there, so discovering you have an API, just made my day.

I have made this script to keep my DNS settings updated, which only interacts with desec if the IP truly has changed. Use it freely.

Here is the somewhat readerfriendly shell script:

#!/bin/bash
currentIp=$(curl --silent https://ipinfo.io/ip)
FILE=/home/user/ip.txt
touch $FILE
LOG=/home/user/logs/publicip.log
touch $LOG
echo "Date: $(date +'%Y-%m-%d')" &>> $LOG
echo "Start time: $(date +'%T')" &>> $LOG
echo "" &>> $LOG
if [ ! -s "$FILE" ]
then
  ipFile="0"
else
  ipFile=$(cat "$FILE")
fi
echo "$currentIp : Current IP" >> $LOG
echo "$ipFile : Stored IP" >> $LOG

if [ $currentIp != $ipFile ]
then
  echo "IP Changed in store. Checking DNS......" >> $LOG
  echo "$currentIp" &> $FILE
  domainIP=$(curl --silent https://desec.io/api/v1/domains/yourdomain.dom/rrsets/www/A/ -H "Authorization: Token {Your own token}" | jq -r '.records[0]')
  echo "Public DNS resolved IP: $domainIP" >> $LOG
  if [ $currentIp != $domainIP ]
    then echo "Not matching. Trying to update....." >> $LOG
    apiResult=$(curl --silent -X PUT https://desec.io/api/v1/domains/yourdomain.dom/rrsets/ -H "Authorization: Token {Your own token}" -H "Content-Type: application/json" -d '[{"subname": "www", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "cloud", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "meet", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "cloud-dev", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "join", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "", "type": "TXT", "ttl": 3600, "records": ["\"protonmail-verification={Your protonmail verification hash}\"","\"v=spf1 ip4:'$currentIp' include:_spf.protonmail.ch mx -all\""]},{"subname": "cloud", "type": "TXT", "ttl": 3600, "records": ["\"v=spf1 ip4:'$currentIp' mx -all\""]}]')
    echo "Waiting for zone updating...." >> $LOG
    for i in {0..8}; do echo -ne "Waiting for zone update. "$i"0s of total 80s"'\r'; sleep 10s; done; echo; echo "Done." >> $LOG; echo "Re-validating NSLookup" >> $LOG
    if [ $currentIp != $(curl --silent https://desec.io/api/v1/domains/yourdomain.dom/rrsets/www/A/ -H "Authorization: Token {Your own token}" | jq -r '.records[0]') ]
    then
      echo "Something went wrong. Please check your DNS config manually!!" >> $LOG
    else
      echo "Matching. Zone has been successfully updated." >> $LOG
    fi
  else
    echo
    "Matching. No need to update zone." >> $LOG
  fi
else
  echo "IP not changed" >> $LOG
fi
echo "" &>> $LOG
echo "Finished: $(date +'%T')" >> $LOG
echo "" &>> $LOG
echo "########################################" &>> $LOG

And here I have it as implemented.

#!/bin/bash
currentIp=$(curl --silent https://ipinfo.io/ip); FILE=/home/user/ip.txt; touch $FILE; LOG=/home/user/logs/publicip.log; touch $LOG; echo "Date: $(date +'%Y-%m-%d')" &>> $LOG; echo "Start time: $(date +'%T')" &>> $LOG; echo "" &>> $LOG
if [ ! -s "$FILE" ]; then ipFile="0"; else ipFile=$(cat "$FILE"); fi
echo "$currentIp : Current IP" >> $LOG; echo "$ipFile : Stored IP" >> $LOG
if [ $currentIp != $ipFile ]
then
 echo "IP Changed in store. Checking DNS......" >> $LOG; echo "$currentIp" &> $FILE; domainIP=$(curl --silent https://desec.io/api/v1/domains/yourdomain.dom/rrsets/www/A/ -H "Authorization: Token {Your own token}" | jq -r '.records[0]'); echo "Public DNS resolved IP: $domainIP" >> $LOG
 if [ $currentIp != $domainIP ]; then echo "Not matching. Trying to update....." >> $LOG; apiResult=$(curl --silent -X PUT https://desec.io/api/v1/domains/yourdomain.dom/rrsets/ -H "Authorization: Token {Your own token}" -H "Content-Type: application/json" -d '[{"subname": "www", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "cloud", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "meet", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "cloud-dev", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "join", "type": "A", "ttl": 3600, "records": ["'$currentIp'"]},{"subname": "", "type": "TXT", "ttl": 3600, "records": ["\"protonmail-verification={Your protonmail verification hash}\"","\"v=spf1 ip4:'$currentIp' include:_spf.protonmail.ch mx -all\""]},{"subname": "cloud", "type": "TXT", "ttl": 3600, "records": ["\"v=spf1 ip4:'$currentIp' mx -all\""]}]'); echo "Waiting for zone updating...." >> $LOG
   for i in {0..8}; do echo -ne "Waiting for zone update. "$i"0s of total 80s"'\r'; sleep 10s; done; echo; echo "Done." >> $LOG; echo "Re-validating NSLookup" >> $LOG
   if [ $currentIp != $(curl --silent https://desec.io/api/v1/domains/yourdomain.dom/rrsets/www/A/ -H "Authorization: Token {Your own token}" | jq -r '.records[0]') ]; then echo "Something went wrong. Please check your DNS config manually!!" >> $LOG; else echo "Matching. Zone has been successfully updated." >> $LOG; fi; else echo "Matching. No need to update zone." >> $LOG; fi
else echo "IP not changed" >> $LOG; fi
echo "" &>> $LOG; echo "Finished: $(date +'%T')" >> $LOG; echo "" &>> $LOG; echo "########################################" &>> $LOG

You could probably get it much cleaner by using dig short - but not if you like me, has LAN side DNS because then it just returns the LAN IP of the dig command - instead of calling an external service to reveal your public IP.

Hi @kerasit,

Seems a bit complicated, so I have not analysed the script in detail. Might work though.

A few points:

  • https://checkip.dedyn.io/, https://checkipv4.dedyn.io/ and https://checkipv6.dedyn.io/ can be used to get your own public IP (though with IPv6 you will likely get the newest current IP of the machine that is asking, which might change every few minutes, depending on your ISP/setup).
  • There is a simple HTTP API for updating DDNS at deSEC: See: IP Update API — deSEC DNS API documentation. So no need to use the more complicated general API you are using. Probably more stable as well. Most consumer routers allow using this API directly, which would have the advantage that the router already knows when the IPs changes and does not have to poll. Also just change one A record and base the other DNS records off that hostname.
  • Instead of the complicated curl command to determine whether the IP was changed, you could indeed use something like dig +short @ns1.desec.io <your-ddns-hostname> A. I would expect the load on the deSEC infrastructure be less than using API calls as you are currently doing.
  • Why aren’t you checking the API result? That would be preferrable to trying to do the same thing over and over again. (You might also run into API rate limitations if you make too many API calls in a short time.)
  • Your loop is doing up to 9 iterations (≈90s) in the worst case. That seems excessive. If the update has not happened within 10-15s then it probably never will. Normally it happens faster in my experience. You could also wait longer for the later iterations. Use the loop index to calculate the sleep time. You need to coordinate this with your calling mechanism (cron?) though, to avoid overlapping runs of the script. They would serve no purpose.

HTH
fiwswe

Thanks for that really cool feedback post! One nit:

That’s not entirely true. We send NOTIFY messages to our secondary servers, but they sometimes get lost (especially in locations very remote from our primary, in terms of Internet topology). We catch these missing updates by sync’ing the full list of zone serials every minute on each secondary.

From the diff, the secondary can see which updates were lost (and retransmit it from the primary), and also infer which domains have been added or removed.

So, there’s a small percentage of updates, in certain locations, that sometimes arrive only after around a minute or so. Updates that take longer than 90s, however, should be exceedingly rare (e.g. when there is a connectivity problem at an intercontinental carrier, or something).

Stay secure,
Peter

1 Like

I have the needed for more fine grained control of some of the records. And my IP changes rapid these day due to construction workers triggering fuses, but otherwise it rarely changes. And it is a home service, so it is not that critical to verify every minute. I verifies once a day.

I am not looping my request to desec api. I am waiting 90 seconds until I do ONE api request to validate zone settings.

I use it once a day, and because of construction work and power outage, my IP has changed a couple of times each day. I can verify that it works excactly like I want it to.

@peter Thanks for clarifying how the updates work.

True. Like I wrote I only did a cursory check of your script and missed that detail.

Ok, fine. I just wanted to point out that most users could simplify this significantly.

That is always an issue with DDNS. On the one hand you generally want to update ASAP after a change of public IPs, OTOH you don’t want to constantly poll for changes. As I wrote, allowing the router to handle DDNS avoids the polling and reduces the window of time where DNS is out of sync with reality. But with DDNS there are always windows of time where DNS is out of sync.

Can multiple domains be updated with just 1 command & token?

Premise: under the same deSec account (same token) I have multiple separate domains registered, all pointing to the same server. When my ISP changes my IP I want to update them all with 1 command rather than send & wait for multiple such commands to complete, each authorized with the same token.

Something like:

curl --ipv4 https://update.dedyn.io/?hostname=dom1.de,dom2.com,dom3.org \
  --header "Authorization: Token xYz4"

Could this or something simliar work?
@peter

This is entirely off-topic in this thread. Besides, your question is answered here: Update all hosts with one link (FRITZ!Box) - #2 by peter

Stay secure,
Peter

1 Like