Dynamic DNS hostname with Cloudflare

Python script to automatically update a Cloudflare DNS Host (A) record with the new IP address of the server.

C05348A3-9AB8-42C9-A6E0-81DB3AC59FEB
           

If you use Cloudflare as a DNS provider, you can use a Python script to update the DNS Host record of a VM every time a VM comes up with a new IP address. The script has to be executed on the server itself because it will make a call to https://api.ipify.org/ to get the host's external IP address.

Obtain a Cloudflare API token with proper authorization

Before being able to execute this script, you need to obtain a few details from Cloudflare. In your account, under the "Overview" section of your Domain, under "API", you should be able to find your Zone ID, copy that information and replace <zone_id> with it in the script.

Then, click on "Get your API token".

On that page, under "API Tokens", click on "Create Token" and use the "Edit zone DNS" template.

You'll need the following permissions:

  • Resources: Zone, Permissions: Zone, Read
  • Resources: Zone, Permissions: DNS, Edit

Under "Zone Resources", select "Include Specific zones" and select your Domain name.

Cloudflare will then provide you with an API Token you can use, you will need to replace the value of <cloudflare_token> in the script with that token.

You also need to replace the value of <your.dns.name> with the DNS Hostname (A Record) that you will want your host to have.

Obtain the DNS record's ID

You will then need to obtain the proper value of <record_id>, which is the Cloudflare ID of that particular DNS entry. To do so, you'll have to manually create that host (A) record in Cloudflare with any IP address, and then query the ID of that record using this script:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests

token = '<cloudflare_token>'
DNSname = '<your.dns.name>'
zone_id = '<zone_id>'

url = f'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?type=A&name={DNSname}&page=1&per_page=20&order=type&direction=desc&match=all'

headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': f'Bearer {token}'
}

try:
    r = requests.request(method='GET', url=url, headers=headers, timeout=25)
except Exception as x:
    print('Connection failed :( %s' % x.__class__.__name__)
else:
    print(r.status_code)
    if r.status_code == 200:
        response = r.content
        print(response)

    else:
        print('Error: %s' % r.status_code)
        print(r.headers)
        print(r.text)

This script will output something like this (heavily redacted). Note the value of the field "id" returned and replace "<record_id>" with the value of that field in the script below. You will only have to do this once, the ID will not change as long as you don't delete the record.

python3 get_record_id.py 
200
b'{"result":[{"id":"<record_id>","zone_id":"<zone_id>","zone_name":"dns.name","name":"your.dns.name","type":"A","content":"172.0.0.1","proxiable":true,"proxied":false,"ttl":120,"locked":false,"meta":{"auto_added":false,"managed_by_apps":false,"managed_by_argo_tunnel":false,"source":"primary"},"created_on":"2021-07-31T21:13:57.939216Z","modified_on":"2022-03-07T00:37:04.153213Z"}],"success":true,"errors":[],"messages":[],"result_info":{"page":1,"per_page":20,"count":1,"total_count":1,"total_pages":1}}'

Update the DNS host (A) record with the current IP

This is the script that you can now use to set the IP address for that DNS name as many times as needed. Note that the script first makes a call to https://api.ipify.org/ to obtain the public IP of the host, and then updates the DNS record with that IP address. Dynamic DNS in a nutshell!

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

""""
Update the value of an existing A record in Cloudflare based on the host's current public IP address.
"""


# I M P O R T S ###############################################################

import urllib
import json
import sys
import os
import time
from logging.handlers import RotatingFileHandler
import traceback
import datetime
import requests
import logging

__version__ = "1.0.1"


# G L O B A L S ###############################################################

zone_id = '<zone_id>'
token = '<cloudflare_token>'
DNSname = '<your.dns.name>'
record_id = '<record_id>'

logger = logging.getLogger()
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(formatter)
logger.addHandler(ch)
try:
    logFile = os.path.realpath(__file__).split('.')[-2] + ".log"
    fh = RotatingFileHandler(logFile, maxBytes=(1048576 * 50), backupCount=7)
    fh.setLevel(logging.DEBUG)
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    logger.info("Log file:    %s" % logFile)
except Exception as e:
    logger.warning("Unable to log to file: %s - %s" % (logFile, e))
logger.info("-" * 80)
logger.info("Version:  %s" % (__version__))


# F U N C T I O N S ###########################################################


def main():

    url = f'https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}'

    t0 = time.time()
    try:
        r = requests.request(method='GET', url='https://api.ipify.org/', timeout=25)
    except Exception as x:
        logger.error('Connection failed :( %s' % x.__class__.__name__)
    else:
        if r.status_code == 200:
            response = r.content
            newIP = str(response, 'utf-8').strip()
            logger.info(newIP)
        else:
            logger.error('Error: %s' % r.status_code)
            logger.debug(r.headers)
            logger.debug(r.text)
            logger.debug(sys.exc_info()[:2])
    finally:
        t1 = time.time()
        logger.info('Connection took %.4fs' % (t1 - t0))


    headers = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': f'Bearer {token}'
    }
    data = {
        "type": "A",
        "name": DNSname,
        "content": newIP,
        "ttl": 120,
        "proxied": False
    }

    logger.info("Updating IP address")
    t0 = time.time()
    try:
        r = requests.request(method='PUT', url=url, data=json.dumps(data), headers=headers, timeout=25)
    except Exception as x:
        logger.error('Connection failed :( %s' % x.__class__.__name__)
    else:
        logger.info(r.status_code)
        if r.status_code == 200:
            response = r.content
            logger.info(response)
        else:
            logger.error('Error: %s' % r.status_code)
            logger.error(r.headers)
            logger.error(r.text)
            logger.error(sys.exc_info()[:2])
    finally:
        t1 = time.time()
        logger.info('Connection took %.4fs' % (t1 - t0))

    sys.exit(0)


if __name__ == "__main__":
    main()

# E N D   O F   F I L E #######################################################

Note that this script will blindly update the record even if the IP address hasn't changed. You can make it more efficient if you want by storing that value and updating the record only if it has changed.

This script also writes to a logfile, so that you can review results if you schedule the task.

Posted Comments: 0

Tagged with:
DNS networking