DDNS for Cloudflare using GO

Updating your dynamic IP with Cloudflare DNS using Golang

DDNS for Cloudflare using GO
Go for Golang

Here is a simple program is designed to update DNS records for multiple domain names on Cloudflare every minute. It can be run as a self-contained daemon, detaching from the terminal to operate in the background, ensuring continuous operation. This will update your public IP address every minute allowing Cloudflare DNS to have your latest dynamic IP issued by your ISP.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "os"
    "os/exec"
    "strings"
    "syscall"
    "time"
)

const (
    apiKey       = "****"                  // Cloudflare API key
    zoneID       = "****"                            // Cloudflare zone ID
    recordNames  = "davidivashenko.com,*.davidivashenko.com" // Comma-separated list of DNS record names
    cloudflareAPI = "https://api.cloudflare.com/client/v4" // Cloudflare API base URL
    updateInterval = 1 * time.Minute                       // Interval for updating DNS records
    ttl          = auto                                     // Time-to-live for DNS records
    proxied      = true                                    // Whether the DNS records are proxied
)

type DNSRecord struct {
    ID      string `json:"id"`
    Type    string `json:"type"`
    Name    string `json:"name"`
    Content string `json:"content"`
    TTL     int    `json:"ttl"`
    Proxied bool   `json:"proxied"`
}

type DNSResponse struct {
    Success bool        `json:"success"`
    Result  []DNSRecord `json:"result"`
}

type UpdateResponse struct {
    Success bool `json:"success"`
}

func getCurrentIP() (string, error) {
    resp, err := http.Get("https://ipinfo.io/ip")
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    ip, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return strings.TrimSpace(string(ip)), nil
}

func getDNSRecord(recordName string) (DNSRecord, error) {
    client := &http.Client{}
    req, err := http.NewRequest("GET", fmt.Sprintf("%s/zones/%s/dns_records?type=A&name=%s", cloudflareAPI, zoneID, recordName), nil)
    if err != nil {
        return DNSRecord{}, err
    }
    req.Header.Add("Authorization", "Bearer "+apiKey)
    req.Header.Add("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return DNSRecord{}, err
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return DNSRecord{}, err
    }

    var dnsResp DNSResponse
    err = json.Unmarshal(body, &dnsResp)
    if err != nil {
        return DNSRecord{}, err
    }

    if len(dnsResp.Result) == 0 {
        return DNSRecord{}, fmt.Errorf("no DNS record found for %s", recordName)
    }

    return dnsResp.Result[0], nil
}

func updateDNSRecord(recordName, recordID, ip string) (bool, error) {
    client := &http.Client{}
    dnsRecord := DNSRecord{
        Type:    "A",
        Name:    recordName,
        Content: ip,
        TTL:     ttl,
        Proxied: proxied,
    }
    jsonData, err := json.Marshal(dnsRecord)
    if err != nil {
        return false, err
    }

    req, err := http.NewRequest("PUT", fmt.Sprintf("%s/zones/%s/dns_records/%s", cloudflareAPI, zoneID, recordID), bytes.NewBuffer(jsonData))
    if err != nil {
        return false, err
    }
    req.Header.Add("Authorization", "Bearer "+apiKey)
    req.Header.Add("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return false, err
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return false, err
    }

    var updateResp UpdateResponse
    err = json.Unmarshal(body, &updateResp)
    if err != nil {
        return false, err
    }

    return updateResp.Success, nil
}

func main() {
    if os.Getppid() != 1 {
        // Detach from the parent process and run in the background
        cmd := exec.Command(os.Args[0], os.Args[1:]...)
        cmd.Stdout = os.Stdout
        cmd.Stderr = os.Stderr
        cmd.SysProcAttr = &syscall.SysProcAttr{
            Setsid: true,
        }
        err := cmd.Start()
        if err != nil {
            fmt.Printf("Failed to start daemon: %v\n", err)
            os.Exit(1)
        }
        fmt.Printf("Daemon started with PID %d\n", cmd.Process.Pid)
        os.Exit(0)
    }

    records := strings.Split(recordNames, ",")

    for {
        ip, err := getCurrentIP()
        if err != nil {
            fmt.Printf("Failed to get current IP: %v\n", err)
            time.Sleep(updateInterval)
            continue
        }

        for _, recordName := range records {
            recordName = strings.TrimSpace(recordName)
            dnsRecord, err := getDNSRecord(recordName)
            if err != nil {
                fmt.Printf("Failed to get DNS record for %s: %v\n", recordName, err)
                continue
            }

            success, err := updateDNSRecord(recordName, dnsRecord.ID, ip)
            if err != nil {
                fmt.Printf("Failed to update DNS record for %s: %v\n", recordName, err)
                continue
            }

            if success {
                fmt.Printf("DNS record for %s updated successfully.\n", recordName)
            } else {
                fmt.Printf("Failed to update DNS record for %s.\n", recordName)
            }
        }

        time.Sleep(updateInterval)
    }
}
  1. Compile the Program: go build -o update_dns update_dns.go
  2. Run the Program: ./update_dns

Ensure Startup on Boot

To ensure the program starts on boot, you can still use systemd on Linux or Task Scheduler on Windows.

For Linux using systemd:

  1. Create a systemd service file /etc/systemd/system/update_dns.service
  2. Enable and start the service:

sudo systemctl enable update_dns.service
sudo systemctl start update_dns.service

This setup ensures the Go program runs as a daemon and updates the specified DNS records every minute.