Cloudflare DNS with multiple WAN
-
I have setup a multi wan config with starlink (SL_WAN) without public IP for high throughput and my ADSL provider (P_WAN) for its ability to have a public dynamic IP. But the Cloudflare Dynamic DNS client who is setup to use the P_WAN interface, send my SL_WAN IP to update the records :(
I should probably note that the routers for SL_WAN and P_WAN are not in bridge mode. -
I worked around by using a custom dynamic dns client that run this script as a cgi from my webserver:
#!/var/www/cloudflare/venv/bin/python3 # -*- coding: utf-8 -*- import requests import json import re # Cloudflare API settings API_TOKEN = 'my_api_token' ZONE_ID = 'my_zone_id' # TTL constant AUTO = 120 # 120 = Auto # DNS records to update RECORDS_TO_UPDATE = [ {'name': 'domain.org', 'type': 'A', 'proxied': True, 'ttl': AUTO}, {'name': '*.domain.org', 'type': 'A', 'proxied': True, 'ttl': AUTO}, {'name': 'minecraft.domain.org', 'type': 'A', 'proxied': False, 'ttl': AUTO} ] # API headers HEADERS = { 'Authorization': f'Bearer {API_TOKEN}', 'Content-Type': 'application/json', } def get_public_ip(): """Fetch current public IP from checkip.dyndns.org""" try: response = requests.get("http://checkip.dyndns.org/") ip = re.search(r"Current IP Address: ([\d.]+)", response.text).group(1) return ip except Exception as e: raise RuntimeError(f"Failed to detect public IP: {e}") def get_dns_record_id(record_name, record_type): """Retrieve DNS record ID from Cloudflare""" url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records' params = {'type': record_type, 'name': record_name} response = requests.get(url, headers=HEADERS, params=params) if response.status_code != 200: raise RuntimeError(f"Failed to fetch DNS record: {response.text}") result = response.json().get('result', []) if not result: raise RuntimeError(f"No matching DNS record for {record_name} ({record_type})") return result[0]['id'] def update_dns_record(record_id, name, record_type, proxied, ttl, new_ip): """Update a specific DNS record""" url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records/{record_id}' payload = { 'type': record_type, 'name': name, 'content': new_ip, 'ttl': ttl, 'proxied': proxied } response = requests.put(url, headers=HEADERS, data=json.dumps(payload)) if response.status_code == 200: print(f"โ Updated {name} to {new_ip}") else: print(f"โ Failed to update {name}: {response.text}")
I also ensure that all requests to checkip.dyndns.org go through my P_WAN with a rule :)
-
@Cabu Sounds like the bug listed at https://docs.netgate.com/pfsense/en/latest/releases/25-03.html#dynamic-dns ?
-
Oups the script was not complete :( Here an updated version :)
#!/var/www/cloudflare/venv/bin/python3 # -*- coding: utf-8 -*- import requests import json import re import os # Cloudflare API settings API_TOKEN = 'my_api_token' ZONE_ID = 'my_zone_id' # TTL constant (120 = "Auto" on Cloudflare) AUTO = 120 # DNS records to update RECORDS_TO_UPDATE = [ {'name': 'domain.org', 'type': 'A', 'proxied': True, 'ttl': AUTO}, {'name': '*.domain.org', 'type': 'A', 'proxied': True, 'ttl': AUTO}, {'name': 'minecraft.domain.org', 'type': 'A', 'proxied': False, 'ttl': AUTO} ] # API headers HEADERS = { 'Authorization': f'Bearer {API_TOKEN}', 'Content-Type': 'application/json', } # File path to store last known IP LAST_IP_FILE = os.path.join(os.path.dirname(__file__), 'last_ip.txt') def get_public_ip(): """Fetch current public IP from checkip.dyndns.org""" try: response = requests.get("http://checkip.dyndns.org/") ip = re.search(r"Current IP Address: ([\d.]+)", response.text).group(1) return ip except Exception as e: raise RuntimeError(f"Failed to detect public IP: {e}") def load_last_ip(): """Read the last saved public IP address""" try: with open(LAST_IP_FILE, 'r') as f: return f.read().strip() except FileNotFoundError: return None def save_current_ip(ip): """Save the current public IP address""" with open(LAST_IP_FILE, 'w') as f: f.write(ip) def get_all_dns_records(): """Fetch all DNS records in the Cloudflare zone""" url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records' response = requests.get(url, headers=HEADERS) if response.status_code != 200: raise RuntimeError(f"Failed to fetch DNS records: {response.text}") return response.json().get('result', []) def update_dns_record(record_id, name, record_type, proxied, ttl, new_ip): """Update a DNS record on Cloudflare""" url = f'https://api.cloudflare.com/client/v4/zones/{ZONE_ID}/dns_records/{record_id}' payload = { 'type': record_type, 'name': name, 'content': new_ip, 'ttl': ttl, 'proxied': proxied } response = requests.put(url, headers=HEADERS, data=json.dumps(payload)) if response.status_code == 200: print(f"โ Updated {name} to {new_ip}") else: raise RuntimeError(f"Failed to update {name}: {response.text}") def main(): # Output HTTP header (for CGI) print('Content-Type: text/plain; charset=utf-8\n') try: current_ip = get_public_ip() last_ip = load_last_ip() print(f"๐ Current Public IP: {current_ip}") if current_ip == last_ip: print("โธ๏ธ Public IP has not changed. Skipping update.") return all_records = get_all_dns_records() record_id_map = {(rec['name'], rec['type']): rec['id'] for rec in all_records} all_success = True for record in RECORDS_TO_UPDATE: key = (record['name'], record['type']) record_id = record_id_map.get(key) if not record_id: print(f"โ ๏ธ No record ID found for {record['name']} ({record['type']})") all_success = False continue try: update_dns_record(record_id, record['name'], record['type'], record['proxied'], record['ttl'], current_ip) except Exception as e: print(f"โ Failed to update {record['name']}: {e}") all_success = False if all_success: save_current_ip(current_ip) print("โ All records updated successfully. IP saved.") else: print("โ ๏ธ Some updates failed. IP not saved to ensure retry next time.") except Exception as e: print(f"๐ซ Script failed: {e}") if __name__ == '__main__': main()
Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.