From 61418c565f69bcedb7ead27c97eed88ff56ab4a4 Mon Sep 17 00:00:00 2001 From: amantahiliani Date: Fri, 3 Oct 2025 01:01:07 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 4 + README.md | 191 +++++++++++++++++++++++++++++ cloudflare_updater_util_example.sh | 13 ++ config.go | 26 ++++ dns.go | 184 +++++++++++++++++++++++++++ go.mod | 3 + main.go | 34 +++++ 7 files changed, 455 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cloudflare_updater_util_example.sh create mode 100644 config.go create mode 100644 dns.go create mode 100644 go.mod create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a61040 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +main +.env +cloudflare-dns-updater +cloudflare_updater_util.sh \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ae34d6c --- /dev/null +++ b/README.md @@ -0,0 +1,191 @@ +# Cloudflare DNS Updater + +A simple Go application that automatically updates Cloudflare DNS records with your current public IP address. Perfect for dynamic IP scenarios where you need to keep your domain pointing to your current IP. + +## Features + +- 🔄 Automatically detects your current public IP address +- 📝 Updates existing DNS A records or creates new ones +- ⚡ Supports multiple DNS records in a single run +- 🛡️ Uses Cloudflare API with secure authentication +- 🔧 Configurable via environment variables +- 🚀 Lightweight and fast + +## Prerequisites + +- Go 1.24.0 or later +- A Cloudflare account with API access +- A domain managed by Cloudflare + +## Installation + +1. Clone or download this repository +2. Build the application: + ```bash + go build -o cloudflare-dns-updater main.go config.go dns.go + ``` + +## Configuration + +### Environment Variables + +The application uses the following environment variables: + +| Variable | Description | Required | +|----------|-------------|----------| +| `CLOUDFLARE_EMAIL` | Your Cloudflare account email | Yes | +| `CLOUDFLARE_AUTH_KEY` | Your Cloudflare API token | Yes | +| `CLOUDFLARE_ZONE_ID` | The Zone ID of your domain | Yes | +| `CLOUDFLARE_RECORD_NAMES` | Comma-separated list of DNS record names to update | Yes | +| `CLOUDFLARE_FORCE_IP` | Force a specific IP address (optional) | No | + +### Getting Your Cloudflare Credentials + +1. **Email**: Your Cloudflare account email address +2. **Auth Key**: + - Go to [Cloudflare Dashboard](https://dash.cloudflare.com/profile/api-tokens) + - Click "Create Token" + - Use "Custom token" template + - Permissions: `Zone:Zone:Read`, `Zone:DNS:Edit` + - Zone Resources: Include specific zone or all zones +3. **Zone ID**: + - Go to your domain's overview page in Cloudflare + - Find "Zone ID" in the right sidebar +4. **Record Names**: + - List of subdomains you want to update (e.g., `subdomain.example.com,www.example.com`) + +## Quick Start + +1. Copy the example configuration file: + ```bash + cp cloudflare_updater_util_example.sh cloudflare_updater_util.sh + ``` + +2. Edit `cloudflare_updater_util.sh` with your actual credentials: + ```bash + nano cloudflare_updater_util.sh + ``` + +3. Make the script executable: + ```bash + chmod +x cloudflare_updater_util.sh + ``` + +4. Run the updater: + ```bash + ./cloudflare_updater_util.sh + ``` + +## Automated Updates with Cron + +To keep your DNS records updated automatically, set up a cron job: + +1. Open your crontab: + ```bash + crontab -e + ``` + +2. Add a line to run the updater every 5 minutes: + ```bash + */5 * * * * /path/to/your/cloudflare_updater_util.sh + ``` + + Or every 10 minutes: + ```bash + */10 * * * * /path/to/your/cloudflare_updater_util.sh + ``` + +3. Save and exit. The cron job will now run automatically. + +### Example Cron Schedules + +- Every 5 minutes: `*/5 * * * *` +- Every 10 minutes: `*/10 * * * *` +- Every hour: `0 * * * *` +- Every 6 hours: `0 */6 * * *` + +## Usage Examples + +### Basic Usage +```bash +export CLOUDFLARE_EMAIL="your-email@example.com" +export CLOUDFLARE_AUTH_KEY="your-api-token" +export CLOUDFLARE_ZONE_ID="your-zone-id" +export CLOUDFLARE_RECORD_NAMES="home.example.com,server.example.com" +./cloudflare-dns-updater +``` + +### With Forced IP +```bash +export CLOUDFLARE_EMAIL="your-email@example.com" +export CLOUDFLARE_AUTH_KEY="your-api-token" +export CLOUDFLARE_ZONE_ID="your-zone-id" +export CLOUDFLARE_RECORD_NAMES="home.example.com" +export CLOUDFLARE_FORCE_IP="192.168.1.100" +./cloudflare-dns-updater +``` + +## How It Works + +1. **IP Detection**: The application queries `api.ipify.org` to get your current public IP address +2. **Record Check**: For each specified DNS record, it checks if an A record already exists +3. **Update or Create**: + - If the record exists and the IP is different, it updates the record + - If the record doesn't exist, it creates a new A record + - If the IP is the same, no action is taken + +## Logging + +The application provides detailed logging: +- Configuration details (without sensitive information) +- Cloudflare API URLs being called +- Record existence status +- Update/create operations +- Error messages + +## Error Handling + +The application handles various error scenarios: +- Network connectivity issues +- Invalid API credentials +- Cloudflare API errors +- JSON parsing errors +- Missing environment variables + +## Security Notes + +- Never commit your `cloudflare_updater_util.sh` file with real credentials +- Use Cloudflare API tokens instead of global API keys when possible +- Limit API token permissions to only what's necessary +- Consider using environment variables in production instead of hardcoded values + +## Troubleshooting + +### Common Issues + +1. **"Failed to load config"**: Check that all required environment variables are set +2. **"Failed to check if record exists"**: Verify your API credentials and Zone ID +3. **"Failed to update/create DNS"**: Check API token permissions and record names + +### Debug Mode + +Add logging to see detailed API responses: +```bash +export CLOUDFLARE_EMAIL="your-email@example.com" +export CLOUDFLARE_AUTH_KEY="your-api-token" +export CLOUDFLARE_ZONE_ID="your-zone-id" +export CLOUDFLARE_RECORD_NAMES="home.example.com" +go run main.go config.go dns.go +``` + +## License + +This project is open source. Feel free to modify and distribute as needed. + +## Contributing + +Contributions are welcome! Please feel free to submit issues and pull requests. + +## Support + +If you encounter any issues, please check the troubleshooting section above or create an issue in the repository. diff --git a/cloudflare_updater_util_example.sh b/cloudflare_updater_util_example.sh new file mode 100644 index 0000000..9e73176 --- /dev/null +++ b/cloudflare_updater_util_example.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +export CLOUDFLARE_EMAIL="placeholder@gmail.com" +export CLOUDFLARE_AUTH_KEY="placeholder" +export CLOUDFLARE_ZONE_ID="placeholder" +export CLOUDFLARE_RECORD_NAMES="placeholder.com" +export CLOUDFLARE_FORCE_IP="placeholder" + +# Build the binary +go build -o cloudflare-dns-updater main.go config.go dns.go + +# Run the binary +./cloudflare-dns-updater \ No newline at end of file diff --git a/config.go b/config.go new file mode 100644 index 0000000..1e0ad00 --- /dev/null +++ b/config.go @@ -0,0 +1,26 @@ +package main + +import ( + "os" + "strings" +) + +// Email, Auth Key, Zone ID, Record Names + +type Config struct { + Email string + AuthKey string + ZoneID string + RecordNames []string + ForceIP string +} + +func LoadConfig() (*Config, error) { + return &Config{ + Email: os.Getenv("CLOUDFLARE_EMAIL"), + AuthKey: os.Getenv("CLOUDFLARE_AUTH_KEY"), + ZoneID: os.Getenv("CLOUDFLARE_ZONE_ID"), + RecordNames: strings.Split(os.Getenv("CLOUDFLARE_RECORD_NAMES"), ","), + ForceIP: os.Getenv("CLOUDFLARE_FORCE_IP"), + }, nil +} diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..0493fd8 --- /dev/null +++ b/dns.go @@ -0,0 +1,184 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +type IpResponse struct { + Ip string `json:"ip"` +} + +type DnsRecord struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + Ttl int `json:"ttl"` + Proxied bool `json:"proxied"` +} + +type DnsRecords struct { + Result []DnsRecord `json:"result"` +} + +func getIp() (string, error) { + // Get IP from ipify and return it as a IpResponse + + ipUrl := "https://api.ipify.org?format=json" + resp, err := http.Get(ipUrl) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var ipResponse IpResponse + if err := json.Unmarshal(body, &ipResponse); err != nil { + return "", err + } + return ipResponse.Ip, nil + +} + +type DnsUpdater struct { + config *Config +} + +func (d *DnsUpdater) CheckIfRecordsExist(recordName string) (DnsRecord, error) { + cloudFlareUrl := "https://api.cloudflare.com/client/v4/zones/" + d.config.ZoneID + "/dns_records?name=" + recordName + "&type=A" + log.Printf("CloudFlare URL: %s", cloudFlareUrl) + + req, err := http.NewRequest("GET", cloudFlareUrl, nil) + if err != nil { + return DnsRecord{}, err + } + req.Header.Set("X-Auth-Email", d.config.Email) + req.Header.Set("Authorization", "Bearer "+d.config.AuthKey) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return DnsRecord{}, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return DnsRecord{}, err + } + + log.Printf("Body: %s", string(body)) + + var dnsRecords DnsRecords + if err := json.Unmarshal(body, &dnsRecords); err != nil { + return DnsRecord{}, err + } + log.Printf("DNS Records: %+v", dnsRecords) + + // Check if the record exists + for _, record := range dnsRecords.Result { + if record.Name == recordName && record.Type == "A" { + return record, nil + } + } + return DnsRecord{}, nil +} + +func (d *DnsUpdater) Update(record DnsRecord) error { + ip, err := getIp() + if err != nil { + return err + } + if d.config.ForceIP != "" { + ip = d.config.ForceIP + } + if record.Content == ip { + return nil + } + + record.Content = ip + record.Ttl = 3600 + + cloudFlareUrl := "https://api.cloudflare.com/client/v4/zones/" + d.config.ZoneID + "/dns_records/" + record.Id + + body, err := json.Marshal(record) + if err != nil { + return err + } + + req, err := http.NewRequest("PATCH", cloudFlareUrl, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("X-Auth-Email", d.config.Email) + req.Header.Set("Authorization", "Bearer "+d.config.AuthKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("failed to update DNS: %s", string(body)) + } + log.Printf("Updated DNS: %s", string(body)) + return nil +} + +func (d *DnsUpdater) Create(recordName string) error { + ip, err := getIp() + if err != nil { + return err + } + if d.config.ForceIP != "" { + ip = d.config.ForceIP + } + record := DnsRecord{ + Name: recordName, + Type: "A", + Content: ip, + Ttl: 3600, + Proxied: false, + } + + cloudFlareUrl := "https://api.cloudflare.com/client/v4/zones/" + d.config.ZoneID + "/dns_records" + + body, err := json.Marshal(record) + if err != nil { + return err + } + + req, err := http.NewRequest("POST", cloudFlareUrl, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Set("X-Auth-Email", d.config.Email) + req.Header.Set("Authorization", "Bearer "+d.config.AuthKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err = io.ReadAll(resp.Body) + if err != nil { + return err + } + if resp.StatusCode != 200 { + return fmt.Errorf("failed to create DNS: %s", string(body)) + } + log.Printf("Created DNS: %s", string(body)) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbd532d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module cloudflare-dns-updater + +go 1.24.0 diff --git a/main.go b/main.go new file mode 100644 index 0000000..d91148a --- /dev/null +++ b/main.go @@ -0,0 +1,34 @@ +package main + +import "log" + +func main() { + config, err := LoadConfig() + if err != nil { + log.Fatalf("Failed to load config: %v", err) + } + log.Printf("Config: %+v", config) + dnsUpdater := DnsUpdater{config: config} + for _, recordName := range config.RecordNames { + record, err := dnsUpdater.CheckIfRecordsExist(recordName) + if err != nil { + log.Fatalf("Failed to check if record exists: %v", err) + } + if record.Id != "" { + log.Printf("Record %s exists", recordName) + err = dnsUpdater.Update(record) + if err != nil { + log.Fatalf("Failed to update DNS: %v", err) + } + } else { + log.Printf("Record %s does not exist. Creating it.....", recordName) + err = dnsUpdater.Create(recordName) + if err != nil { + log.Fatalf("Failed to create DNS: %v", err) + } + } + } + if err != nil { + log.Fatalf("Failed to update or create DNS: %v", err) + } +}