Building a Python DNS Updater for Reliable IP Changes
A changing public IP address can make a home lab, small office service, or remote access host difficult to reach. A Python script that detects the current address and updates a DNS record through a provider’s API removes the need to edit records manually each time an internet connection changes.
This pattern is useful for Australian developers running services over the NBN, where residential connections may receive dynamic addresses. It can also help a small business keep a subdomain available when a router reconnects after a power interruption in Sydney, Melbourne, or regional areas.
The finished utility should be safe to run repeatedly, use credentials carefully, and change a record only when the address has genuinely changed. A small amount of state management and logging makes the tool suitable for a cron job, a systemd timer, or a lightweight monitoring host.
Define The DNS Update Workflow
The updater has four responsibilities: discover the machine’s public IP, retrieve the existing DNS record, compare the two values, and submit an update when necessary. Keeping those tasks separate makes the program easier to test and allows the IP discovery method or DNS provider to be replaced later.
For an IPv4 record, a service such as https://api.ipify.org?format=json can return the address in a simple JSON response. The script should validate that the result is a real IPv4 address with Python’s ipaddress module before sending it to the DNS provider. If the hostname uses IPv6, use an AAAA record and an IPv6-aware discovery endpoint instead.
A provider-specific API normally needs a zone identifier, record identifier, record type, hostname, and new content. The record identifier is often different from the zone name, so the first run may need to search for the record and save its ID in configuration. The update operation should be idempotent: running it ten times with the same address should produce one change at most.
Protect Credentials And Configuration
API tokens should be stored outside the source file. Environment variables, a root-readable configuration file, or a secrets manager are preferable to hard-coded credentials. For a Linux host, a systemd service can load variables from a protected environment file with permissions such as 0600.
A narrowly scoped token is safer than a global account key. Most providers allow a token limited to DNS record editing for one zone. This reduces the damage caused by an accidental disclosure in a public Git repository, a copied terminal command, or an overly verbose log file.
A practical configuration might contain the zone ID, record name, record type, and TTL, while the token remains in the environment. Australian operators should also consider where application logs and secrets are stored when using a hosted monitoring platform, particularly for business systems subject to internal privacy policies.
Implement The Provider Request
The following example uses a Cloudflare-style API and the requests package. The record lookup happens by hostname and type, while the update uses the returned record ID. A production implementation should check the response body as well as the HTTP status and should set a timeout on every network request.
import ipaddress
import os
import requests
API = "https://api.cloudflare.com/client/v4"
zone_id = os.environ["DNS_ZONE_ID"]
token = os.environ["DNS_API_TOKEN"]
hostname = "home.example.com"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
ip_response = requests.get(
"https://api.ipify.org?format=json", timeout=10
)
ip_response.raise_for_status()
new_ip = ip_response.json()["ip"]
ipaddress.ip_address(new_ip)
params = {"type": "A", "name": hostname}
records = requests.get(
f"{API}/zones/{zone_id}/dns_records",
headers=headers, params=params, timeout=10
)
records.raise_for_status()
matches = records.json()["result"]
if len(matches) != 1:
raise RuntimeError("Expected exactly one matching DNS record")
record = matches[0]
if record["content"] != new_ip:
payload = {
"type": "A",
"name": hostname,
"content": new_ip,
"ttl": 300,
"proxied": False,
}
update = requests.put(
f"{API}/zones/{zone_id}/dns_records/{record['id']}",
headers=headers, json=payload, timeout=10
)
update.raise_for_status()
print(f"Updated {hostname} to {new_ip}")
else:
print("DNS record already matches")
The proxied setting is provider-specific and should reflect the service being exposed. A direct SSH endpoint, for example, generally cannot use an HTTP proxy. The script should also refuse to update when no record exists or when multiple records match, rather than silently changing an unintended hostname.
Handle Failures And DNS Behaviour
Network operations fail for ordinary reasons: a temporary DNS outage, an expired token, rate limiting, or a router that has not finished reconnecting. Catch requests exceptions, report a concise error, and return a non-zero exit status so the scheduler can retry. Exponential backoff is useful when several attempts are made within one run.
DNS caching means an update is not always visible immediately. A five-minute TTL may be suitable for a dynamic home service, while a lower value can increase query traffic and a higher value can leave clients using the old address longer. Testing should use dig or nslookup against several resolvers rather than relying only on the provider’s API response.
Time zones matter when an operator reads logs. Store timestamps in UTC and display local time only in dashboards or reports. This avoids confusion between Brisbane, which does not observe daylight saving, and Melbourne or Sydney, where daylight-saving changes can affect maintenance windows.
Schedule And Maintain The Utility
A systemd timer is usually more reliable than leaving a terminal process running. The service can execute the Python file every five or ten minutes, while the timer adds a small random delay to avoid synchronised requests. Cron is also suitable for a simple server, provided the environment variables and working directory are defined explicitly.
Logging should record the hostname, detected address, action taken, and failure reason, but never the token or complete request headers. A local rotating log or the system journal provides enough history to identify repeated failures. A warning after several unsuccessful runs can be sent through an existing email or monitoring service.
Before deploying, test with a non-production hostname and deliberately simulate an unchanged address, a changed address, an invalid API response, and a revoked token. Developers interested in practical Linux utilities and the broader history of small automation projects can find related open-source projects in Phil Schwartz’s portfolio, including tools shaped by real operational needs. For Australian businesses, documenting ownership, renewal dates, and access to the .au domain alongside the script keeps DNS maintenance understandable when responsibility changes at the end of the financial year.
