Skip to content

Instantly share code, notes, and snippets.

@cvn
Last active November 11, 2022 16:25
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cvn/7ac82d2ba0f5fa569c4a360387d89438 to your computer and use it in GitHub Desktop.
Save cvn/7ac82d2ba0f5fa569c4a360387d89438 to your computer and use it in GitHub Desktop.
A script for updating dynamic DNS using Opalstack's API
#!/usr/bin/env python3
# update_ip.py - A script for updating dynamic DNS using Opalstack's API
# by Chad von Nau
#
# * Python 2 and 3 compatible.
# * Supports IPv4 and IPv6 addresses.
# * Records the last IP in a JSON file and only calls the API if the IP has changed.
#
# Instructions:
#
# 1. Create a new DNS Record on Opalstack, with the domain you want to use.
# 2. Create an Opalstack API token, if you don't already have one.
# 3. Create a PHP app on Opalstack, with this as the index.php:
# <?php echo $_SERVER['REMOTE_ADDR']; ?>
# 4. Set the variables below.
# 5. Run this script, pass the domain as an argument, ex:
# python update_ip.py my-domain.com
# 6. Optionally, automate this to run periodically. I run it every hour using cron.
# To minimize logging, I use `sleep` to wait for WiFi after machine wake, and I redirect stdout to /dev/null.
# sleep 20 && /usr/local/bin/python3 /Users/example/update_ip.py example.com > /dev/null
# Variables
token = 'YOUR_OPALSTACK_API_TOKEN'
ip_service = 'http://YOUR_PHP_APP_URL' # expect service to return IP address as string
# Imports
import os
import sys
import json
import argparse
try:
# python 3
from urllib.request import Request, urlopen
from urllib.error import URLError
except ImportError:
# python 2
from urllib2 import Request, urlopen, URLError
# Command line arguments
parser = argparse.ArgumentParser(description="Update dynamic DNS using Opalstack's API.")
parser.add_argument('domain', help="domain name")
parser.add_argument('-f', '--force', action='store_true', help="force update")
args = parser.parse_args()
domain = args.domain
force = args.force
# Get IP address from remote service
try:
current_ip = urlopen(ip_service).read().decode('utf-8')
except URLError:
try:
urlopen('https://www.google.com')
sys.exit('Error: Could not connect to IP service "{}". Not updating.'.format(ip_service))
except URLError:
# Print to stdout instead of stderr because, for logging purposes, we don't want to treat it as an error.
print('No internet. Not updating.')
sys.exit(1)
if not current_ip:
sys.exit('Error: No IP returned by IP service "{}". Not updating.'.format(ip_service))
# Get file path of info file
filename = 'info-{}.json'.format(domain)
path = os.path.dirname(os.path.realpath(__file__))
# strip out any nasty characters
# http://stackoverflow.com/questions/7406102/create-sane-safe-filename-from-any-unsafe-string
keep_characters = ('-','.','_')
filename = ''.join(c for c in filename if c.isalnum() or c in keep_characters).rstrip()
info_file = '{}/{}'.format(path, filename)
# Load data from info file
if os.path.isfile(info_file):
with open(info_file, 'r') as f:
info = json.load(f)
else:
info = {}
# Define util function
def api_request(url, data=None):
headers = {
'Authorization': "Token {}".format(token),
'Content-Type': 'application/json',
}
data = json.dumps(data).encode() if data else None
req = Request(url, data, headers=headers)
resp = urlopen(req).read()
return json.loads(resp)
# Update record if necessary
if force or info.get('content') != current_ip:
dns_uuid = info.get('id')
domain_uuid = info.get('domain')
# Get UUIDs if necessary
if not (dns_uuid and domain_uuid):
dns_list = api_request('https://my.opalstack.com/api/v1/dnsrecord/list/?embed=domain')
item = next((item for item in dns_list if item.get('domain').get('name') == domain), None)
if not item:
sys.exit('Error: No DNS record found for "{}"'.format(domain))
dns_uuid = item.get('id')
domain_uuid = item.get('domain').get('id')
# Set the IP address on Opalstack.
update_data = {
"id" : dns_uuid,
"domain" : domain_uuid,
"type" : "AAAA" if ':' in current_ip else "A",
"content" : current_ip,
}
dns_update = api_request('https://my.opalstack.com/api/v1/dnsrecord/update/', [update_data])
# Write response to file
with open(info_file, 'w') as f:
json.dump(dns_update[0], f, indent=2)
print('IP updated to %s' % current_ip)
else:
print('IP has not changed (%s). Not updating.' % current_ip)
@mightyohm
Copy link

mightyohm commented Oct 12, 2021

Looks like this script is broken as of this week (the v0 API is now returning 404).

@cvn
Copy link
Author

cvn commented Oct 13, 2021

I updated the script to use the v1 API. Thanks for the heads up, @mightyohm.

@mightyohm
Copy link

The new script appears to be working. Thanks for updating it!

@elromanos
Copy link

You should consider lowering the TTL value to the lowest possible Opalstack allows, especially if you are using this to self-host services in your home.

The lower the TTL, the faster the change will propagate across the internet.

@cvn
Copy link
Author

cvn commented Oct 15, 2021

That’s not a bad idea, @elromanos. The current value fits my use case, and it’s easy enough to change, so I’m going to leave the script as-is for now.

Another interesting alternative is to get the TTL value from the existing DNS record, instead of setting it explicitly in the script. This would allow you to have different TTL values for each record, and manage them all in the Opalstack UI.

@cvn
Copy link
Author

cvn commented Dec 24, 2021

I've updated the script with better logging. Info messages will be output to stdout, and errors to stderr. So, you can control what you log more easily. In my case, I redirect stdout to /dev/null when I use it with cron, so I only log when something is wrong. See the instructions for an example.

@elromanos the script no longer sets the TTL, and so it will preserve the existing TTL on a record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment