2025-07-24
i had the domain for a while now and wanted to try self hosting the site myself, however one of the main things that you need for hosting a website is an ip address that is accessible which i don’t have due to my isp using a cgnat.
the only way i could self host was either using services like tailscale or ngrok both of while i didn’t really fancy using. so i turned to using ipv6. (some)ipv6 addresses can be accessible from the internet and are unique to my device. however the ipv6 address assigned to me weren’t permanent i.e the prefix address was constantly being changed by my isp. this was a major issue as i couldn’t just create an AAAA record and be done with it, i needed to update the dns record as my ip changed, this is called as dynamic dns where i can update my dns records whenever my address changes.
however, namecheap doesn’t support dynamic dns for AAAA records and doesn’t provide an api for me to change it either. so i changed the domain’s nameservers from namecheap’s to cloudflare, which gave me an api to change the record in a single request.
so the basic idea was to:
every 10 minutes run a cron job that runs the dns script
the dns script checks the current ip to the stored ip in a file
if the current ip isn’t the same as the one in the file run, update the dns record using cloudflare’s api to match the current ip and store the current ip in the file
the script that i wrote
from cloudflare import Cloudflare
import os
import logging
ZONE_ID = os.environ.get('ZONE_ID')
CLOUDFLARE_API_TOKEN = os.environ.get('CLOUDFLARE_API_TOKEN')
DEFAULT_RECORD_NAME = "athpat.me"
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
console_handler = logging.FileHandler('dnsupdate.log')
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
import subprocess
def get_ipv6():
bash_script = r'''
ip -6 addr show scope global dynamic mngtmpaddr | \
awk '
/inet6/ {
ip=$2
next
}
/preferred_lft/ {
if ($2 != "0sec") {
split(ip, parts, "/")
print parts[1]
}
}
' | head -n1
'''
try:
result = subprocess.check_output(
bash_script,
shell=True,
text=True
).strip()
return result if result else None
except subprocess.CalledProcessError as e:
print("Error fetching IPv6:", e)
return None
client = Cloudflare(api_token=CLOUDFLARE_API_TOKEN)
def update_ipv6_record(content, record_name=DEFAULT_RECORD_NAME):
page = client.dns.records.list(
zone_id=ZONE_ID,
)
dns_record = next(
(
record
for record in page.result
if record.type == "AAAA" and record.name == record_name
),
None,
)
if not dns_record:
raise Exception(f"couldn't find record id of {record_name}")
response = client.dns.records.edit(
dns_record_id=dns_record.id,
zone_id=ZONE_ID,
name=record_name,
type="AAAA",
content=content,
ttl=60,
proxied=False,
)
logger.info(
f"updated ipv6 dns record for {dns_record.name} from {dns_record.content} to {content} on {response.modified_on}"
)
try:
try:
stored_ip = open("ip_addr", "r").read()
except:
stored_ip = ""
current_ip = get_ipv6()
if current_ip != stored_ip:
logger.info(f"ipv6 changed, old: {stored_ip} current: {current_ip}")
update_ipv6_record(content=current_ip)
open("ip_addr", "w").write(current_ip)
else:
logger.info(f"stored_ip: {stored_ip} == current_ip: {current_ip}, ignoring")
except Exception as e:
logger.error(e)could’ve written it in bash but python feels way more comfortable to
me. also instead of polling every 10 minutes i’ll have to look into
things like /etc/networkd-dispatcher/routable.d/ or maybe
netlink, but for the time being it’s sufficient.