Quick Cloudflare Dynamic DNS Script Using UPnP

Robin Chan

I have an OpenWRT router which is constantly running as a switch, (it's an old Netgear DGN3500), I recently installed a USB drive and Python3 onto this which makes it a really powerful little thing.

I also have a home server - but any smaller jobs I can migrate from it, the better. I created the below script to constantly ping Cloudflare using UPnP to update my external IP Address onto an A record.

I like this approach as it requires no external server to tell me my address - just the routers UPnP service, it also self-recovers and logs any occurrences of an internet outage, as well as previous IP addresses.

I use this for things like a home VPN and a self hosted Gitlab.

To run this on windows, you can use the below batch file coupled with a hidden scheduled task.

@echo off
python cf.py
exit /b 0

Replace the following variables:

VALUE_TO_UPDATE: The name of the record - e.g. vpn.example.com
CF_EMAIL: Cloudflare email
CF_API_KEY: Cloudflare API key
CF_RECORD_URL: The URL of the resource to update e.g. https://cloudflare_api_url/zones/3b...c5/dns_records/30...22

import upnpclient
import pickle
from datetime import datetime
import json
import requests
import os

try:
    if not os.path.exists("igd.p"):
        pickle.dump({}, open("igd.p", "wb"))

    def log_format(msg):
        time = datetime.now().strftime("%m/%d/%Y, %H:%M:%S")
        return f"[{time}] - {msg}"

    def log_msg(msg, file='idg.log'):
        with open(file, "a") as f:
            f.write(log_format(msg))

    def dump_idg(ip, status, last_error, uptime):
        igd = {
            "ip": ip,
            "status": status,
            "last_error": last_error,
            "uptime": uptime
        }

        pickle.dump(igd, open("igd.p", "wb"))

    device = upnpclient.Device("http://192.168.4.1:1900/igd.xml")
    # devices = upnpclient.discover() - for auto discovery

    igd = pickle.load(open("igd.p", "rb"))

    ip = device.WANIPConnection \
        .GetExternalIPAddress().get('NewExternalIPAddress')

    stats = device.WANIPConnection.GetStatusInfo()

    status = stats.get('NewConnectionStatus')
    last_error = stats.get('NewLastConnectionError')
    uptime = stats.get('NewUptime')

    if (
        ip != igd.get("ip") or
        status != igd.get("status") or
        last_error != igd.get("last_error") or
        int(uptime) < int(igd.get("uptime")
                          )):

        if (status != "Connected"):
            log_msg(status)
            dump_idg(ip, status, last_error, uptime)
            exit()

        log_msg(json.dumps(igd))

        data = {
            "type": "A",
            "name": "VALUE_TO_UPDATE",
            "content": ip,
            "ttl": 600
        }

        headers = {
            "X-Auth-Email": "CF_EMAIL",
            "X-Auth-Key": "CF_API_KEY",
            "Content-Type": "application/json"
        }

        resp = requests.put("CF_RECORD_URL",
                            data=json.dumps(data), headers=headers)

    dump_idg(ip, status, last_error, uptime)
except Exception as e:
    log_msg(str(e), 'exceptions.log')