Dynamc DNS client for IPv6

Dynamic DNS is used to automatically update the DNS record of a host which IP changes often. This allows you to provide web services from a host at home behind your personal (V)DSL connection with a non-changing name. There are several providers for DDNS. I use SPDyn. The update of the DNS record is handled by a client like ddclient or directly by your router (e.g. Fritzbox). For IPv4 this is easy. The IP is always the external IP of the router which forwards ports to hosts on your local network. Since my ISP uses Dual-Stack and also provides me IPv6 I created an AAAA-record in the past pointing to the IPv6 address of my local host. When doing so I didn’t realise my ISP changes the IPv6 prefix with every reconnect. Because of this the IPv6 address of my local host changes quite regularly and the AAAA-record created got invalid. I noticed this due to an error when renewing a Let’s Encrypt certificate (post about acme-tiny).

If an AAAA-record is available Let’s Encrypt prefers it for validation of the domain (API Announcements). Because my AAAA-record was not valid anymore the renewal of the certificate threw an error:

Parsing account key...
Parsing CSR...
Registering account...
Already registered!
Verifying domain.com...
Traceback (most recent call last):
  File "acme_tiny.py", line 199, in 
    main(sys.argv[1:])
  File "acme_tiny.py", line 195, in main
    signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca)
  File "acme_tiny.py", line 150, in get_crt
    domain, challenge_status))
ValueError: domain.com challenge did not pass: {u'status': u'invalid', u'validationRecord': [{u'addressesResolved': [u'217.251.47.207', u'2003:86:2455:9c00:d1:3ff:fe81:bd3f'], u'url': u'http://domain.com/.well-known/acme-challenge/PDUNtwiHq5dncDrvs4V2NE9nSR9vLF2WhnAbX1jQ7f0', u'hostname': u'domain.com', u'addressesTried': [], u'addressUsed': u'2003:86:2455:9c00:d1:3ff:fe81:b46f', u'port': u'80'}], u'keyAuthorization': u'PDUNtwiHq5dncDrvs4V2NE9nSR9vLF2WhnAbX1jQ7f0.YW7Ac9LxjjuWvWzD542ZzSKxcFKDIdehVVzAuYA0vHI', u'uri': u'https://acme-v01.api.letsencrypt.org/acme/challenge/-yjij3RP1r4YC_TkQrUemgjhfWI17pQZSjMZ8kr-Lps/1441804350', u'token': u'PDUNtwiHq5dncDrvs4V2NE9nSR9vLF2WhnAbX1jQ7f0', u'error': {u'status': 400, u'type': u'urn:acme:error:connection', u'detail': u'Fetching http://domain.com/.well-known/acme-challenge/PDUNtwiHq5dncDrvs4V2NE9nSR9vLF2WhnAbX1jQ7f0: Timeout'}, u'type': u'http-01'}

You can see the IPv6 address is used when looking at addressUsed. Because the address is invalid a timeout is reached.

To correct this and be able to access my local host via IPv6 in addition to the A-record for IPv4 the AAAA-record for IPv6 also has to be updated.

The Wiki of SPDyn describes how to configure the update for IPv4 and IPv6 with a Fritzbox. But with this the AAAA-record is updated to the IPv6 address of the router and not the host on my local network. So I only use the Fritzbox to update the A-record for IPv4. Since SPDyn also offers to update the DNS records with an URL I wrote a little Script for updating the AAAA-record via cronjob.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

USER="user"
PASSWORD="password"
HOSTNAME="subdomain.spdns.org"
CACHE_FILE="/tmp/update_ipv6_cache"

while getopts ":d" opt; do
    case $opt in
        d)    DEBUG=1;;
    esac
done


IPV6=`ip addr show eth0 | grep 'scope global dynamic' | grep -Po 'inet6 \K[0-9a-fA-F:]+' | grep -v fd00`
URL="https://update.spdyn.de/nic/update?hostname=$HOSTNAME&myip=$IPV6"
if [ -f $CACHE_FILE ]; then
    IPV6_CACHE=`cat $CACHE_FILE`
else
    IPV6_CACHE=""
fi

if [[ $IPV6 == $IPV6_CACHE ]]; then
    if [[ $DEBUG -eq 1 ]]; then
       echo "IP not changed"
    fi
    exit
else
    echo $IPV6 > $CACHE_FILE
    if [[ $DEBUG -eq 1 ]]; then
        echo wget -q -O - "$URL" --http-user=$USER --http-password=$PASSWORD
        wget -q -O - "$URL" --http-user=$USER --http-password=$PASSWORD
    else
        wget -q -O - "$URL" --http-user=$USER --http-password=$PASSWORD >/dev/null 2>&1
    fi
fi

Variables at the beginning specify the data necessary to generate the URL (e.g. user and password). The first step is to get the correct IPv6 address (line 15). In my case all IPv6 addresses of the device eth0 are checked for global dynamic and non local addresses (fd00). Perhaps this part has to be adjusted for other scenarios. After this the IPv6 address gets compared to a cached IP possibly available (line 23). If the two values are the same no update of the AAAA-record is needed and the script ends. If the two addresses differ or no cached value is available wget is used to make the HTTP request which updates the AAAA-record (line 34). If you start the script with the parameter -d (debug) the wget command (line 31) or the info that no change is necessary (line 25) are echoed.

This script is started every 15 minutes via cronjob and updates the AAAA-record at SPDyn when needed. To communicate with the host on the local network some configuration on my Fritzbox is still necessary. After all these changes the host is accessible via IPv6 and the renewal of the Let’s Encrypt certificate is working again.