Netgate Discussion Forum
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Search
    • Register
    • Login

    Cloudflare DNS with multiple WAN

    Scheduled Pinned Locked Moved Routing and Multi WAN
    4 Posts 2 Posters 145 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • C
      Cabu
      last edited by Cabu

      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.

      C 1 Reply Last reply Reply Quote 0
      • C
        Cabu @Cabu
        last edited by

        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 :)

        S C 2 Replies Last reply Reply Quote 0
        • S
          SteveITS Galactic Empire @Cabu
          last edited by

          @Cabu Sounds like the bug listed at https://docs.netgate.com/pfsense/en/latest/releases/25-03.html#dynamic-dns ?

          Pre-2.7.2/23.09: Only install packages for your version, or risk breaking it. Select your branch in System/Update/Update Settings.
          When upgrading, allow 10-15 minutes to restart, or more depending on packages and device speed.
          Upvote ๐Ÿ‘ helpful posts!

          1 Reply Last reply Reply Quote 1
          • C
            Cabu @Cabu
            last edited by

            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()
            
            1 Reply Last reply Reply Quote 0
            • First post
              Last post
            Copyright 2025 Rubicon Communications LLC (Netgate). All rights reserved.