DDNS: proxy hostname auto-tracks dynamic WAN IP (Cloudflare A record + 15min updater timer) — fixes hardcoded-IP drift
This commit is contained in:
61
ddns.py
Normal file
61
ddns.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""DDNS updater — keep the proxy hostname pointed at the home WAN IP.
|
||||||
|
|
||||||
|
The SOCKS5/HTTP gateway is exposed on the home WAN IP (dynamic Comcast). The
|
||||||
|
storefront advertises proxy.clean-proxys.thetempleofdoom.com instead of the raw
|
||||||
|
IP; this updater keeps that A record in sync. DNS-only (proxied=false) because
|
||||||
|
Cloudflare's orange-cloud cannot carry raw TCP (SOCKS5/HTTP CONNECT).
|
||||||
|
|
||||||
|
Runs on CT777 (egresses via the home WAN, so ipify returns the real home IP —
|
||||||
|
NOT the MacBook's Nord egress). Scheduled by ddns.timer every 15 min.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CFG = json.load(open("/opt/pleiades/config.json"))
|
||||||
|
CF = CFG["cloudflare"]
|
||||||
|
|
||||||
|
API = "https://api.cloudflare.com/client/v4"
|
||||||
|
HDR = {"X-Auth-Email": CF["email"], "X-Auth-Key": CF["api_key"],
|
||||||
|
"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_wan_ip():
|
||||||
|
r = requests.get("https://api.ipify.org", timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def find_record():
|
||||||
|
r = requests.get(f"{API}/zones/{CF['zone_id']}/dns_records",
|
||||||
|
params={"name": CF["record_name"], "type": "A"},
|
||||||
|
headers=HDR, timeout=15)
|
||||||
|
r.raise_for_status()
|
||||||
|
recs = r.json().get("result", [])
|
||||||
|
return recs[0] if recs else None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
wan = get_wan_ip()
|
||||||
|
rec = find_record()
|
||||||
|
body = {"type": "A", "name": CF["record_name"], "content": wan,
|
||||||
|
"ttl": 120, "proxied": False}
|
||||||
|
if rec is None:
|
||||||
|
r = requests.post(f"{API}/zones/{CF['zone_id']}/dns_records",
|
||||||
|
headers=HDR, json=body, timeout=15)
|
||||||
|
ok = r.json().get("success")
|
||||||
|
print(f"created {CF['record_name']} -> {wan} (success={ok})")
|
||||||
|
return
|
||||||
|
cur = rec.get("content")
|
||||||
|
if cur == wan:
|
||||||
|
print(f"no change: {CF['record_name']} -> {wan}")
|
||||||
|
return
|
||||||
|
r = requests.put(f"{API}/zones/{CF['zone_id']}/dns_records/{rec['id']}",
|
||||||
|
headers=HDR, json=body, timeout=15)
|
||||||
|
ok = r.json().get("success")
|
||||||
|
print(f"updated {CF['record_name']}: {cur} -> {wan} (success={ok})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
ddns.service
Normal file
8
ddns.service
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Clean Proxys DDNS updater (one-shot)
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
WorkingDirectory=/opt/pleiades
|
||||||
|
ExecStart=/usr/bin/python3 /opt/pleiades/ddns.py
|
||||||
10
ddns.timer
Normal file
10
ddns.timer
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Clean Proxys DDNS updater (every 15 min)
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnBootSec=60s
|
||||||
|
OnUnitActiveSec=15min
|
||||||
|
Unit=ddns.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
Reference in New Issue
Block a user