This repository has been archived on 2025-12-03. You can view files and clone it, but cannot push or open issues or pull requests.
cloudflare-ddns/cloudflare-ddns.py
Brendan Jackman 0f3708a482 Add a flag to modify config.json location
On Kubernetes, it's really awkward to write a Secret into the root directory:

https://www.jeffgeerling.com/blog/2019/mounting-kubernetes-secret-single-file-inside-pod

Therefore this adds support for reading the config from an arbitrary path. The
behaviour is unchanged if you don't set this new flag.
2021-02-28 18:24:25 +01:00

165 lines
5.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import argparse, requests, json, sys, signal, os, time
PATH = os.getcwd() + "/"
version = float(str(sys.version_info[0]) + "." + str(sys.version_info[1]))
if(version < 3.5):
raise Exception("This script requires Python 3.5+")
class GracefulExit:
kill_now = False
signals = {
signal.SIGINT: 'SIGINT',
signal.SIGTERM: 'SIGTERM'
}
def __init__(self):
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, signum, frame):
print("🛑 Stopping main thread...")
self.kill_now = True
def deleteEntries(type):
# Helper function for deleting A or AAAA records
# in the case of no IPv4 or IPv6 connection, yet
# existing A or AAAA records are found.
try:
for c in config["cloudflare"]:
answer = cf_api(
"zones/" + c['zone_id'] + "/dns_records?per_page=100&type=" + type, "GET", c)
for r in answer["result"]:
identifier = str(r["id"])
response = cf_api(
"zones/" + c['zone_id'] + "/dns_records/" + identifier, "DELETE", c)
print("Deleted stale record " + identifier)
except Exception:
print("Error deleting " + type + " record(s)")
def getIPs():
a = None
aaaa = None
try:
a = requests.get("https://1.1.1.1/cdn-cgi/trace").text.split("\n")
a.pop()
a = dict(s.split("=") for s in a)["ip"]
except Exception:
print("⚠️ Warning: IPv4 not detected.")
deleteEntries("A")
try:
aaaa = requests.get("https://[2606:4700:4700::1111]/cdn-cgi/trace").text.split("\n")
aaaa.pop()
aaaa = dict(s.split("=") for s in aaaa)["ip"]
except Exception:
print("⚠️ Warning: IPv6 not detected.")
deleteEntries("AAAA")
ips = []
if(a is not None):
ips.append({
"type": "A",
"ip": a
})
if(aaaa is not None):
ips.append({
"type": "AAAA",
"ip": aaaa
})
return ips
def commitRecord(ip, config):
for c in config["cloudflare"]:
subdomains = c["subdomains"]
response = cf_api("zones/" + c['zone_id'], "GET", c)
base_domain_name = response["result"]["name"]
ttl = 300 # default Cloudflare TTL
for subdomain in subdomains:
subdomain = subdomain.lower().strip()
record = {
"type": ip["type"],
"name": subdomain,
"content": ip["ip"],
"proxied": c["proxied"],
"ttl": ttl
}
dns_records = cf_api(
"zones/" + c['zone_id'] + "/dns_records?per_page=100&type=" + ip["type"], "GET", c)
fqdn = base_domain_name
if subdomain:
fqdn = subdomain + "." + base_domain_name
identifier = None
modified = False
duplicate_ids = []
for r in dns_records["result"]:
if (r["name"] == fqdn):
if identifier:
if r["content"] == ip["ip"]:
duplicate_ids.append(identifier)
identifier = r["id"]
else:
duplicate_ids.append(r["id"])
else:
identifier = r["id"]
if r['content'] != record['content'] or r['proxied'] != record['proxied']:
modified = True
if identifier:
if modified:
print("📡 Updating record " + str(record))
response = cf_api(
"zones/" + c['zone_id'] + "/dns_records/" + identifier, "PUT", c, {}, record)
else:
print(" Adding new record " + str(record))
response = cf_api(
"zones/" + c['zone_id'] + "/dns_records", "POST", c, {}, record)
for identifier in duplicate_ids:
identifier = str(identifier)
print("🗑️ Deleting stale record " + identifier)
response = cf_api(
"zones/" + c['zone_id'] + "/dns_records/" + identifier, "DELETE", c)
return True
def cf_api(endpoint, method, config, headers={}, data=False):
api_token = config['authentication']['api_token']
if api_token != '' and api_token != 'api_token_here':
headers = {
"Authorization": "Bearer " + api_token,
**headers
}
else:
headers = {
"X-Auth-Email": config['authentication']['api_key']['account_email'],
"X-Auth-Key": config['authentication']['api_key']['api_key'],
}
if(data == False):
response = requests.request(
method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers)
else:
response = requests.request(
method, "https://api.cloudflare.com/client/v4/" + endpoint, headers=headers, json=data)
return response.json()
def updateIPs(config):
for ip in getIPs():
commitRecord(ip, config)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--repeat", type=bool)
parser.add_argument("--config-path", dest="config", type=open, default=os.path.join(PATH, "config.json"))
args = parser.parse_args()
if args.repeat:
delay = 5*60
print("⏲️ Updating IPv4 (A) & IPv6 (AAAA) records every 5 minutes")
next_time = time.time()
killer = GracefulExit()
while not killer.kill_now:
time.sleep(max(0, next_time - time.time()))
updateIPs(json.load(args.config))
next_time += (time.time() - next_time) // delay * delay + delay
else:
updateIPs(json.load(args.config))