cmc/cleberg.net

My personal web garden & blog.

clone: git clone https://gitbay.org/cmc/cleberg.net.git

main: content/blog/2022-02-10-njalla-dns-api.org · raw

  1#+date:        [2022-02-10 Thu 00:00:00]
  2#+title:       Dynamic DNS Updates via the Njalla API
  3#+description: A Python script to update DNS records automatically via the Njalla API.
  4#+slug:        njalla-dns-api
  5#+filetags:    :security:
  6
  7* Njalla's API
  8
  9As noted in my recent post about [[https://cleberg.net/blog/ditching-cloudflare/][switching to Njalla from Cloudflare]], I was
 10searching for a way to replace my [[https://cleberg.net/blog/cloudflare-dns-api.html][bash script]] to update my domain's =A= record
 11with my home's internet protocol (IP) address dynamically.
 12
 13To reiterate what I said in those posts, this is a common necessity for those of
 14us who have non-static IP addresses that can change at any moment due to
 15internet service provider (ISP) policy.
 16
 17In order to keep a home server running smoothly, the server admin needs to have
 18a process to constantly monitor their public IP address and update their
 19domain's DNS records if it changes.
 20
 21This post explains how to use Python to update Njalla's DNS (domain name system)
 22records whenever a machine's public IP address changes.
 23
 24** Creating a Token
 25
 26To use Njalla's API, you will first need to create a token that will be used to
 27authenticate you every time you call the API (application programming
 28interface). Luckily, this is very easy to do if you have an account with Njalla.
 29
 30Simply go the [[https://njal.la/settings/api/][API Settings]] page and click the =Add Token= button. Next, enter a
 31name for the token and click =Add=.
 32
 33Finally, click the =Manage= button next to your newly created token and copy the
 34=API Token= field.
 35
 36** Finding the Correct API Request
 37
 38Once you have a token, you're ready to call the Njalla API for any number of
 39requests. For a full listing of available requests, see the [[https://njal.la/api/][Njalla API
 40Documentation]].
 41
 42For this demo, we are using the =list-records= and =edit-record= requests.
 43
 44The =list-records= request requires the following payload to be sent when
 45calling the API:
 46
 47#+begin_src txt
 48params: {
 49    domain: string
 50}
 51#+end_src
 52
 53The =edit-record= request requires the following payload to be sent when calling
 54the API:
 55
 56#+begin_src txt
 57params: {
 58    domain: string
 59    id: int
 60    content: string
 61}
 62#+end_src
 63
 64* Server Set-Up
 65
 66To create this script, we will be using Python. By default, I use Python 3 on my
 67servers, so please note that I did not test this in Python 2, and I do not know
 68if Python 2 will work for this.
 69
 70** Creating the Script
 71
 72First, find a suitable place to create your script. Personally, I just create a
 73directory called =ddns= in my home directory:
 74
 75#+begin_src sh
 76mkdir ~/ddns
 77#+end_src
 78
 79Next, create a Python script file:
 80
 81#+begin_src sh
 82nano ~/ddns/ddns.py
 83#+end_src
 84
 85The following code snippet is quite long, so I won't go into depth on each part.
 86However, I suggest you read through the entire script before running it; it is
 87quite simple and contains comments to help explain each code block.
 88
 89*Note*: You will need to update the following variables for this to work:
 90
 91- =token=: This is the Njalla API token you created earlier.
 92- =user_domain=: This is the top-level domain you want to modify.
 93- =include_subdomains=: Set this to =True= if you also want to modify subdomains
 94  found under the TLD (top-level domain).
 95- =subdomains=: If =include_subdomains= = =True=, you can include your list of
 96  subdomains to be modified here.
 97
 98#+begin_src python
 99#!/usr/bin/python
100# -*- coding: utf-8 -*-
101# Import Python modules
102
103from requests import get
104import requests
105import json
106
107# Set global variables
108
109url = 'https://njal.la/api/1/'
110token = '<your-api-token>'
111user_domain = 'example.com'
112include_subdomains = True
113subdomains = ['one', 'two']
114
115
116# Main API call function
117
118def njalla(method, **params):
119    headers = {'Authorization': 'Njalla ' + token}
120    response = requests.post(url, json={'method': method,
121                             'params': params}, headers=headers).json()
122    if 'result' not in response:
123        raise Exception('API Error', response)
124    return response['result']
125
126
127# Gather all DNS records for a domain
128
129def get_records(domain):
130    return njalla('list-records', domain=user_domain)
131
132
133# Update a DNS record for a domain
134
135def update_record(domain, record_id, record_content):
136    return njalla('edit-record', domain=domain, id=record_id,
137                  content=record_content)
138
139
140# Get public IP addresses
141
142ipv4 = get('https://api.ipify.org').text
143print('IPv4: {}'.format(ipv4))
144ipv6 = get('https://api64.ipify.org').text
145print('IPv6: {}'.format(ipv6))
146
147# Call API to get all DNS records
148
149data = get_records(user_domain)
150
151# Loop through records and check if each one is IPv4 (A) or IPv6 (AAAA)
152# Update only if DNS is different from server IP
153
154for record in data['records']:
155    if record['name'] == '@' or (include_subdomains and record['name'] \
156        in subdomains):
157        if record['type'] == 'A':
158            if record['content'] == ipv4:
159                print(record['type'], 'record for', record['name'],
160                      'already matches public IPv4 address. Skipping...'
161                      )
162            else:
163                print('IPv4 of', ipv4,
164                      'does not match Njalla's value of',
165                      record['content'], '. Updating...')
166                update_record(user_domain, record['id'], ipv4)
167        elif record['type'] == 'AAAA':
168            if record['content'] == ipv6:
169                print(record['type'], 'record for', record['name'],
170                      'already matches public IPv6 address. Skipping...'
171                      )
172            else:
173                print('IPv6 of', ipv6,
174                      'does not match Njalla's value of',
175                      record['content'], '. Updating...')
176                update_record(user_domain, record['id'], ipv6)
177#+end_src
178
179** Running the Script
180
181Once you've created the script and are ready to test it, run the following
182command:
183
184#+begin_src sh
185python3 ~/ddns/ddns.py
186#+end_src
187
188** Setting the Script to Run Automatically
189
190To make sure the scripts run automatically, add it to the =cron= file so that it
191will run on a schedule. To do this, open the =cron= file:
192
193#+begin_src sh
194crontab -e
195#+end_src
196
197In the cron file, paste the following at the bottom of the editor in order to
198check the IP every five minutes:
199
200#+begin_src sh
201*/5 * * * * python3 /home/<your_username>/ddns/ddns.py
202#+end_src