Skip to content

Instantly share code, notes, and snippets.

@nourselim0
Created October 4, 2025 02:22
Show Gist options
  • Select an option

  • Save nourselim0/985bcd6b29c2906a6a5c9bb86d38a5be to your computer and use it in GitHub Desktop.

Select an option

Save nourselim0/985bcd6b29c2906a6a5c9bb86d38a5be to your computer and use it in GitHub Desktop.
Cloudflare Dynamic DNS
import json
import os
import sys
import tempfile
from typing import cast
import requests
from requests.models import Response
CLOUDFLARE_API_TOKEN = "YOUR_API_TOKEN"
ZONE_NAME = "example.com" # Your root domain
RECORD_NAME = "dynamic.example.com" # The subdomain to update
REQ_HEADERS = {
"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}",
"Content-Type": "application/json",
}
STATE_FILE = os.path.join(tempfile.gettempdir(), "cloudflare_ddns.json")
def load_state() -> dict:
if not os.path.exists(STATE_FILE):
return {}
try:
with open(STATE_FILE, mode="r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return {}
def save_state(state: dict):
try:
with open(STATE_FILE, "w", encoding="utf-8") as f:
json.dump(state, f)
except Exception as e:
print(f"Warning, failed to write state file: {e}")
def handle_response(
resp: Response, http_err_msg: str, api_err_msg: str = None
) -> dict | str:
if resp.headers.get("Content-Type", "").startswith("application/json"):
data = cast(dict, resp.json())
err_text = json.dumps(data, indent=2)
else:
data = resp.text
err_text = data
if not resp.ok:
print(f"{http_err_msg} ({resp.status_code})\n{err_text}")
sys.exit(1)
if isinstance(data, str):
return data
if not data.get("success") or not data.get("result"):
print(f"{api_err_msg}\n{err_text}")
sys.exit(1)
return data
def get_external_ip() -> str:
resp = requests.get("https://api.ipify.org", timeout=10)
text = handle_response(resp, "Failed to fetch external IP")
return text.strip()
def get_zone_id() -> str:
url = f"https://api.cloudflare.com/client/v4/zones?name={ZONE_NAME}"
resp = requests.get(url, headers=REQ_HEADERS, timeout=10)
data = handle_response(
resp, "Failed to fetch zone info", f"Could not find zone: {ZONE_NAME}"
)
return data["result"][0]["id"]
def get_record_id(zone_id: str) -> tuple[str, str]:
url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records?type=A&name={RECORD_NAME}"
resp = requests.get(url, headers=REQ_HEADERS, timeout=10)
data = handle_response(
resp, "Failed to fetch DNS record", f"Could not find record: {RECORD_NAME}"
)
rec = data["result"][0]
return rec["id"], rec["content"]
def update_dns_record(zone_id: str, record_id: str, new_ip: str):
resp = requests.patch(
f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{record_id}",
json={
"type": "A",
"content": new_ip,
"ttl": 1, # 1 means "auto" (usually 5 min)
"proxied": False, # Dynamic DNS should not be proxied
},
headers=REQ_HEADERS,
timeout=10,
)
handle_response(
resp,
"Failed to update DNS record",
"Failed to update DNS record",
)
def main():
state = load_state()
current_ip = get_external_ip()
print(f"Current external IP: {current_ip}")
if state.get("last_external_ip") == current_ip:
print(f"No update needed, external IP is unchanged: {current_ip}")
sys.exit(0)
state["last_external_ip"] = current_ip
zone_id = state.get("zone_id") or get_zone_id()
state["zone_id"] = zone_id
record_id, old_ip = get_record_id(zone_id)
if old_ip == current_ip:
print(f"No update needed, DNS record is already set to {current_ip}")
else:
update_dns_record(zone_id, record_id, current_ip)
print(f"DNS record updated from {old_ip} to {current_ip}")
save_state(state)
if __name__ == "__main__":
main()
@nourselim0
Copy link
Author

nourselim0 commented Oct 4, 2025

If you want this script to run on windows whenever any network interface connects or disconnects, you can use the Task Scheduler and add a trigger "on an event" then choose "Custom" and paste this XML:

<QueryList>
  <Query Id="0" Path="Microsoft-Windows-NetworkProfile/Operational">
    <Select Path="Microsoft-Windows-NetworkProfile/Operational">*[System[Provider[@Name='Microsoft-Windows-NetworkProfile'] and (EventID=10000 or EventID=10001)]]</Select>
  </Query>
</QueryList>

this basically means it tracks the events you see in the Event Viewer under Applications and Services Logs > Microsoft > Windows > NetworkProfile > Operational and filters by Event ID 10000 (connected) and 10001 (disconnected).

If anybody knows how to do something similar on any Linux distro, share it in a comment here ✨

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