1
0

add support for ipv4 in ipv6 addresses

This commit is contained in:
John Aylward 2019-07-21 01:13:34 -04:00
parent 9a990b48ce
commit edf24ec90c
3 changed files with 96 additions and 38 deletions

View File

@ -23,6 +23,7 @@ create a file ".digitalocean-dynamic-ip.json"(dot prefix to hide the file) and p
"doPageSize" : 20, "doPageSize" : 20,
"useIPv4": true, "useIPv4": true,
"useIPv6": false, "useIPv6": false,
"allowIPv4InIPv6": false,
"domains": [ "domains": [
{ {
"domain": "example.com", "domain": "example.com",
@ -48,10 +49,12 @@ create a file ".digitalocean-dynamic-ip.json"(dot prefix to hide the file) and p
``` ```
The TTL can optionally be updated if passed in the configuration. Digital Ocean has a minimum TTL of 30 seconds. The `type` and the `name` must match existing records in the Digital Ocean DNS configuration. Only `types` of `A` and `AAAA` allowed at the moment. The TTL can optionally be updated if passed in the configuration. Digital Ocean has a minimum TTL of 30 seconds. The `type` and the `name` must match existing records in the Digital Ocean DNS configuration. Only `types` of `A` and `AAAA` allowed at the moment.
If you want to reduce the number of calls made to the digital ocean API and have more than 20 DNS records in your domain, you can adjust the `doPageSize` parameter. By default, Digital Ocean returns 20 records per page. If you want to reduce the number of calls made to the digital ocean API and have more than 20 DNS records in your domain, you can adjust the `doPageSize` parameter. By default, Digital Ocean returns 20 records per page. Digital Ocean has a max page size of 200 items.
By default, the configuration checks both IPv4 and IPv6 addresses assuming your provider set up your connection as dual stack. If you know you only have ipv4 or ipv6 you can disable using one or the other in the config. To disable one or the other, set the `useIPv4` or `useIPv6` settings to `false`. If the options aren't present, or are set to `null`, then the configuration assumes a value of `true`. By default, the configuration checks both IPv4 and IPv6 addresses assuming your provider set up your connection as dual stack. If you know you only have ipv4 or ipv6 you can disable using one or the other in the config. To disable one or the other, set the `useIPv4` or `useIPv6` settings to `false`. If the options aren't present, or are set to `null`, then the configuration assumes a value of `true`.
The `allowIPv4InIPv6` configuration option will allow adding an IPv4 address to be used in a AAAA record for IPv6 lookups.
```bash ```bash
#run #run
go build digitalocean-dynamic-ip.go go build digitalocean-dynamic-ip.go

View File

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/hex"
"encoding/json" "encoding/json"
"flag" "flag"
"fmt" "fmt"
@ -26,11 +27,12 @@ var config ClientConfig
// ClientConfig : configuration json // ClientConfig : configuration json
type ClientConfig struct { type ClientConfig struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
DOPageSize int `json:"doPageSize"` DOPageSize int `json:"doPageSize"`
UseIPv4 *bool `json:"useIPv4"` UseIPv4 *bool `json:"useIPv4"`
UseIPv6 *bool `json:"useIPv6"` UseIPv6 *bool `json:"useIPv6"`
Domains []Domain `json:"domains"` AllowIPv4InIPv6 bool `json:"allowIPv4InIPv6"`
Domains []Domain `json:"domains"`
} }
// Domain : domains to be changed // Domain : domains to be changed
@ -128,7 +130,7 @@ func CheckLocalIPs() (ipv4, ipv6 net.IP) {
ipv4 = ipv4.To4() ipv4 = ipv4.To4()
} }
if ipv4 == nil { if ipv4 == nil {
log.Printf("Unable to parse `%s` as an IPv4 address\n", ipv4String) log.Printf("Unable to parse `%s` as an IPv4 address", ipv4String)
} }
} }
} }
@ -140,7 +142,7 @@ func CheckLocalIPs() (ipv4, ipv6 net.IP) {
} else { } else {
ipv6 = net.ParseIP(ipv6String) ipv6 = net.ParseIP(ipv6String)
if ipv6 == nil { if ipv6 == nil {
log.Printf("Unable to parse `%s` as an IPv4 address\n", ipv6String) log.Printf("Unable to parse `%s` as an IPv6 address", ipv6String)
} }
} }
} }
@ -165,7 +167,12 @@ func GetDomainRecords(domain string) []DNSRecord {
pageParam := "" pageParam := ""
// 20 is the default page size // 20 is the default page size
if config.DOPageSize > 0 && config.DOPageSize != 20 { if config.DOPageSize > 0 && config.DOPageSize != 20 {
pageParam = "?per_page=" + strconv.Itoa(config.DOPageSize) pageSize := config.DOPageSize
// don't let users set more than the max size
if pageSize > 200 {
pageSize = 200
}
pageParam = "?per_page=" + strconv.Itoa(pageSize)
} }
for url := "https://api.digitalocean.com/v2/domains/" + url.PathEscape(domain) + "/records" + pageParam; url != ""; url = page.Links.Pages.Next { for url := "https://api.digitalocean.com/v2/domains/" + url.PathEscape(domain) + "/records" + pageParam; url != ""; url = page.Links.Pages.Next {
page = getPage(url) page = getPage(url)
@ -185,7 +192,7 @@ func getPage(url string) DOResponse {
checkError(err) checkError(err)
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
// log.Print(string(body)) // log.Println(string(body))
var jsonDOResponse DOResponse var jsonDOResponse DOResponse
e := json.Unmarshal(body, &jsonDOResponse) e := json.Unmarshal(body, &jsonDOResponse)
checkError(e) checkError(e)
@ -193,51 +200,63 @@ func getPage(url string) DOResponse {
} }
// UpdateRecords : Update DNS records of domain // UpdateRecords : Update DNS records of domain
func UpdateRecords(domain string, ipv4, ipv6 net.IP, toUpdateRecords []DNSRecord) { func UpdateRecords(domain Domain, ipv4, ipv6 net.IP) {
log.Printf("%s: %d to update\n", domain, len(toUpdateRecords)) log.Printf("%s: %d to update", domain.Domain, len(domain.Records))
updated := 0 updated := 0
doRecords := GetDomainRecords(domain) doRecords := GetDomainRecords(domain.Domain)
// look for the item to update // look for the item to update
if len(doRecords) < 1 { if len(doRecords) < 1 {
log.Printf("%s: No DNS records found", domain) log.Printf("%s: No DNS records found in Digital Ocean", domain.Domain)
return return
} }
log.Printf("%s: %d DNS records found", domain, len(doRecords)) log.Printf("%s: %d DNS records found in Digital Ocean", domain.Domain, len(doRecords))
for _, toUpdateRecord := range toUpdateRecords { for _, toUpdateRecord := range domain.Records {
if toUpdateRecord.Type != "A" && toUpdateRecord.Type != "AAAA" { if toUpdateRecord.Type != "A" && toUpdateRecord.Type != "AAAA" {
log.Printf("%s: Unsupported type (Only A and AAAA records supported) for updates %+v", domain, toUpdateRecord) log.Printf("%s: Unsupported type (Only A and AAAA records supported) for updates %+v", domain.Domain, toUpdateRecord)
continue continue
} }
if ipv4 == nil && toUpdateRecord.Type == "A" { if ipv4 == nil && toUpdateRecord.Type == "A" {
log.Printf("%s: You are trying to update an IPv4 A record with no IPv4 address: config: %+v", domain, toUpdateRecord) log.Printf("%s: You are trying to update an IPv4 A record with no IPv4 address: config: %+v", domain.Domain, toUpdateRecord)
continue
}
if ipv6 == nil && toUpdateRecord.Type == "AAAA" {
log.Printf("%s: You are trying to update an IPv6 AAAA record with no IPv6 address: config: %+v", domain, toUpdateRecord)
continue continue
} }
if toUpdateRecord.ID > 0 { if toUpdateRecord.ID > 0 {
// update the record directly. skip the extra search // update the record directly. skip the extra search
log.Printf("%s: Unable to directly update records yet. Record: %+v", domain, toUpdateRecord) log.Printf("%s: Unable to directly update records yet. Record: %+v", domain.Domain, toUpdateRecord)
continue continue
} }
var currentIP string var currentIP string
if toUpdateRecord.Type == "A" { if toUpdateRecord.Type == "A" {
currentIP = ipv4.String() currentIP = ipv4.String()
} else if ipv6 == nil || ipv6.To4() != nil {
if ipv6 == nil {
ipv6 = ipv4
}
log.Printf("%s: You are trying to update an IPv6 AAAA record without an IPv6 address: ip: %s config: %+v",
domain.Domain,
ipv6,
toUpdateRecord,
)
if config.AllowIPv4InIPv6 {
currentIP = toIPv6String(ipv6)
log.Printf("%s: Converting IPv4 `%s` to IPv6 `%s`", domain.Domain, ipv6.String(), currentIP)
} else {
continue
}
} else { } else {
currentIP = ipv6.String() currentIP = ipv6.String()
} }
log.Printf("%s: trying to update `%s` : `%s`", domain, toUpdateRecord.Type, toUpdateRecord.Name) log.Printf("%s: trying to update `%s` : `%s`", domain.Domain, toUpdateRecord.Type, toUpdateRecord.Name)
for _, doRecord := range doRecords { for _, doRecord := range doRecords {
//log.Printf("%s: checking `%s` : `%s`", domain, doRecord.Type, doRecord.Name) //log.Printf("%s: checking `%s` : `%s`", domain.Domain, doRecord.Type, doRecord.Name)
if doRecord.Name == toUpdateRecord.Name && doRecord.Type == toUpdateRecord.Type { if doRecord.Name == toUpdateRecord.Name && doRecord.Type == toUpdateRecord.Type {
if doRecord.Data == currentIP && (toUpdateRecord.TTL < 30 || doRecord.TTL == toUpdateRecord.TTL) { if doRecord.Data == currentIP && (toUpdateRecord.TTL < 30 || doRecord.TTL == toUpdateRecord.TTL) {
log.Printf("%s: IP/TTL did not change %+v", domain, doRecord) log.Printf("%s: IP/TTL did not change %+v", domain.Domain, doRecord)
continue continue
} }
log.Printf("%s: updating %+v", domain, doRecord) log.Printf("%s: updating %+v", domain.Domain, doRecord)
// set the IP address // set the IP address
doRecord.Data = currentIP doRecord.Data = currentIP
if toUpdateRecord.TTL >= 30 && doRecord.TTL != toUpdateRecord.TTL { if toUpdateRecord.TTL >= 30 && doRecord.TTL != toUpdateRecord.TTL {
@ -247,7 +266,7 @@ func UpdateRecords(domain string, ipv4, ipv6 net.IP, toUpdateRecords []DNSRecord
checkError(err) checkError(err)
client := &http.Client{} client := &http.Client{}
request, err := http.NewRequest("PUT", request, err := http.NewRequest("PUT",
"https://api.digitalocean.com/v2/domains/"+url.PathEscape(domain)+"/records/"+strconv.FormatInt(int64(doRecord.ID), 10), "https://api.digitalocean.com/v2/domains/"+url.PathEscape(domain.Domain)+"/records/"+strconv.FormatInt(int64(doRecord.ID), 10),
bytes.NewBuffer(update)) bytes.NewBuffer(update))
checkError(err) checkError(err)
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")
@ -256,28 +275,63 @@ func UpdateRecords(domain string, ipv4, ipv6 net.IP, toUpdateRecords []DNSRecord
checkError(err) checkError(err)
defer response.Body.Close() defer response.Body.Close()
body, err := ioutil.ReadAll(response.Body) body, err := ioutil.ReadAll(response.Body)
log.Printf("%s: DO update response for %s: %s\n", domain, doRecord.Name, string(body)) log.Printf("%s: DO update response for %s: %s", domain.Domain, doRecord.Name, string(body))
updated++ updated++
} }
} }
} }
log.Printf("%s: %d of %d records updated\n", domain, updated, len(toUpdateRecords)) log.Printf("%s: %d of %d records updated", domain.Domain, updated, len(domain.Records))
}
// toIPv6String : net.IP.String will always output an IPv4 address in dot
// notation (127.0.0.1) even if we convert it using net.IP.To16().
// For AAAA records, we can't have that. Instead, force the
// IP to have the IPv6 colon notation.
func toIPv6String(ip net.IP) (currentIP string) {
if ip == nil {
return ""
}
if ipv4 := ip.To4(); ipv4 != nil {
ip = ipv4
}
l := len(ip)
if l < 16 {
// ensure "v4InV6Prefix" for IPv4 addresses
currentIP = "::ffff:"
}
// byte length of an ipv6 segment.
segSize := 2
for i := 0; i < l; i += segSize {
end := i + segSize
bs := ip[i:end]
addColon := (end + 1) < l
currentIP += hex.EncodeToString(bs)
if addColon {
currentIP += ":"
}
}
return currentIP
}
func areZero(bs []byte) bool {
for _, b := range bs {
if b != 0 {
return false
}
}
return true
} }
func main() { func main() {
config = GetConfig() config = GetConfig()
currentIPv4, currentIPv6 := CheckLocalIPs() currentIPv4, currentIPv6 := CheckLocalIPs()
if currentIPv4 == nil && currentIPv6 == nil { if currentIPv4 == nil && currentIPv6 == nil {
log.Fatalf("current IP addresses are not a valid, or both are disabled in the config. Check you configuration and internet connection", log.Fatal("current IP addresses are not a valid, or both are disabled in the config. Check you configuration and internet connection")
currentIPv4,
currentIPv6,
)
} }
for _, domain := range config.Domains { for _, domain := range config.Domains {
domainName := domain.Domain log.Printf("%s: START", domain.Domain)
log.Printf("%s: START\n", domainName) UpdateRecords(domain, currentIPv4, currentIPv6)
UpdateRecords(domainName, currentIPv4, currentIPv6, domain.Records) log.Printf("%s: END", domain.Domain)
log.Printf("%s: END\n", domainName)
} }
} }

View File

@ -3,6 +3,7 @@
"doPageSize": 20, "doPageSize": 20,
"useIPv4": true, "useIPv4": true,
"useIPv6": true, "useIPv6": true,
"allowIPv4InIPv6": false,
"domains": [ "domains": [
{ {
"domain": "example.com", "domain": "example.com",