Skip to content

Commit

Permalink
Support multiple domains
Browse files Browse the repository at this point in the history
Closes #18
  • Loading branch information
Zebradil committed Mar 16, 2022
1 parent 0fdb929 commit d58c7b0
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 42 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ It is provided with systemd service and timer files for automation.
```
Updates AAAA records at Cloudflare according to the current IPv6 address.
Requires a network interface name for a IPv6 address lookup, domain name
Requires a network interface name for a IPv6 address lookup, domain name[s]
and Cloudflare API token with edit access rights to corresponding DNS zone.
Usage:
cloudflare-dynamic-dns [flags]
Flags:
--config string config file (default is $HOME/.cloudflare-dynamic-dns.yaml)
--domain string Domain name to assign the IPv6 address to
--domains strings Domain names to assign the IPv6 address to
-h, --help help for cloudflare-dynamic-dns
--iface string Network interface to look up for a IPv6 address
--log-level string Sets logging level: trace, debug, info, warning, error, fatal, panic (default "info")
Expand Down Expand Up @@ -66,15 +66,17 @@ sudo install -m700 -d /etc/cloudflare-dynamic-dns/config.d
### Run manually

0. Follow steps from [Installation](#instllation) section
1. Run `./cloudflare-dynamic-dns --domain example.com --iface eth0 --token cloudflare-api-token`
1. Run `./cloudflare-dynamic-dns --domains 'example.com,*.example.com' --iface eth0 --token cloudflare-api-token`
- NOTE: instead of compiling `cloudflare-dynamic-dns` binary, it can be replaced with `go run main.go` in the command above.

Instead of specifying command line arguments, it is possible to create `~/.cloudflare-dynamic-dns.yaml` with the following structure:

```yaml
iface: eth0
token: cloudflare-api-token
domain: example.com
domains:
- example.com
- "*.example.com"
```
And then run `./cloudflare-dynamic-dns` (or `go run main.go`) without arguments.
Expand All @@ -96,7 +98,9 @@ Make sure that required systemd files are installed (see [Installation](#instlla
sudo tee -a /etc/cloudflare-dynamic-dns/config.d/example.com.yaml <<EOF
iface: eth0
token: cloudflare-api-token
domain: example.com
domains:
- example.com
- "*.example.com"
EOF

# 3. Enable systemd timer
Expand All @@ -107,4 +111,7 @@ This way (via running multiple timers) you can use multiple configurations at th

By default a timer is triggered one minute after boot and then every 5 minutes. It is not configurable currently.

To avoid unnecessary requests to Cloudflare API state files are used. They're created in `/var/lib/cloudflare-dynamic-dns/` and named after `domain` configuration variable in corresponding config files. A state file contains IPv6 address which was set in a Cloudflare DNS AAAA record during the last successful run. If the current IPv6 address is the same as in the state file, no additional API requests are done.
To avoid unnecessary requests to Cloudflare API state files are used.
They're created in `/var/lib/cloudflare-dynamic-dns/` and named using configuration variable in corresponding config files (`iface` and md5 hash of `domains`).
A state file contains IPv6 address which was set in a Cloudflare DNS AAAA record during the last successful run.
If the current IPv6 address is the same as in the one in the state file, no additional API requests are done.
85 changes: 49 additions & 36 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package cmd

import (
"context"
"crypto/md5"
"fmt"
"net"
"os"
Expand All @@ -44,7 +45,7 @@ var rootCmd = &cobra.Command{
Short: "Updates AAAA records at Cloudflare according to the current IPv6 address",
Long: `Updates AAAA records at Cloudflare according to the current IPv6 address.
Requires a network interface name for a IPv6 address lookup, domain name
Requires a network interface name for a IPv6 address lookup, domain name[s]
and Cloudflare API token with edit access rights to corresponding DNS zone.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
level, err := log.ParseLevel(viper.GetString("log-level"))
Expand All @@ -65,7 +66,7 @@ and Cloudflare API token with edit access rights to corresponding DNS zone.`,
}

var (
domain = viper.GetString("domain")
domains = viper.GetStringSlice("domains")
iface = viper.GetString("iface")
systemd = viper.GetBool("systemd")
token = viper.GetString("token")
Expand All @@ -82,23 +83,27 @@ and Cloudflare API token with edit access rights to corresponding DNS zone.`,
}

if systemd {
stateFilepath = filepath.Join(os.Getenv("STATE_DIRECTORY"), domain)
stateFilepath = filepath.Join(os.Getenv("STATE_DIRECTORY"), fmt.Sprintf("%s_%x", iface, md5.Sum([]byte(strings.Join(domains, "_")))))
}

log.WithFields(log.Fields{
"domain": domain,
"domains": domains,
"iface": iface,
"stateFilepath": stateFilepath,
"systemd": systemd,
"token": fmt.Sprintf("[%d characters]", len(token)),
"ttl": ttl,
}).Info("Configuration")

if len(domains) == 0 {
log.Fatal("No domains specified")
}

