From 14b876b2abe9017f15aa3f6198a04f2f9127fa02 Mon Sep 17 00:00:00 2001 From: Boris Zhao Date: Thu, 23 Jan 2020 17:39:56 +0800 Subject: [PATCH] Support updating IPv6 records on CloudFlare Support updating IPv6 records on CloudFlare Support updating IPv6 records for fixing issue #44 Known bugs: Addresses with leading zeros(e.g. 2001:0db8:0000:0000:0000:ff00:0042:8329) and without leading zeros(e.g. 2001:db8:0:0:0:ff00:42:8329) will be treated as different addresses. Addresses with consecutive sections of zeros(e.g. 2001:0db8:0000:0000:0000:ff00:0042:8329) and without consecutive sections of zeros(e.g. 2001:db8::ff00:42:8329) will be treated as different addresses. --- LICENSE | 7 +++++++ README.md | 7 ++++--- alidnsutil/alidnsutil.go | 4 ++-- cfutil/cfutil.go | 38 +++++++++++++++++++++++---------- conf/config.go | 7 +++++-- config.yaml.template | 5 ++++- constants/errormsg.go | 6 ++++-- go.mod | 9 ++++++-- go.sum | 2 ++ main.go | 45 ++++++++++++++++++++++++++++++++-------- 10 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7bb4e7b --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 boris1993 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index d439d71..5520dcf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # dnsupdater -|master| dev | -|:----:|:---:| -|[![Build Status](https://travis-ci.org/boris1993/dnsupdater.svg?branch=master)](https://travis-ci.org/boris1993/dnsupdater)|[![Build Status](https://travis-ci.org/boris1993/dnsupdater.svg?branch=dev)](https://travis-ci.org/boris1993/dnsupdater)| +[![Build Status](https://travis-ci.org/boris1993/dnsupdater.svg?branch=master)](https://travis-ci.org/boris1993/dnsupdater) +![GitHub](https://img.shields.io/github/license/boris1993/dnsupdater) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/boris1993/dnsupdater) +![Total download](https://img.shields.io/github/downloads/boris1993/dnsupdater/total.svg) Obtain your current external IP address and update to the specified DNS record on CloudFlare diff --git a/alidnsutil/alidnsutil.go b/alidnsutil/alidnsutil.go index 958cb69..dbe65ec 100644 --- a/alidnsutil/alidnsutil.go +++ b/alidnsutil/alidnsutil.go @@ -10,9 +10,9 @@ import ( "strings" ) -// ProcessRecords takes the configuration and the current IP address +// ProcessRecords takes the configuration as well as the current IP address // then check and update each DNS record in Aliyun DNS -func ProcessRecords(config *conf.Config, currentIPAddress string) { +func ProcessRecords(config conf.Config, currentIPAddress string) { log.Println(len(config.AliDNSRecords), constants.MsgAliDNSRecordsFoundSuffix) for _, aliDNSRecord := range config.AliDNSRecords { diff --git a/cfutil/cfutil.go b/cfutil/cfutil.go index 807e27c..445d5b1 100644 --- a/cfutil/cfutil.go +++ b/cfutil/cfutil.go @@ -14,9 +14,9 @@ import ( "github.com/boris1993/dnsupdater/model" ) -// ProcessRecords takes the configuration and the current IP address, +// ProcessRecords takes the configuration as well as the current IP address, // then check and update each DNS record in CloudFlare -func ProcessRecords(config *conf.Config, currentIPAddress string) error { +func ProcessRecords(config conf.Config, currentIPv4Address string, currentIPv6Address string) error { if config.System.CloudFlareAPIEndpoint == "" { return errors.New(constants.ErrCloudFlareAPIAddressEmpty) @@ -30,6 +30,7 @@ func ProcessRecords(config *conf.Config, currentIPAddress string) error { if cloudFlareRecord.APIKey == "" || cloudFlareRecord.AuthEmail == "" || cloudFlareRecord.DomainName == "" || + cloudFlareRecord.DomainType == "" || cloudFlareRecord.ZoneID == "" { // Print error and skip to next record when bad configuration found log.Errorln(constants.ErrCloudFlareRecordConfigIncomplete) @@ -39,6 +40,15 @@ func ProcessRecords(config *conf.Config, currentIPAddress string) error { // Prints which record is being processed log.Println(constants.MsgHeaderDomainProcessing, cloudFlareRecord.DomainName) + var newIpAddress = "" + if cloudFlareRecord.DomainType == "A" { + newIpAddress = currentIPv4Address + } else if cloudFlareRecord.DomainType == "AAAA" { + newIpAddress = currentIPv6Address + } else { + log.Errorln(constants.ErrInvalidDomainType) + } + // Then fetch the IP address of the specified DNS record id, recordAddress, err := getCFDnsRecordIpAddress(cloudFlareRecord) @@ -48,12 +58,12 @@ func ProcessRecords(config *conf.Config, currentIPAddress string) error { } // Do nothing when the IP address didn't change. - if currentIPAddress == recordAddress { + if newIpAddress == recordAddress { log.Println(constants.MsgIPAddrNotChanged) continue } else { // Update the IP address when changed. - status, err := updateCFDNSRecord(id, currentIPAddress, cloudFlareRecord) + status, err := updateCFDNSRecord(id, cloudFlareRecord.DomainType, newIpAddress, cloudFlareRecord) if err != nil { log.Errorln(err) @@ -73,9 +83,9 @@ func ProcessRecords(config *conf.Config, currentIPAddress string) error { } // getCFDnsRecordIpAddress gets the IP address associated with the specified DNS record, -// which is identified by the combination of the record type(hard coded as A type for now) and the domain name. +// which is identified by the combination of the record type, and the domain name. // -// cloudFlareRecord contains the information which this process needed, and it is coming from the config.yaml. +// cloudFlareRecord contains the information, which are needed by this process, and it is coming from the config.yaml. // // The first value returned is the ID of this DNS record, // the second value returned is the IP address of this record, @@ -86,9 +96,15 @@ func getCFDnsRecordIpAddress(cloudFlareRecord conf.CloudFlare) (string, string, client := &http.Client{} req, err := http.NewRequest(http.MethodGet, - APIEndpoint+"/zones/"+cloudFlareRecord.ZoneID+"/dns_records?type=A&name="+cloudFlareRecord.DomainName, + APIEndpoint+"/zones/"+cloudFlareRecord.ZoneID+"/dns_records?type="+cloudFlareRecord.DomainType+"&name="+cloudFlareRecord.DomainName, nil) + if err != nil { + return "", "", err + } + + log.Debug("Request URI: \n" + req.URL.String()) + req.Header.Add("X-Auth-Email", cloudFlareRecord.AuthEmail) req.Header.Add("X-Auth-Key", cloudFlareRecord.APIKey) req.Header.Add("Content-Type", "application/json") @@ -102,6 +118,7 @@ func getCFDnsRecordIpAddress(cloudFlareRecord conf.CloudFlare) (string, string, } if resp.StatusCode != 200 { + // TODO Maybe return the corresponding error message instead of the error code return "", "", errors.New(resp.Status) } @@ -147,18 +164,17 @@ func getCFDnsRecordIpAddress(cloudFlareRecord conf.CloudFlare) (string, string, // updateCFDNSRecord updates the specified DNS record identified by the record ID. // -// id is the record ID, address is the IP address to be written, -// and cloudFlareRecord contains the information corresponding to the DNS record to be updated. +// id is the record ID, address is the IP address to be written, and cloudFlareRecord contains the information corresponding to the DNS record to be updated. // // The return value is the status(true or false) of the update process, // or an error will be returned if any error occurs. -func updateCFDNSRecord(id string, address string, cloudFlareRecord conf.CloudFlare) (bool, error) { +func updateCFDNSRecord(id string, recordType string, address string, cloudFlareRecord conf.CloudFlare) (bool, error) { APIEndpoint := conf.Get().System.CloudFlareAPIEndpoint client := &http.Client{} updateRecordData := model.UpdateRecordData{} - updateRecordData.RecordType = "A" + updateRecordData.RecordType = recordType updateRecordData.Name = cloudFlareRecord.DomainName updateRecordData.Content = address diff --git a/conf/config.go b/conf/config.go index 7514861..fd60822 100644 --- a/conf/config.go +++ b/conf/config.go @@ -16,7 +16,7 @@ var once sync.Once var Debug bool var Path string -var conf *Config +var conf Config // Config describes the top-level properties in Config.yaml type Config struct { @@ -28,6 +28,7 @@ type Config struct { // System describes the System properties in Config.yaml type System struct { IPAddrAPI string `yaml:"IPAddrAPI"` + IPv6AddrAPI string `yaml:"IPv6AddrAPI"` CloudFlareAPIEndpoint string `yaml:"CloudFlareAPIEndpoint"` } @@ -38,6 +39,7 @@ type CloudFlare struct { ZoneID string `yaml:"ZoneID"` AuthEmail string `yaml:"AuthEmail"` DomainName string `yaml:"DomainName"` + DomainType string `yaml:"DomainType"` } type AliDNS struct { @@ -47,7 +49,7 @@ type AliDNS struct { DomainName string `yaml:"DomainName"` } -func Get() *Config { +func Get() Config { once.Do(func() { err := initConfig() @@ -103,6 +105,7 @@ func printDebugInfo() { log.Debugf("%10v: %s", "ZoneID", item.ZoneID) log.Debugf("%10v: %s", "AuthEmail", item.AuthEmail) log.Debugf("%10v: %s", "DomainName", item.DomainName) + log.Debugf("%10v: %s", "DomainType", item.DomainType) log.Debugln() } diff --git a/config.yaml.template b/config.yaml.template index bf10c64..164b5b4 100644 --- a/config.yaml.template +++ b/config.yaml.template @@ -1,18 +1,21 @@ --- System: IPAddrAPI: "https://api.ipify.org/" + IPv6AddrAPI: "https://api6.ipify.org/" CloudFlareAPIEndpoint: "https://api.cloudflare.com/client/v4" CloudFlareRecords: - APIKey: "YOUR_CLOUDFLARE_API_KEY" ZoneID: "ZONE_ID_OF_YOUR_DOMAIN" AuthEmail: "EMAIL_FOR_LOGGING_INTO_CLOUDFLARE" DomainName: "FULL_DOMAIN_YOU_WANT_TO_UPDATE. e.g.:test1.example.com" + DomainType: "Type of this domain. A for IPv4 records, and AAAA for IPv6 records" - APIKey: "YOUR_CLOUDFLARE_API_KEY" ZoneID: "ZONE_ID_OF_YOUR_DOMAIN" AuthEmail: "EMAIL_FOR_LOGGING_INTO_CLOUDFLARE" DomainName: "FULL_DOMAIN_YOU_WANT_TO_UPDATE. e.g.:test2.example.com" + DomainType: "Type of this domain. A for IPv4 records, and AAAA for IPv6 records" AliDNSRecords: - AccessKeyID: "YOUR_ALIYUN_ACCESS_KEY_ID" AccessKeySecret: "YOUR_ALIYUN_ACCESS_KEY_SECRET" RegionID: "cn-hangzhou" # Only this region is accepted so DO NOT MODIFY THIS PROPERTY - DomainName: "FULL_DOMAIN_YOU_WANT_TO_UPDATE. e.g.:test3.example.com" \ No newline at end of file + DomainName: "FULL_DOMAIN_YOU_WANT_TO_UPDATE. e.g.:test3.example.com" diff --git a/constants/errormsg.go b/constants/errormsg.go index 44013e4..0bbcfc1 100644 --- a/constants/errormsg.go +++ b/constants/errormsg.go @@ -2,9 +2,10 @@ package constants const MsgHeaderDNSRecordUpdateSuccessful = "Successfully updated the DNS record" -const MsgHeaderCurrentIPAddr = "Current IP address is:" +const MsgHeaderCurrentIPAddr = "Current IPv4 address is:" +const MsgHeaderCurrentIPv6Addr = "Current IPv6 address is:" const MsgHeaderFetchingIPOfDomain = "Fetching the IP address of domain" -const MsgHeaderLoadingConfig = "Loading configuration from" +const MsgHeaderLoadingConfig = "Loading configurations from" const MsgHeaderDomainProcessing = "Processing" const MsgCloudFlareRecordsFoundSuffix = "CloudFlare DNS record(s) found" const MsgAliDNSRecordsFoundSuffix = "Aliyun DNS record(s) found" @@ -26,3 +27,4 @@ const ErrIPAddressFetchingAPIEmpty = "API address for fetching current IP addres const ErrCloudFlareAPIAddressEmpty = "CloudFlare API endpoint address cannot be empty" const ErrCloudFlareRecordConfigIncomplete = "Incomplete CloudFlare configuration found" const ErrAliDNSRecordConfigIncomplete = "Incomplete Aliyun DNS configuration found" +const ErrInvalidDomainType = "Invalid domain type. Only A and AAAA records are supported for now." diff --git a/go.mod b/go.mod index b5a3c15..ec1e7b3 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,19 @@ module github.com/boris1993/dnsupdater go 1.12 require ( - github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190520072130-def1e914efff + github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20191104061422-c78ca2ea5e12 + github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5 // indirect + github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect - github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect github.com/json-iterator/go v1.1.6 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/satori/go.uuid v1.2.0 // indirect github.com/sirupsen/logrus v1.4.2 github.com/smartystreets/assertions v0.0.0-20190401211740-f487f9de1cd3 // indirect golang.org/x/sys v0.0.0-20190516110030-61b9204099cb // indirect + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 9bb63bc..4c1520c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190520072130-def1e914efff h1:ojiFYVfdKow1Q6HyWCSdBBFzEqUzKSQzo/ZVJOQqBtQ= github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190520072130-def1e914efff/go.mod h1:0nPXeXAsIm3YH7imFCamfa0u+PueDefehKCQ8dicxmc= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20191104061422-c78ca2ea5e12 h1:muZ1bS7r8Yav9uNNtRNWzQulwu/sB1uhOG5sXgBHqh0= +github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20191104061422-c78ca2ea5e12/go.mod h1:mNZkuqaeM5UCiAdkV4r+lrheu8Q5fe/487bRFrGYZ8A= github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190307165228-86c17b95fcd5/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/main.go b/main.go index 69cebd3..e8dc5a6 100644 --- a/main.go +++ b/main.go @@ -19,20 +19,20 @@ func main() { var config = conf.Get() // Fetch the current external IP address. - currentIPAddress, err := getCurrentIPAddress(config) + ipAddress, ipv6Address, err := getCurrentIPAddress(config) if err != nil { log.Fatalln(err) } // Process CloudFlare records - err = cfutil.ProcessRecords(config, currentIPAddress) + err = cfutil.ProcessRecords(config, ipAddress, ipv6Address) if err != nil { log.Errorln(err) } - alidnsutil.ProcessRecords(config, currentIPAddress) + alidnsutil.ProcessRecords(config, ipAddress) os.Exit(0) } @@ -53,17 +53,18 @@ func init() { } // getCurrentIPAddress returns the external IP address for your network -func getCurrentIPAddress(config *conf.Config) (string, error) { - if config.System.IPAddrAPI == "" { - return "", errors.New(constants.ErrIPAddressFetchingAPIEmpty) +func getCurrentIPAddress(config conf.Config) (string, string, error) { + if config.System.IPAddrAPI == "" || config.System.IPv6AddrAPI == "" { + return "", "", errors.New(constants.ErrIPAddressFetchingAPIEmpty) } log.Println(constants.MsgCheckingCurrentIPAddr) + //region fetch your IPv4 address resp, err := http.Get(config.System.IPAddrAPI) if err != nil { - return "", err + return "", "", err } // Handle errors when closing the HTTP connection @@ -78,13 +79,39 @@ func getCurrentIPAddress(config *conf.Config) (string, error) { body, err := ioutil.ReadAll(resp.Body) if err != nil { - return "", err + return "", "", err } // Body only contains the IP address ipAddress := string(body) + //endregion + + //region fetch your IPv6 address + resp, err = http.Get(config.System.IPv6AddrAPI) + + if err != nil { + return "", "", err + } + + defer func() { + err := resp.Body.Close() + + if err != nil { + log.Errorln(constants.ErrCloseHTTPConnectionFail, err) + } + }() + + body, err = ioutil.ReadAll(resp.Body) + + if err != nil { + return "", "", err + } + + ipv6Address := string(body) + //endregion log.Println(constants.MsgHeaderCurrentIPAddr, ipAddress) + log.Println(constants.MsgHeaderCurrentIPv6Addr, ipv6Address) - return ipAddress, nil + return ipAddress, ipv6Address, nil }