DIY dynamic DNS with your providers DNS API
Got a notice from my ISP that in a few weeks time I will no longer have a static IP address. Not the ISP fault however, they would be happy to provide the service. It is the company that manages the city fiber network that blames a system upgrade for the coming inability to provide static IP addresses.
The municipality should never have sold off the optic fiber network to a private company. Important things like roads, hospitals, schools, the police and city fiber networks should be public and nothing else.
Unwilling to give up my home server I started looking for a solution.
Dynamic DNS
Why not use one of many dynamic DNS providers?
I do not mind paying for stuff but I want to use as few services as possible.
My first instinct is to self host. When that is not a good option I use a few trusted services, e.g. GleSYS.
Doing it yourself is also a lot more fun.
Find a good server hosting company with an API
Luckily my favourite hosting company here in Sweden, GleSYS, has an excellent API.
They allow a minimum TTL of 60s, perfekt for a dynamic DNS setup. After some questions on their community Slack I was ready to start experimenting.
Other hosting companies that has API for this includes Linode and DigitalOcean as well as all the big cloud providers I assume.
For a server with private IP address behind a router
Use a cron script that runs on reboot and every 5 minuts
This is a bit crud but gets the job done.
File: /etc/cron.d/dynamicdns
@reboot root if [ -x /usr/local/bin/dynamicdns.sh ]; then /usr/local/bin/dynamicdns.sh; fi
*/5 * * * * root if [ -x /usr/local/bin/dynamicdns.sh ]; then /usr/local/bin/dynamicdns.sh; fi
File: /usr/local/bin/dynamicdns.sh
#!/usr/bin/env bash
# shell script hardening
set -euo pipefail
IP=$(curl --silent https://ipinfo.io/ip)
/usr/local/bin/dynamicdns.py "${IP}"
I leave it up to the reader to combine dynamicdns.sh
and dynamicdns.py
in to a single script if running via cron.
For a server with a public IP address
This is my own use case.
Use DHCP client script hooks to detect when there is a new IP address
First job was to find out how to detect when the IP address changes and run a script to update the DNS. Turns out there is DHCP client script enter and exit hooks that can take care of this.
On Debian you place your exit script in /etc/dhcp/dhclient-exit-hooks.d/
.
File: /etc/dhcp/dhclient-exit-hooks.d/dynamicdns
Replace “enp3s0” with your WAN interface.
if [ "${interface}" = "enp3s0" ]; then
case ${reason}" in BOUND|REBIND|RENEW)
/usr/local/bin/dynamicdns.py "${new_ip_address}"
;;
esac
fi
This will run the dynamicdns.py
script when changes are detected on the main WAN interface and there is a new IP address. See documentation for what reasons BOUND|REBIND|RENEW stands for. The interface, reason and new_ip_address variables are provided by the DHCP client.
Now on to the script that actually update the DNS entries.
I opted to write it in Python. It needs to take the new_ip_address
as an argument, remember the last IP address, do some sanity checking and talk to the API. Logging is always a good idea as well.
File: /usr/local/bin/dynamicdns.py
OBS! You will need to set the variables and adapt updatedns()
for your providers API. The way you specify dns_records
might need adapting as well.
#!/usr/bin/env python3
"""
Script to update DNS entries for a list of domains via GlySYS API.
"""
import argparse
import ipaddress
import logging
import os
import requests
from logging.handlers import RotatingFileHandler
# Set variables.
apikey = 'CHANGE_THIS_to_your_api_key'
apiurl = 'https://api.glesys.com/domain/updaterecord/'
apiuser = 'CHANGE_THIS_to_your_api_user'
current_ip_file = '/var/spool/dynamicdns/current_ip.txt'
dns_records = {
'12345': 'example.org',
'23456': 'vpn.example.org',
'34567': 'server.example.org',
}
def logger():
logfile = '/var/log/dynamicdns.log'
log_format = ('%(asctime)s %(levelname)-8s %(message)s')
logging.basicConfig(
level=logging.INFO,
format=log_format,
handlers=[
RotatingFileHandler(logfile, maxBytes=50000)
],
)
return logging.getLogger(__name__)
logger = logger()
def updatedns(recordid, new_ip_address):
r = requests.post(apiurl, auth=(apiuser, apikey), headers={'accept': 'application/json'}, data={'recordid': recordid, 'data': new_ip_address})
output = r.json()
response_code = output['response']['status']['code']
response_text = output['response']['status']['text']
response_ip = output['response']['record']['data']
if response_code == 200:
logger.info(f'{dns_records[recordid]} has been updated to IP {response_ip}')
else:
logger.error(f'{dns_records[recordid]} could not be updated Error code: {response_code} {response_text}')
def get_current_ip():
current_ip = None
try:
with open(current_ip_file, 'r') as f:
current_ip = f.read()
except FileNotFoundError:
pass
return current_ip
def set_current_ip(new_ip_address):
os.makedirs(os.path.dirname(current_ip_file), exist_ok=True)
with open(current_ip_file, 'w') as f:
f.write(new_ip_address)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('new_ip_address', type=str, help='The new IP address.')
args = parser.parse_args()
new_ip_address = args.new_ip_address
try:
ipaddress.ip_address(new_ip_address)
except ValueError:
logger.warning(f'Not a valid new IP: {new_ip_address}')
else:
if new_ip_address != get_current_ip():
set_current_ip(new_ip_address)
[updatedns(recordid, new_ip_address) for recordid in dns_records]
if __name__ == '__main__':
main()
Conclusion
With this in place the DNS entries for my home server should update within 60s of an IP address change. This is ok for the services I run. Not as good as a real static IP address but it will do for now.
The most useful function for my home server is as a VPN with Wireguard. Later this year, when we hopefully can start traveling again, it will see some traffic after many month of rest.