Skip to content

Instantly share code, notes, and snippets.

@duyanh14
Created January 14, 2024 12:50
Show Gist options
  • Save duyanh14/298ee1629f0304a9c36329851ccfcc65 to your computer and use it in GitHub Desktop.
Save duyanh14/298ee1629f0304a9c36329851ccfcc65 to your computer and use it in GitHub Desktop.
Cloudflare DDNS Updater
package main
import (
"encoding/json"
"fmt"
"github.com/go-resty/resty/v2"
"github.com/robfig/cron"
"io"
"net/http"
"strings"
"time"
)
// CHANGE THESE
var (
authToken = "" // API Token (Recommended)
zoneIdentifier = "" // Can be found in the "Overview" tab of your domain
recordName = "" // Which record you want to be synced
)
type DNSRecord struct {
ID string `json:"id"`
Content string `json:"content"`
}
type CloudflareResponse struct {
Result []DNSRecord `json:"result"`
Success bool `json:"success"`
Errors []string `json:"errors"`
Messages []string `json:"messages"`
}
func main() {
err := runUpdate()
if err != nil {
fmt.Printf("First update error: %v\n", err)
}
c := cron.New()
// Run the task every hour
c.AddFunc("@hourly", func() {
err := runUpdate()
if err != nil {
fmt.Printf("Update error: %v\n", err)
}
})
c.Start()
select {}
}
func runUpdate() error {
fmt.Println("Check Initiated")
ip, err := getExternalIP()
if err != nil {
return fmt.Errorf("Network error, cannot fetch external network IP: %v", err)
}
fmt.Printf(" > Fetched current external network IP: %s\n", ip)
headerAuthParamHeader := make(map[string]string)
if authToken != "" {
headerAuthParamHeader["Authorization"] = "Bearer " + authToken
}
seekCurrentDNSValueURL := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records?name=%s&type=A", zoneIdentifier, recordName)
response, err := executeRequest("GET", seekCurrentDNSValueURL, nil, headerAuthParamHeader)
if err != nil {
return fmt.Errorf("Network error, cannot fetch DNS record: %v", err)
}
var cfResponse CloudflareResponse
if err := json.Unmarshal(response, &cfResponse); err != nil {
return fmt.Errorf("Error decoding JSON response: %v", err)
}
if !cfResponse.Success {
fmt.Println("Error in Cloudflare API response.")
return nil
}
// Ensure there is at least one record
if len(cfResponse.Result) == 0 {
fmt.Println("No DNS records found.")
return nil
}
// We'll process the first record from the response
record := cfResponse.Result[0]
fmt.Printf(" > Fetched current DNS record value : %s\n", record.Content)
if ip == record.Content {
fmt.Printf("Update for A record '%s' cancelled.\n Reason: IP has not changed.\n", record.ID)
return nil
}
fmt.Println(" > Different IP addresses detected, synchronizing...")
jsonDataV4 := fmt.Sprintf(`{"id":"%s","type":"A","proxied":false,"name":"%s","content":"%s","ttl":120}`, zoneIdentifier, recordName, ip)
updateURL := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", zoneIdentifier, record.ID)
_, err = executeRequest("PUT", updateURL, []byte(jsonDataV4), headerAuthParamHeader)
if err != nil {
return fmt.Errorf("Update error: %v", err)
}
fmt.Printf("Update for A record '%s' succeeded.\n - Old value: %s\n + New value: %s\n", record.ID, record.Content, ip)
return nil
}
func getExternalIP() (string, error) {
resp, err := http.Get("https://icanhazip.com/")
if err != nil {
return "", err
}
defer resp.Body.Close()
ipBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
ip := strings.TrimSpace(string(ipBytes))
return ip, nil
}
func executeRequest(method, url string, data []byte, headers map[string]string) ([]byte, error) {
client := resty.New()
client.SetTimeout(10 * time.Second)
client.SetHeaders(headers)
response, err := client.R().
SetBody(data).
Execute(method, url)
if err != nil {
return nil, err
}
return response.Body(), nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment