{"id":21588811,"url":"https://github.com/sunsetquest/dynamic-dns-record-updater-for-cloudflare","last_synced_at":"2026-05-17T12:14:50.210Z","repository":{"id":262324580,"uuid":"886881793","full_name":"SunsetQuest/Dynamic-DNS-Record-Updater-For-Cloudflare","owner":"SunsetQuest","description":"Dynamic DNS (DDNS) is a service that automatically updates your Cloudflare DNS records when your public IP address changes. This tool effectively allows your domain website to be hosted on a dynamic(not static) address.","archived":false,"fork":false,"pushed_at":"2024-11-22T19:04:29.000Z","size":52,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-24T16:12:17.126Z","etag":null,"topics":["cloudflare","ddns","dns","domain-name"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SunsetQuest.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-11T19:21:14.000Z","updated_at":"2025-01-20T07:06:19.000Z","dependencies_parsed_at":null,"dependency_job_id":"d40784f5-678b-4045-872e-0e12909aabd7","html_url":"https://github.com/SunsetQuest/Dynamic-DNS-Record-Updater-For-Cloudflare","commit_stats":null,"previous_names":["sunsetquest/dynamic-dns-record-updater-for-cloudflare"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SunsetQuest%2FDynamic-DNS-Record-Updater-For-Cloudflare","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SunsetQuest%2FDynamic-DNS-Record-Updater-For-Cloudflare/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SunsetQuest%2FDynamic-DNS-Record-Updater-For-Cloudflare/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SunsetQuest%2FDynamic-DNS-Record-Updater-For-Cloudflare/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SunsetQuest","download_url":"https://codeload.github.com/SunsetQuest/Dynamic-DNS-Record-Updater-For-Cloudflare/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244197664,"owners_count":20414437,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","ddns","dns","domain-name"],"created_at":"2024-11-24T16:11:21.207Z","updated_at":"2026-05-17T12:14:50.116Z","avatar_url":"https://github.com/SunsetQuest.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Dynamic DNS Record Updater for Cloudflare\n\nAn application that automatically updates your Cloudflare DNS records with your current external IP address. This tool effectively turns your domain into a dynamic DNS, allowing you to access services hosted on a dynamic IP address.\n\n## Overview\n\nThis tool periodically checks your external IP address and updates specified DNS records on Cloudflare if a change is detected. It's ideal for users with a dynamic IP address who need to maintain up-to-date DNS records without manual intervention.\n\n## Features\n\n- **Automated IP Detection**: Fetches your current external IP address using a reliable API.\n- **Dynamic DNS Updates**: Updates your Cloudflare DNS records when your IP address changes.\n- **Multiple Domain Support**: Configure multiple domains or subdomains to update simultaneously.\n- **Configuration Flexibility**: Uses a `config.json` file for easy setup and supports environment variables for sensitive data.\n- **Logging**: Provides detailed logs of operations and errors for easy troubleshooting.\n\n## Prerequisites\n\n- **.NET 6.0 SDK or higher**: Ensure you have the .NET SDK installed. You can download it from the [.NET website](https://dotnet.microsoft.com/download).\n\n## Installation\n\n1. **Clone the Repository**\n\n   ```shell\n   git clone https://github.com/SunsetQuest/Dynamic-DNS-Record-Updater-For-Cloudflare\n   cd dynamic-dns-cloudflare\n   ```\n\n2. **Install Dependencies**\n\n   Restore the NuGet packages required by the application:\n\n   ```shell\n   dotnet restore\n   ```\n\n## Configuration\n\n### 1. Create a Cloudflare API Token\n\nYou need a Cloudflare API token with the following permissions:\n\n- **Zone - DNS - Edit**\n\n**Steps to Create an API Token:**\n\n1. Log in to your Cloudflare dashboard\n2. Navigate to **My Profile** \u003e **API Tokens**.\n3. Click on **Create Token**.\n4. Use the **Edit DNS Zone** template or create a custom token with **Zone - DNS - Edit** permissions.\n5. Restrict the token to the specific zone (domain) you want to update.\n6. Copy the generated API token and keep it secure.\n\n### 2. Prepare the Configuration File\n\nCreate a `config.json` file in the application directory or a custom location.\n\n**Example `config.json`:**\n\n```json\n{\n  \"ZoneId\": \"your_zone_id\",\n  \"ApiToken\": \"your_api_token\", // Optional: It's recommended to use the environment variable for better security\n  \"Domains\": [ \"yourdomain.com\", \"subdomain.yourdomain.com\" ],\n  \"FrequencyToCheckInMinutes\": 120\n}\n```\n\n**Notes:**\n\n- **ZoneId**: Your Cloudflare Zone ID, found in the dashboard under **Overview**.\n- **ApiToken**: Your Cloudflare API token. For security, it's recommended to set this via an environment variable.\n- **Domains**: A list of domains or subdomains you wish to update.\n- **FrequencyToCheckInMinutes**: How often (in minutes) the application checks for IP changes.\n\n### 3. Set the API Token as an Environment Variable (Recommended)\n\nSet the `CF_API_TOKEN` environment variable to securely provide your API token to the application.\n\n**For Windows Command Prompt:**\n\n```shell\nset CF_API_TOKEN=your_api_token\n```\n\n**For Windows PowerShell:**\n\n```powershell\n$env:CF_API_TOKEN=\"your_api_token\"\n```\n\n**For Linux/macOS Terminal:**\n\n```shell\nexport CF_API_TOKEN=your_api_token\n```\n\n## Usage\n\n### Running the Application\n\n**Default Configuration File (`config.json` in the same directory):**\n\n```shell\ndotnet run\n```\n\n**Custom Configuration File Path:**\n\n```shell\ndotnet run -- -config /path/to/your/config.json\n```\n\n- The `--` indicates that the following arguments are for your application, not for `dotnet`.\n\n### As a Compiled Executable\n\nYou can publish the application as a self-contained executable:\n\n```shell\ndotnet publish -c Release -r win-x64 --self-contained true\n```\n\nReplace `win-x64` with `linux-x64` or `osx-x64` depending on your operating system.\n\nRun the published executable:\n\n```shell\n./bin/Release/net6.0/win-x64/publish/dynamic-dns-cloudflare.exe -config /path/to/your/config.json\n```\n\n## Logging\n\nThe application uses the built-in logging framework to provide informative messages. Logs include timestamps and indicate whether an operation was successful or if an error occurred.\n\n## Error Handling\n\n- The application will log errors and exit gracefully if:\n\n  - The configuration file is missing or invalid.\n  - Required configuration fields are missing.\n  - There are issues connecting to the external IP service or Cloudflare API.\n\n- Detailed error messages and stack traces are provided in the logs to assist with troubleshooting.\n\n## Security Considerations\n\n- **API Token**: Avoid storing your API token in the `config.json` file. Use environment variables to keep sensitive information secure.\n- **Configuration File**: Ensure your `config.json` file is stored securely and not committed to version control if it contains sensitive data.\n\n## Dependencies\n\n- **Newtonsoft.Json**: For JSON parsing.\n- **Microsoft.Extensions.Logging.Console**: For logging to the console.\n\nInstall these packages via NuGet:\n\n```shell\ndotnet add package Newtonsoft.Json\ndotnet add package Microsoft.Extensions.Logging.Console\n```\n\n## Example Output\n\n```\n12:00:00 Information: Checking yourdomain.com to ensure it matches 203.0.113.10.\n12:00:02 Information: yourdomain.com IP address matches. No update needed.\n12:00:02 Information: Checking subdomain.yourdomain.com to ensure it matches 203.0.113.10.\n12:00:03 Information: IP addresses do not match. Updating DNS record...\n12:00:05 Information: subdomain.yourdomain.com DNS record updated successfully.\n```\n\n## Graphic Illustration\n```mermaid\nflowchart TD\n    A[Start] --\u003e B[External IP Check]\n    B --\u003e C[Fetch Current External IP from Service]\n    C --\u003e D[DNS Record Retrieval]\n    D --\u003e E[Fetch Current DNS Records from Cloudflare]\n    E --\u003e F[Comparison]\n    F --\u003e G{Does the External IP match the DNS record IP?}\n\n    G -- Yes --\u003e H[End - No Update Needed - wait FrequencyToCheckInMinutes and start again]\n    G -- No --\u003e I[Update DNS Records]\n    I --\u003e J[Update Cloudflare DNS via API]\n    J --\u003e K[End - DNS Records Updated - wait FrequencyToCheckInMinutes and start again]\n```\n\n## Full Code Example\n\nHere's the complete code for the application:\n\n```csharp\n// Dynamic DNS Record Updater for Cloudflare\n// License: MIT license\n// This tool was created to automatically update DNS records.\n// Created by ChatGPT and Ryan Scott White\n\n// Instructions:\n//   1. Create an API token with the required permissions.\n//   2. Set the ZoneId, ApiToken, Domains list, and FrequencyToCheckInMinutes in the config.json file.\n\nusing Microsoft.Extensions.Logging;  // Install-Package Microsoft.Extensions.Logging.Console\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Linq; // Install-Package Newtonsoft.Json\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Text;\nusing System.Threading.Tasks;\n\npublic class Configuration\n{\n    public required string ZoneId { get; set; }\n    public required string ApiToken { get; set; }\n    public required List\u003cstring\u003e Domains { get; set; }\n    public int FrequencyToCheckInMinutes { get; set; }\n}\n\npublic class Program\n{\n    private static ILogger\u003cProgram\u003e? _logger;\n\n    public static async Task Main(string[] args)\n    {\n        // Setup logging\n        using ILoggerFactory loggerFactory = LoggerFactory.Create(builder =\u003e\n        {\n            _ = builder.AddSimpleConsole(options =\u003e\n            {\n                options.IncludeScopes = true;\n                options.SingleLine = true;\n                options.TimestampFormat = \"hh:mm:ss \";\n            });\n            _ = builder.SetMinimumLevel(LogLevel.Information);\n        });\n        _logger = loggerFactory.CreateLogger\u003cProgram\u003e();\n\n        // Parse command-line arguments to get the configuration file path\n        string configFilePath = \"config.json\"; // Default config file name\n        if (args.Length \u003e 0)\n        {\n            for (int i = 0; i \u003c args.Length; i++)\n            {\n                if (args[i].Equals(\"-config\", StringComparison.OrdinalIgnoreCase) \u0026\u0026 i + 1 \u003c args.Length)\n                {\n                    configFilePath = args[i + 1];\n                    break;\n                }\n            }\n        }\n\n        // Load configuration from file\n        Configuration config = LoadConfiguration(configFilePath);\n\n        // Validate configuration\n        if (config == null || !ValidateConfiguration(config))\n        {\n            _logger.LogError(\"Configuration is missing or invalid. Exiting application.\");\n            return;\n        }\n\n        // Override ApiToken with environment variable if set\n        string envApiToken = Environment.GetEnvironmentVariable(\"CF_API_TOKEN\");\n        if (!string.IsNullOrEmpty(envApiToken))\n        {\n            config.ApiToken = envApiToken;\n            _logger.LogInformation(\"ApiToken loaded from environment variable.\");\n        }\n\n        // Main loop\n        string lastExternalIP = \"\";\n        while (true)\n        {\n            if (lastExternalIP != \"\")\n            {\n                _logger.LogInformation(\"Waiting {Minutes} minutes.\", config.FrequencyToCheckInMinutes);\n                await Task.Delay(TimeSpan.FromMinutes(config.FrequencyToCheckInMinutes));\n            }\n\n            using HttpClient client = new();\n            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(\"Bearer\", config.ApiToken);\n            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(\"application/json\"));\n\n            // Get external IP address\n            string newExternalIP = await GetExternalIPAddressAsync();\n            if (string.IsNullOrEmpty(newExternalIP))\n            {\n                _logger.LogWarning(\"Failed to get external IP address.\");\n                continue;\n            }\n\n            // Check if IP has changed\n            if (lastExternalIP == newExternalIP)\n            {\n                _logger.LogInformation(\"No changes detected in external IP ({IP}).\", lastExternalIP);\n                continue;\n            }\n            lastExternalIP = newExternalIP;\n\n            // Check each domain\n            foreach (string domain in config.Domains)\n            {\n                _logger.LogInformation(\"Checking {Domain} to ensure it matches {IP}.\", domain, newExternalIP);\n\n                string dnsRecordId = await GetDnsRecordIdByNameAsync(client, config.ZoneId, domain);\n                if (string.IsNullOrEmpty(dnsRecordId))\n                {\n                    _logger.LogWarning(\"Failed to get DNS Record ID for domain ({Domain}).\", domain);\n                    continue;\n                }\n\n                string dnsRecordIp = await GetCloudflareDnsRecordIpAsync(client, config.ZoneId, dnsRecordId);\n\n                if (string.IsNullOrEmpty(dnsRecordIp))\n                {\n                    _logger.LogWarning(\"Failed to get IP address from DNS Record ID.\");\n                    continue;\n                }\n\n                if (newExternalIP != dnsRecordIp)\n                {\n                    _logger.LogInformation(\"IP addresses do not match. Updating DNS record...\");\n                    _logger.LogInformation(\"DNS Record IP: {DnsRecordIp}\", dnsRecordIp);\n                    _logger.LogInformation(\"External IP: {ExternalIp}\", newExternalIP);\n\n                    bool updateSuccess = await UpdateCloudflareDnsRecordAsync(client, config.ZoneId, dnsRecordId, newExternalIP, domain);\n\n                    _logger.LogInformation(\"{{Domain}} DNS record update Success: {UpdateSuccess}.\", domain, updateSuccess);\n                }\n                else\n                {\n                    _logger.LogInformation(\"{Domain} IP address matches. No update needed.\", domain);\n                }\n            }\n        }\n    }\n\n    private static Configuration? LoadConfiguration(string filePath)\n    {\n        if (File.Exists(filePath))\n        {\n            try\n            {\n                string jsonConfig = File.ReadAllText(filePath);\n                Configuration config = JsonConvert.DeserializeObject\u003cConfiguration\u003e(jsonConfig);\n\n                // Set default frequency if not specified\n                if (config.FrequencyToCheckInMinutes \u003c= 0)\n                {\n                    config.FrequencyToCheckInMinutes = 120; // Default to 120 minutes\n                }\n\n                return config;\n            }\n            catch (Exception ex)\n            {\n                _logger.LogError(ex, \"Error reading configuration file.\");\n                return null;\n            }\n        }\n        else\n        {\n            _logger.LogError(\"Configuration file not found: {FilePath}\", filePath);\n            return null;\n        }\n    }\n\n    private static bool ValidateConfiguration(Configuration config)\n    {\n        bool isValid = true;\n\n        if (string.IsNullOrEmpty(config.ZoneId))\n        {\n            _logger.LogError(\"ZoneId is missing in configuration.\");\n            isValid = false;\n        }\n\n        if (string.IsNullOrEmpty(config.ApiToken))\n        {\n            _logger.LogError(\"ApiToken is missing in configuration.\");\n            isValid = false;\n        }\n\n        if (config.Domains == null || config.Domains.Count == 0)\n        {\n            _logger.LogError(\"Domains list is missing or empty in configuration.\");\n            isValid = false;\n        }\n\n        return isValid;\n    }\n\n    private static async Task\u003cstring\u003e GetExternalIPAddressAsync()\n    {\n        using HttpClient client = new();\n        try\n        {\n            string ipAddress = (await client.GetStringAsync(\"https://api.ipify.org\")).Trim();\n            bool isValidIP = System.Net.IPAddress.TryParse(ipAddress, out System.Net.IPAddress ip) \u0026\u0026 ip.ToString() == ipAddress;\n            if (!isValidIP)\n            {\n                _logger.LogWarning(\"Invalid IP address received: {IPAddress}\", ipAddress);\n                ipAddress = \"\";\n            }\n\n            return ipAddress;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error getting external IP address.\");\n            return \"\";\n        }\n    }\n\n    private static async Task\u003cstring\u003e GetDnsRecordIdByNameAsync(HttpClient client, string zoneId, string recordName)\n    {\n        string url = $\"https://api.cloudflare.com/client/v4/zones/{zoneId}/dns_records?type=A\u0026name={Uri.EscapeDataString(recordName)}\";\n\n        try\n        {\n            HttpResponseMessage response = await client.GetAsync(url);\n            string content = await response.Content.ReadAsStringAsync();\n\n            if (!response.IsSuccessStatusCode)\n            {\n                _logger.LogError(\"Error fetching DNS Record ID. Status Code: {StatusCode}\", response.StatusCode);\n                _logger.LogError(\"Response Content: {Content}\", content);\n                return \"\";\n            }\n\n            JObject dnsRecords = JObject.Parse(content);\n            JArray resultArray = (JArray)dnsRecords[\"result\"];\n\n            if (resultArray != null \u0026\u0026 resultArray.Count \u003e 0)\n            {\n                JToken dnsRecord = resultArray[0];\n                string dnsRecordId = dnsRecord[\"id\"]?.ToString() ?? \"\";\n                return dnsRecordId;\n            }\n\n            _logger.LogWarning(\"DNS record not found for {RecordName}.\", recordName);\n            return \"\";\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error retrieving DNS Record ID.\");\n            return \"\";\n        }\n    }\n\n    private static async Task\u003cstring\u003e GetCloudflareDnsRecordIpAsync(HttpClient client, string zoneId, string dnsRecordId)\n    {\n        string url = $\"https://api.cloudflare.com/client/v4/zones/{zoneId}/dns_records/{dnsRecordId}\";\n\n        try\n        {\n            HttpResponseMessage response = await client.GetAsync(url);\n            string content = await response.Content.ReadAsStringAsync();\n\n            if (!response.IsSuccessStatusCode)\n            {\n                _logger.LogError(\"Error fetching DNS Record IP. Status Code: {StatusCode}\", response.StatusCode);\n                _logger.LogError(\"Response Content: {Content}\", content);\n                return \"\";\n            }\n\n            // Parse the JSON response to extract the IP address\n            JObject dnsRecord = JObject.Parse(content);\n            string currentIp = dnsRecord[\"result\"]?[\"content\"]?.ToString() ?? \"\";\n\n            return currentIp;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error retrieving DNS Record IP.\");\n            return \"\";\n        }\n    }\n\n    private static async Task\u003cbool\u003e UpdateCloudflareDnsRecordAsync(HttpClient client, string zoneId, string dnsRecordId, string newIp, string domain)\n    {\n        string url = $\"https://api.cloudflare.com/client/v4/zones/{zoneId}/dns_records/{dnsRecordId}\";\n\n        var payload = new\n        {\n            type = \"A\",\n            name = domain,\n            content = newIp,\n            ttl = 3600,\n            proxied = true,\n            comment = \"Dynamic DNS update\"\n        };\n\n        string jsonPayload = JsonConvert.SerializeObject(payload);\n\n        StringContent requestContent = new(jsonPayload, Encoding.UTF8, \"application/json\");\n\n        try\n        {\n            HttpResponseMessage response = await client.PutAsync(url, requestContent);\n            string responseContent = await response.Content.ReadAsStringAsync();\n\n            if (!response.IsSuccessStatusCode)\n            {\n                _logger.LogError(\"Error updating DNS record. Status Code: {StatusCode}\", response.StatusCode);\n                _logger.LogError(\"Response Content: {Content}\", responseContent);\n                return false;\n            }\n\n            // Check if the update was successful\n            JObject result = JObject.Parse(responseContent);\n            return result[\"success\"]?.ToObject\u003cbool\u003e() ?? false;\n        }\n        catch (Exception ex)\n        {\n            _logger.LogError(ex, \"Error updating DNS record.\");\n            return false;\n        }\n    }\n}\n```\n\n## Contributing\n\nContributions are welcome! Please submit a pull request or open an issue to discuss any changes or enhancements.\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE.txt) file for details.\n\n## Acknowledgments\n\n- **ChatGPT**: Initial code generation and assistance.\n- **Ryan Scott White**: Additional custom changes and project setup.\n\n## Disclaimer\n\nThis tool is provided \"as is\" without warranty of any kind. Use it at your own risk.\n\n---\n\n*Note: Remember to replace placeholder values like `yourusername`, `your_zone_id`, `your_api_token`, and domain names with your actual information before using the application.*\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsunsetquest%2Fdynamic-dns-record-updater-for-cloudflare","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsunsetquest%2Fdynamic-dns-record-updater-for-cloudflare","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsunsetquest%2Fdynamic-dns-record-updater-for-cloudflare/lists"}