addr := getIpv6Address(iface)

if systemd && addr == getOldIpv6Address(stateFilepath) {
log.Info("The address hasn't changed, nothing to do")
log.Info(fmt.Sprintf("To bypass this check run without --systemd flag or remove the state file: %s", stateFilepath))
log.Info("To bypass this check run without --systemd flag or remove the state file: ", stateFilepath)
return
}

Expand All @@ -107,44 +112,52 @@ and Cloudflare API token with edit access rights to corresponding DNS zone.`,
log.WithError(err).Fatal("Couldn't create API client")
}

ctx := context.Background()

zoneID, err := api.ZoneIDByName(getZoneFromDomain(domain))
if err != nil {
log.WithError(err).Fatal("Couldn't get ZoneID")
for _, domain := range domains {
log.Info("Processing domain: ", domain)
processDomain(api, domain, addr, ttl)
}

dnsRecordFilter := cloudflare.DNSRecord{Type: "AAAA", Name: domain}
existingDNSRecords, err := api.DNSRecords(ctx, zoneID, dnsRecordFilter)
if err != nil {
log.WithError(err).WithField("filter", dnsRecordFilter).Fatal("Couldn't get DNS records")
if systemd {
setOldIpv6Address(stateFilepath, addr)
}
log.WithField("records", existingDNSRecords).Debug("Found DNS records")
},
}

desiredDNSRecord := cloudflare.DNSRecord{Type: "AAAA", Name: domain, Content: addr, TTL: ttl}
func processDomain(api *cloudflare.API, domain string, addr string, ttl int) {
ctx := context.Background()

if len(existingDNSRecords) == 0 {
createNewDNSRecord(api, zoneID, desiredDNSRecord)
} else if len(existingDNSRecords) == 1 {
updateDNSRecord(api, zoneID, existingDNSRecords[0], desiredDNSRecord)
} else {
updated := false
for oldRecord := range existingDNSRecords {
if !updated && existingDNSRecords[oldRecord].Content == desiredDNSRecord.Content {
updateDNSRecord(api, zoneID, existingDNSRecords[oldRecord], desiredDNSRecord)
updated = true
} else {
deleteDNSRecord(api, zoneID, existingDNSRecords[oldRecord])
}
}
if !updated {
createNewDNSRecord(api, zoneID, desiredDNSRecord)
zoneID, err := api.ZoneIDByName(getZoneFromDomain(domain))
if err != nil {
log.WithError(err).Fatal("Couldn't get ZoneID")
}

dnsRecordFilter := cloudflare.DNSRecord{Type: "AAAA", Name: domain}
existingDNSRecords, err := api.DNSRecords(ctx, zoneID, dnsRecordFilter)
if err != nil {
log.WithError(err).WithField("filter", dnsRecordFilter).Fatal("Couldn't get DNS records")
}
log.WithField("records", existingDNSRecords).Debug("Found DNS records")

desiredDNSRecord := cloudflare.DNSRecord{Type: "AAAA", Name: domain, Content: addr, TTL: ttl}

if len(existingDNSRecords) == 0 {
createNewDNSRecord(api, zoneID, desiredDNSRecord)
} else if len(existingDNSRecords) == 1 {
updateDNSRecord(api, zoneID, existingDNSRecords[0], desiredDNSRecord)
} else {
updated := false
for oldRecord := range existingDNSRecords {
if !updated && existingDNSRecords[oldRecord].Content == desiredDNSRecord.Content {
updateDNSRecord(api, zoneID, existingDNSRecords[oldRecord], desiredDNSRecord)
updated = true
} else {
deleteDNSRecord(api, zoneID, existingDNSRecords[oldRecord])
}
}
if systemd {
setOldIpv6Address(stateFilepath, addr)
if !updated {
createNewDNSRecord(api, zoneID, desiredDNSRecord)
}
},
}
}

func createNewDNSRecord(api *cloudflare.API, zoneID string, desiredDNSRecord cloudflare.DNSRecord) {
Expand Down Expand Up @@ -200,7 +213,7 @@ func init() {
rootCmd.Flags().Bool("systemd", false, `Switch operation mode for running in systemd
In this mode previously used ipv6 address is preserved between runs to avoid unnecessary calls to CloudFlare API`)
rootCmd.Flags().Int("ttl", 1, "Time to live, in seconds, of the DNS record. Must be between 60 and 86400, or 1 for 'automatic'")
rootCmd.Flags().String("domain", "", "Domain name to assign the IPv6 address to")
rootCmd.Flags().StringSlice("domains", []string{}, "Domain names to assign the IPv6 address to")
rootCmd.Flags().String("iface", "", "Network interface to look up for a IPv6 address")
rootCmd.Flags().String("log-level", "info", "Sets logging level: trace, debug, info, warning, error, fatal, panic")
rootCmd.Flags().String("token", "", "Cloudflare API token with DNS edit access rights")
Expand Down

0 comments on commit d58c7b0

Please sign in to comment.