A few months ago, I wrote about setting up a home server prone to external IP changes. Since my domain oriane.ink
was hosted by Gandi, the idea was to leverage Gandi's LiveDNS API to update the DNS records whenever the IP address would change. If you're interested in reading more about the why & how of dynamic DNS, please follow the link to the previous article.
But Gandi got bought by a Dutch conglomerate, which promptly decided to skyrocket their billing. Standard mailboxes went from dumb-free to 60€/year, basically overnight, which is a really shitty thing to do to users. So I decided to migrate my email, along with my domain, to OVH. I'm not aware of OVH stabbing people in the back, and that's all I could care for.
With Gandi gone, I realized, whoops, that I would need a replacement for my dynamic DNS setup. Fortunately, OVH also provides an API for their DNS services. Unfortunately, their documentation isn't nearly as good as Gandi's. I traded Bash scripting for Python, and after some tedious trial and error, I got to a working solution, which I am pleased to share below.
First, assuming you're on a Linux-based system, make sure you have a working Python environment, then install the requirements with the pip
package manager. Afterwards, set up a directory with root privileges (although the script itself won't need such privileges to run).
pip install ovh requests
sudo mkdir /opt/dyndns_ovh
sudo chown <username>:<usergroup> /opt/dyndns_ovh
Then, copy the following script into the new directory, as /opt/dyndns_ovh/dyndns_ovh.py
.
#!/usr/bin/python3
import sys
import ovh, requests
APPLICATION_KEY = "XXXXXXXXXXXX"
APPLICATION_SECRET = "XXXXXXXXXXXXXXXXXXXX"
CONSUMER_KEY = "XXXXXXXXXXXXXXXXXXXX"
DOMAIN = "my-ovh-domain.com"
OVH_ENDPOINT_RECORD = f"/domain/zone/{DOMAIN}/record"
OVH_ENDPOINT_REFRESH = f"/domain/zone/{DOMAIN}/refresh"
EXT_IP_LK_PATH = "/opt/dyndns_ovh/last_known_ip"
def main():
# get current external IP address
ip_current = requests.get("https://ifconfig.me").text
with open(EXT_IP_LK_PATH, "w+") as f:
ip_lastknown = f.read()
# if the external IP has not changed, exit early
if ip_current == ip_lastknown:
sys.exit()
f.seek(0)
f.write(ip_current)
f.truncate()
client = ovh.Client(
endpoint="ovh-eu",
application_key=APPLICATION_KEY,
application_secret=APPLICATION_SECRET,
consumer_key=CONSUMER_KEY,
)
# get all existing A records
records = client.get(OVH_ENDPOINT_RECORD, fieldType="A")
# delete them
for record in records:
client.delete(OVH_ENDPOINT_RECORD + "/" + str(record))
# add a new record with the current IP
client.post(OVH_ENDPOINT_RECORD, fieldType="A", target=ip_current)
# refresh the zone
client.post(OVH_ENDPOINT_REFRESH)
if __name__ == "__main__":
main()
We begin by retrieving our public IP address from https://ifconfig.me, a free service operated by IPinfo. Then we compare it to the last IP address we stored: either they match and there's nothing to do, or the address has changed and we need to replace the stale DNS record.
The next part requires OVH API credentials to run. Once connected to your OVH account, you can create new tokens here. This web interface is... not good. I expect it to be nuked in a few years. Anyway, to get the script to do its job, you need to grant rights at least for GET
, POST
and DELETE
operations, on /domain/zone/*
resources. This should get you the three (3) required tokens.
The main course of the script, the API calls, are pretty straightforward. Get all A-type records (those used for IPv4 mapping; with a private setup, there's usually a single one), remove them, add a new record, refresh the zone. (I guess the stale record could be updated in-place with a PUT
request, but that would require storing the identifier of the record resource, and I chose not to bother with that.) You may check the API documentation if you need some tweaking.
And finally, add this line to crontab -e
to get the script running every half hour:
*/30 * * * * /usr/bin/python3 /opt/dyndns_ovh/dyndns_ovh.py
Hopefully this will live on as a functional, low-maintenance solution for at-home dynamic DNS!
Archived captures for external links featured in this post :