From 041edc440a7fed8b04e45d7d320d44394750fa23 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Tue, 17 Nov 2020 08:54:08 +0100 Subject: [PATCH 1/5] cleanup and start initial support for ipv6 --- cmd/cf-ddns-agent/main.go | 2 +- pkg/util/main.go | 34 ++++++++++++++++++++++------------ 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/cmd/cf-ddns-agent/main.go b/cmd/cf-ddns-agent/main.go index 0629e21..7b6b87c 100644 --- a/cmd/cf-ddns-agent/main.go +++ b/cmd/cf-ddns-agent/main.go @@ -111,7 +111,7 @@ func PerformUpdate() (err error) { if err != nil { log.Errorf("An error was encountered during IP discovery. Check previous log entries for more details.") } else { - err = util.PerformRecordUpdate(Options.CfAPIToken, Options.Domain, Options.Host, MyIP) + err = util.PerformRecordUpdate(Options.CfAPIToken, Options.Domain, Options.Host, MyIP, nil) if err != nil { log.Error("An error was encountered during updating of the DNS record. Check previous log entries for more details.") } diff --git a/pkg/util/main.go b/pkg/util/main.go index e82ab8f..748d71e 100644 --- a/pkg/util/main.go +++ b/pkg/util/main.go @@ -17,14 +17,24 @@ package util import ( + "net" + "github.com/cloudflare/cloudflare-go" log "github.com/sirupsen/logrus" - - "net" ) -func PerformRecordUpdate(token string, domain string, host string, value net.IP) (err error) { +func PerformRecordUpdate(token string, domain string, host string, ipv4 net.IP, ipv6 net.IP) (err error) { + if ipv4 != nil { + err = UpdateCFRecord(token, domain, host, "A", ipv4) + } + if ipv6 != nil { + err = UpdateCFRecord(token, domain, host, "AAAA", ipv6) + } + return +} + +func UpdateCFRecord(token string, domain string, host string, recordType string, ip net.IP) (err error) { // start with creating a CF api object with the token. api, err := cloudflare.NewWithAPIToken(token) if err != nil { @@ -39,29 +49,29 @@ func PerformRecordUpdate(token string, domain string, host string, value net.IP) return } - foo := cloudflare.DNSRecord{ + cfRecord := cloudflare.DNSRecord{ Name: host, - Type: "A", + Type: recordType, } - records, err := api.DNSRecords(id, foo) + records, err := api.DNSRecords(id, cfRecord) if err != nil { - log.Errorf("Error encountered while checking current value of %s: %s", host, err.Error()) + log.Errorf("Error encountered while checking current IP of %s: %s", host, err.Error()) + return } for _, record := range records { CurrentIP := net.ParseIP(record.Content) - if CurrentIP.Equal(value) { + if CurrentIP.Equal(ip) { log.Infof("IP address up to date for record %s (type %s). No DNS change necessary.", record.Name, record.Type) } else { - log.Infof("Updating IP address of record %s (type %s) to %s", record.Name, record.Type, value) - record.Content = value.String() + log.Infof("Updating IP address of record %s (type %s) to %s", record.Name, record.Type, ip) + record.Content = ip.String() err = api.UpdateDNSRecord(id, record.ID, record) if err != nil { - log.Errorf("Error updating DNS record for %s (type %s) to %s: %s", record.Name, record.Type, value, err.Error()) + log.Errorf("Error updating DNS record for %s (type %s) to %s: %s", record.Name, record.Type, ip, err.Error()) } else { log.Infof("IP address of record record %s (type %s) successfully updated.", record.Name, record.Type) } } } - return } From c889555ee9f024d00202b15b109dfa1577428c62 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Sat, 26 Jun 2021 15:21:35 +0200 Subject: [PATCH 2/5] add ipv6 and some other stuf --- Makefile | 4 +-- README.md | 22 +++++++++++--- cmd/cf-ddns-agent/main.go | 24 ++++++++++++--- go.mod | 11 +++++-- go.sum | 28 +++++++++++++++++ pkg/config/main.go | 13 ++++++-- pkg/discovery/main.go | 64 ++++++++++++++++++++++++++++++++++++--- pkg/util/main.go | 60 ++++++++++++++++++++++++------------ 8 files changed, 187 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 05a7d61..729d327 100644 --- a/Makefile +++ b/Makefile @@ -14,9 +14,9 @@ LD_FLAGS="-s -w -X main.Version=$(VERSION)" # Targets we want to build PLATFORMS=linux/386 linux/amd64 linux/arm linux/arm64 linux/mips linux/mips64 linux/ppc64 linux/riscv64 \ freebsd/386 freebsd/amd64 freebsd/arm freebsd/arm64 \ -openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 \ +openbsd/386 openbsd/amd64 openbsd/arm openbsd/arm64 openbsd/mips64 \ netbsd/386 netbsd/amd64 netbsd/arm netbsd/arm64 \ -darwin/amd64 windows/amd64 \ +darwin/amd64 windows/amd64 darwin/arm64 \ solaris/amd64 # Used for help output diff --git a/README.md b/README.md index a6f7c5a..e3c6927 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,22 @@ To be clear: I do NOT include malware in the executable!** ./cf-ddns-agent -cf-api-token string Specify the CloudFlare API token. Using this parameter is discouraged, and the token should be specified in CF_API_TOKEN environment variable. + -create + Create record with 'auto' TTL if it doesn't exist yet, or generate error otherwise. (default "true") (default true) -daemon Run continuously in background and perform update every minutes. (see 'update-interval') -discovery-url string Specify an alternative IPv4 discovery service. (default "https://api.ipify.org") + -discovery-url-v6 string + Specify an alternative IPv6 discovery service. (default "https://api64.ipify.org") -domain string - Specify the CloudFlare domain. (example: 'mydomain.org') - REQUIRED + Specify the CloudFlare domain. (example: 'mydomain.org') - REQUIRED. + -dry-run + Run in "dry-run" mode, don't actually update the record. (default "false") -host string - Specify the full A record name that needs to be updated. (example: 'myhost.mydomain.org') - REQUIRED + Specify the full A record name that needs to be updated. (example: 'myhost.mydomain.org') - REQUIRED. + -ipv6 + Enable ipv6 support and CAA record updates, check README. (default "false") -update-interval int Specify interval (in minutes) for updating the DNS record when running as a daemon. (see 'daemon'). A minimum of 5 is enforced. (default 15) ```` @@ -50,10 +58,15 @@ If setting the `CF_API_TOKEN` is not possible for some reason, it is possible to - In case of a 4xx http error, no retry occurs as this probably means a configuration issue (invalid discovery url supplied). +## Ipv6 support + +This software now includes experimental ipv6 support. you can enable it with the `-ipv6=true` parameter. This will update the AAAA-record for the specified host. +Make sure an AAAA-record is already created for the host. + ## Roadmap -- IPv6 and AAAA record support -- Multiple IPv4 discovery providers +- Improve IPv6 and AAAA record support +- Multiple IP discovery providers ## License and copyright @@ -81,3 +94,4 @@ The following third-party software is also directly included: - sirupsen/logrus (c) Simon Eskildsen, MIT license. See: https://github.com/sirupsen/logrus - cloudflare/cloudflare-go (c) CloudFlare, BSD license. See: https://github.com/cloudflare/cloudflare-go +- asaskevich/govalidator (c) Alex Saskevich, MIT license. See: https://github.com/asaskevich/govalidator diff --git a/cmd/cf-ddns-agent/main.go b/cmd/cf-ddns-agent/main.go index 7b6b87c..70cd14a 100644 --- a/cmd/cf-ddns-agent/main.go +++ b/cmd/cf-ddns-agent/main.go @@ -53,7 +53,10 @@ func main() { } log.Infof("%s (version %s) is starting...\n", AppName, Version) - log.Infof("IP Discovery service url is set to: %s", Options.DiscoveryURL) + log.Infof("IPv4 Discovery service url is set to: %s", Options.DiscoveryURL) + if Options.Ipv6Enabled { + log.Infof("IPv6 Discovery service url is set to: %s", Options.DiscoveryURLv6) + } if Options.Domain == "" || Options.Host == "" { log.Fatal("Both --domain and --host must be set!") @@ -107,13 +110,24 @@ func Exit(err error) { func PerformUpdate() (err error) { // get ip and update - MyIP, err := discovery.DiscoverIPv4(Options.DiscoveryURL) + MyIPv4, err := discovery.DiscoverIPv4(Options.DiscoveryURL) if err != nil { - log.Errorf("An error was encountered during IP discovery. Check previous log entries for more details.") + log.Errorf("An error was encountered during IPv4 discovery. Check previous log entries for more details.") } else { - err = util.PerformRecordUpdate(Options.CfAPIToken, Options.Domain, Options.Host, MyIP, nil) + err = util.UpdateCFRecord(Options.CfAPIToken, Options.Domain, Options.Host, "A", MyIPv4, Options.DryRun, Options.CreateMode) if err != nil { - log.Error("An error was encountered during updating of the DNS record. Check previous log entries for more details.") + log.Error("An error was encountered during updating of the DNS A-record. Check previous log entries for more details.") + } + } + if Options.Ipv6Enabled { + MyIPv6, err := discovery.DiscoverIPv6(Options.DiscoveryURLv6) + if err != nil { + log.Errorf("An error was encountered during IPv4 discovery. Check previous log entries for more details.") + } else { + err = util.UpdateCFRecord(Options.CfAPIToken, Options.Domain, Options.Host, "AAAA", MyIPv6, Options.DryRun, Options.CreateMode) + if err != nil { + log.Error("An error was encountered during updating of the DNS AAAA-record. Check previous log entries for more details.") + } } } return diff --git a/go.mod b/go.mod index bca8a9f..fb1e674 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module github.com/headincloud/cf-ddns-agent -go 1.13 +go 1.16 require ( - github.com/cloudflare/cloudflare-go v0.13.4 - github.com/sirupsen/logrus v1.7.0 + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect + github.com/cloudflare/cloudflare-go v0.18.0 + github.com/sirupsen/logrus v1.8.1 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect + golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect + golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect ) diff --git a/go.sum b/go.sum index bf8a0b1..c1199a1 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,21 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/cloudflare/cloudflare-go v0.13.4 h1:3Dm3p31K/BxKZhy+Ll2Pf7yStprJtgBKi52ee0NPcAU= github.com/cloudflare/cloudflare-go v0.13.4/go.mod h1:jGTn0jEGfm8MVoTjBdbVDPHDkLmHdvcVIbYWTklYTvs= +github.com/cloudflare/cloudflare-go v0.18.0 h1:9q1yV4XuYqAZKsMUygRFH1rmmDq5rpaVXL+WWfeliao= +github.com/cloudflare/cloudflare-go v0.18.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -15,29 +24,48 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= +golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/config/main.go b/pkg/config/main.go index e6a4890..95cfd37 100644 --- a/pkg/config/main.go +++ b/pkg/config/main.go @@ -20,6 +20,10 @@ import "flag" type ProgramOptions struct { DiscoveryURL string + DiscoveryURLv6 string + Ipv6Enabled bool + DryRun bool + CreateMode bool Domain string Host string CfAPIToken string @@ -28,13 +32,18 @@ type ProgramOptions struct { } var DefaultDiscoveryURL = "https://api.ipify.org" +var DefaultDiscoveryURLv6 = "https://api64.ipify.org" func InitConfig(Options *ProgramOptions) { flag.StringVar(&Options.DiscoveryURL, "discovery-url", DefaultDiscoveryURL, "Specify an alternative IPv4 discovery service.") - flag.StringVar(&Options.Domain, "domain", "", "Specify the CloudFlare domain. (example: 'mydomain.org') - REQUIRED") - flag.StringVar(&Options.Host, "host", "", "Specify the full A record name that needs to be updated. (example: 'myhost.mydomain.org') - REQUIRED") + flag.StringVar(&Options.DiscoveryURLv6, "discovery-url-v6", DefaultDiscoveryURLv6, "Specify an alternative IPv6 discovery service.") + flag.BoolVar(&Options.Ipv6Enabled, "ipv6", false, "Enable ipv6 support and CAA record updates, check README. (default \"false\")") + flag.StringVar(&Options.Domain, "domain", "", "Specify the CloudFlare domain. (example: 'mydomain.org') - REQUIRED.") + flag.StringVar(&Options.Host, "host", "", "Specify the full A record name that needs to be updated. (example: 'myhost.mydomain.org') - REQUIRED.") flag.StringVar(&Options.CfAPIToken, "cf-api-token", "", "Specify the CloudFlare API token. Using this parameter is discouraged, and the token should be specified in CF_API_TOKEN environment variable.") flag.BoolVar(&Options.Daemon, "daemon", false, "Run continuously in background and perform update every minutes. (see 'update-interval')") flag.IntVar(&Options.UpdateInterval, "update-interval", 15, "Specify interval (in minutes) for updating the DNS record when running as a daemon. (see 'daemon'). A minimum of 5 is enforced.") + flag.BoolVar(&Options.DryRun, "dry-run", false, "Run in \"dry-run\" mode, don't actually update the record. (default \"false\")") + flag.BoolVar(&Options.CreateMode, "create", true, "Create record with 'auto' TTL if it doesn't exist yet, or generate error otherwise. (default \"true\")") flag.Parse() } diff --git a/pkg/discovery/main.go b/pkg/discovery/main.go index 1350700..14637cd 100644 --- a/pkg/discovery/main.go +++ b/pkg/discovery/main.go @@ -23,6 +23,7 @@ import ( "net/http" "time" + valid "github.com/asaskevich/govalidator" log "github.com/sirupsen/logrus" ) @@ -67,13 +68,68 @@ func DiscoverIPv4(DiscoveryURL string) (ip net.IP, err error) { log.Errorf("could not read response from IP discovery service: %s", err.Error()) return } - ip = net.ParseIP(string(body)) - if ip == nil { - err = fmt.Errorf("could not parse received value as an IP address") + if valid.IsIPv4(string(body)) { + ip = net.ParseIP(string(body)) + if ip == nil { + err = fmt.Errorf("could not parse received value as an IPv4 address") + log.Error(err.Error()) + return + } + log.Infof("IP address received: %s", ip) + } + return +} + +func DiscoverIPv6(DiscoveryURL string) (ip net.IP, err error) { + currentDelay := 10 * time.Second + incrementDelay := 10 * time.Second + retries := 3 + // get ip + log.Infof("Contacting the IPv6 discovery service (%s)...", DiscoveryURL) + resp, retryable, err := RetryableGet(DiscoveryURL) + if err != nil { log.Error(err.Error()) + if retryable { + for count := 0; count < retries; count++ { + log.Infof("will retry in %s", currentDelay.String()) + time.Sleep(currentDelay) + // action + resp, retryable, err = RetryableGet(DiscoveryURL) + if err != nil { + log.Error(err.Error()) + if retryable { + currentDelay += incrementDelay + continue + } else { + // if not retryable, break loop + break + } + } else { + // if no error, we can break loop as well + break + } + } + } + } + // if still error, return + if err != nil { + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Errorf("could not read response from IP discovery service: %s", err.Error()) return } - log.Infof("IP address received: %s", ip) + if valid.IsIPv6(string(body)) { + ip = net.ParseIP(string(body)) + if ip == nil { + err = fmt.Errorf("could not parse received value as an IPv6 address") + log.Error(err.Error()) + return + } + log.Infof("IP address received: %s", ip) + } return } diff --git a/pkg/util/main.go b/pkg/util/main.go index 748d71e..c8d8d42 100644 --- a/pkg/util/main.go +++ b/pkg/util/main.go @@ -17,24 +17,14 @@ package util import ( + "context" "net" "github.com/cloudflare/cloudflare-go" log "github.com/sirupsen/logrus" ) -func PerformRecordUpdate(token string, domain string, host string, ipv4 net.IP, ipv6 net.IP) (err error) { - if ipv4 != nil { - err = UpdateCFRecord(token, domain, host, "A", ipv4) - } - - if ipv6 != nil { - err = UpdateCFRecord(token, domain, host, "AAAA", ipv6) - } - return -} - -func UpdateCFRecord(token string, domain string, host string, recordType string, ip net.IP) (err error) { +func UpdateCFRecord(token string, domain string, host string, recordType string, ip net.IP, dryRun bool, createMode bool) (err error) { // start with creating a CF api object with the token. api, err := cloudflare.NewWithAPIToken(token) if err != nil { @@ -53,11 +43,39 @@ func UpdateCFRecord(token string, domain string, host string, recordType string, Name: host, Type: recordType, } - records, err := api.DNSRecords(id, cfRecord) + records, err := api.DNSRecords(context.Background(),id, cfRecord) if err != nil { - log.Errorf("Error encountered while checking current IP of %s: %s", host, err.Error()) - return + log.Errorf("Error encountered querying record %s: %s", host, err.Error()) + return } + if len(records)==0 { + if createMode { + log.Infof("No record found for %s (type %s). Will attempt to create one...", host, recordType) + // don't know why cf sdk requires a pointer to a boolean for proxified... + proxied:=true + newRecord := cloudflare.DNSRecord{ + Name: host, + Type: recordType, + Content: ip.String(), + Proxied: &proxied, + } + if !dryRun { + _, err = api.CreateDNSRecord(context.Background(), id, newRecord) + if err != nil { + log.Errorf("Error encountered while creating record for %s: %s", host, err.Error()) + return + } + log.Infof("Record created for %s: %s", host, recordType) + } else { + log.Infof("Skipped creation of DNS record. (dry-run mode active)") + } + return + } else { + log.Errorf("No record found for %s: %s", host, recordType) + return + } + } + for _, record := range records { CurrentIP := net.ParseIP(record.Content) if CurrentIP.Equal(ip) { @@ -65,11 +83,15 @@ func UpdateCFRecord(token string, domain string, host string, recordType string, } else { log.Infof("Updating IP address of record %s (type %s) to %s", record.Name, record.Type, ip) record.Content = ip.String() - err = api.UpdateDNSRecord(id, record.ID, record) - if err != nil { - log.Errorf("Error updating DNS record for %s (type %s) to %s: %s", record.Name, record.Type, ip, err.Error()) + if !dryRun { + err = api.UpdateDNSRecord(context.Background(), id, record.ID, record) + if err != nil { + log.Errorf("Error updating DNS record for %s (type %s) to %s: %s", record.Name, record.Type, ip, err.Error()) + } else { + log.Infof("IP address of record record %s (type %s) successfully updated.", record.Name, record.Type) + } } else { - log.Infof("IP address of record record %s (type %s) successfully updated.", record.Name, record.Type) + log.Infof("Skip update of DNS record. (dry-run mode active)") } } } From 403a6e41a05d35b42ec5810f28489b8df5017d80 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Sat, 26 Jun 2021 15:22:53 +0200 Subject: [PATCH 3/5] go version bump --- build/ci/travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/ci/travis.yml b/build/ci/travis.yml index 1d10ab0..64be3ff 100644 --- a/build/ci/travis.yml +++ b/build/ci/travis.yml @@ -1,7 +1,7 @@ dist: bionic language: go go: -- 1.15.x +- 1.16.x sudo: false if: branch != main jobs: From 1971d949ab51dbdf5cd75cac7ba26a3e9773aa47 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Sat, 26 Jun 2021 16:31:37 +0200 Subject: [PATCH 4/5] updated readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3c6927..70d9d69 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,8 @@ If setting the `CF_API_TOKEN` is not possible for some reason, it is possible to ## Ipv6 support This software now includes experimental ipv6 support. you can enable it with the `-ipv6=true` parameter. This will update the AAAA-record for the specified host. -Make sure an AAAA-record is already created for the host. + +** Attention: Most operating systems use a temporary ipv6 address for outbound connections. This feature needs to be disabled in order for `cf-ddns-agent` to work correctly! Check your operating system's manual. ** ## Roadmap From a376aff020cd9e958af147cabf85c9654b61a785 Mon Sep 17 00:00:00 2001 From: Jeroen Jacobs Date: Sat, 26 Jun 2021 16:32:45 +0200 Subject: [PATCH 5/5] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70d9d69..96936cf 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ If setting the `CF_API_TOKEN` is not possible for some reason, it is possible to This software now includes experimental ipv6 support. you can enable it with the `-ipv6=true` parameter. This will update the AAAA-record for the specified host. -** Attention: Most operating systems use a temporary ipv6 address for outbound connections. This feature needs to be disabled in order for `cf-ddns-agent` to work correctly! Check your operating system's manual. ** +**Attention: Most operating systems use a temporary ipv6 address for outbound connections. This feature needs to be disabled in order for `cf-ddns-agent` to work correctly! Check your operating system's manual.** ## Roadmap