mirror of
https://github.com/anaganisk/digitalocean-dynamic-dns-ip.git
synced 2024-08-30 17:42:10 +00:00
391 lines
11 KiB
Go
391 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
|
|
homedir "github.com/mitchellh/go-homedir"
|
|
)
|
|
|
|
func checkError(err error) {
|
|
if err != nil {
|
|
log.SetOutput(os.Stderr)
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// logError Logs an error message to Stderr and exits the program
|
|
func logError(msg string) {
|
|
log.SetOutput(os.Stderr)
|
|
log.Fatal(msg)
|
|
}
|
|
|
|
// logWarning Logs a warning message to Stderr without exiting the program
|
|
func logWarning(msg string) {
|
|
old := log.Default().Writer()
|
|
log.SetOutput(os.Stderr)
|
|
log.Println(msg)
|
|
log.SetOutput(old)
|
|
}
|
|
|
|
// logWarning Logs a warning message to Stderr without exiting the program
|
|
func logWarningf(format string, v ...interface{}) {
|
|
old := log.Default().Writer()
|
|
log.SetOutput(os.Stderr)
|
|
log.Printf(format, v...)
|
|
log.SetOutput(old)
|
|
}
|
|
|
|
var config ClientConfig
|
|
|
|
// ClientConfig : configuration json
|
|
type ClientConfig struct {
|
|
APIKey string `json:"apiKey"`
|
|
DOPageSize int `json:"doPageSize"`
|
|
UseIPv4 *bool `json:"useIPv4"`
|
|
UseIPv6 *bool `json:"useIPv6"`
|
|
IPv4CheckURL string `json:"ipv4CheckUrl"`
|
|
IPv6CheckURL string `json:"ipv6CheckUrl"`
|
|
AllowIPv4InIPv6 bool `json:"allowIPv4InIPv6"`
|
|
Domains []Domain `json:"domains"`
|
|
}
|
|
|
|
// Domain : domains to be changed
|
|
type Domain struct {
|
|
Domain string `json:"domain"`
|
|
Records []DNSRecord `json:"records"`
|
|
}
|
|
|
|
// DNSRecord : Modifyiable DNS record
|
|
type DNSRecord struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Priority *int `json:"priority"`
|
|
Port *int `json:"port"`
|
|
Weight *int `json:"weight"`
|
|
TTL int `json:"ttl"`
|
|
Flags *uint8 `json:"flags"`
|
|
Tag *string `json:"tag"`
|
|
Data string `json:"data"`
|
|
}
|
|
|
|
// DOResponse : DigitalOcean DNS Records response.
|
|
type DOResponse struct {
|
|
DomainRecords []DNSRecord `json:"domain_records"`
|
|
Meta struct {
|
|
Total int `json:"total"`
|
|
} `json:"meta"`
|
|
Links struct {
|
|
Pages struct {
|
|
First string `json:"first"`
|
|
Previous string `json:"prev"`
|
|
Next string `json:"next"`
|
|
Last string `json:"last"`
|
|
} `json:"pages"`
|
|
} `json:"links"`
|
|
}
|
|
|
|
//GetConfig : get configuration file ~/.digitalocean-dynamic-ip.json
|
|
func GetConfig() ClientConfig {
|
|
cmdHelp := flag.Bool("h", false, "Show the help message")
|
|
cmdHelp2 := flag.Bool("help", false, "Show the help message")
|
|
cmdDbg := flag.Bool("d", false, "Outputs log messages to the standard console")
|
|
cmdDbg2 := flag.Bool("debug", false, "Outputs log messages to the standard console")
|
|
flag.Parse()
|
|
|
|
if *cmdHelp || *cmdHelp2 {
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
if !((*cmdDbg) || (*cmdDbg2)) {
|
|
// if no debug option was selected, discard all debug output
|
|
log.SetOutput(ioutil.Discard)
|
|
} else {
|
|
// default debug output to Stdout instead of Stderr
|
|
log.SetOutput(os.Stdout)
|
|
}
|
|
|
|
configFile := ""
|
|
if len(flag.Args()) == 0 {
|
|
var err error
|
|
configFile, err = homedir.Dir()
|
|
checkError(err)
|
|
configFile += "/.digitalocean-dynamic-ip.json"
|
|
} else {
|
|
configFile = flag.Args()[0]
|
|
}
|
|
|
|
log.Printf("Using Config file: %s", configFile)
|
|
|
|
getfile, err := ioutil.ReadFile(configFile)
|
|
checkError(err)
|
|
var config ClientConfig
|
|
err = json.Unmarshal(getfile, &config)
|
|
checkError(err)
|
|
return config
|
|
}
|
|
|
|
func usage() {
|
|
os.Stdout.WriteString(fmt.Sprintf("To use this program you can specify the following command options:\n"+
|
|
"-h | -help\n\tShow this help message\n"+
|
|
"-d | -debug\n\tPrint debug messages to standard output\n"+
|
|
"[config_file]\n\tlocation of the configuration file\n\n"+
|
|
"If the [config_file] parameter is not passed, then the default\n"+
|
|
"config location of ~/.digitalocean-dynamic-ip.json will be used.\n\n"+
|
|
"example usages:\n\t%[1]s -help\n"+
|
|
"\t%[1]s\n"+
|
|
"\t%[1]s %[2]s\n"+
|
|
"\t%[1]s -debug %[2]s\n"+
|
|
"",
|
|
os.Args[0],
|
|
"/path/to/my/config.json",
|
|
))
|
|
}
|
|
|
|
//CheckLocalIPs : get current IP of server. checks both IPv4 and Ipv6 to support dual stack environments
|
|
func CheckLocalIPs() (ipv4, ipv6 net.IP) {
|
|
var ipv4String, ipv6String string
|
|
ipv4CheckURL := "https://api.ipify.org/?format=text"
|
|
ipv6CheckURL := "https://api64.ipify.org/?format=text"
|
|
if len(config.IPv4CheckURL) > 0 {
|
|
ipv4CheckURL = config.IPv4CheckURL
|
|
}
|
|
if len(config.IPv6CheckURL) > 0 {
|
|
ipv6CheckURL = config.IPv6CheckURL
|
|
}
|
|
|
|
if config.UseIPv4 == nil || *(config.UseIPv4) {
|
|
log.Printf("Checking IPv4 with URL: %s", ipv4CheckURL)
|
|
ipv4String, _ = getURLBody(ipv4CheckURL)
|
|
if ipv4String == "" {
|
|
logWarning("No IPv4 address found. Consider disabling IPv4 checks in the config `\"useIPv4\": false`")
|
|
} else {
|
|
ipv4 = net.ParseIP(ipv4String)
|
|
if ipv4 != nil {
|
|
// make sure we got back an actual ipv4 address
|
|
ipv4 = ipv4.To4()
|
|
log.Printf("Discovered IPv4 address `%s`", ipv4.String())
|
|
}
|
|
if ipv4 == nil {
|
|
logWarningf("Unable to parse `%s` as an IPv4 address", ipv4String)
|
|
}
|
|
}
|
|
}
|
|
|
|
if config.UseIPv6 == nil || *(config.UseIPv6) {
|
|
log.Printf("Checking IPv6 with URL: %s", ipv6CheckURL)
|
|
ipv6String, _ = getURLBody(ipv6CheckURL)
|
|
if ipv6String == "" {
|
|
logWarning("No IPv6 address found. Consider disabling IPv6 checks in the config `\"useIPv6\": false`")
|
|
} else {
|
|
ipv6 = net.ParseIP(ipv6String)
|
|
if ipv6 == nil {
|
|
logWarningf("Unable to parse `%s` as an IPv6 address", ipv6String)
|
|
} else {
|
|
log.Printf("Discovered IPv6 address `%s`", ipv6.String())
|
|
}
|
|
}
|
|
}
|
|
return ipv4, ipv6
|
|
}
|
|
|
|
func getURLBody(url string) (string, error) {
|
|
request, err := http.Get(url)
|
|
checkError(err)
|
|
defer request.Body.Close()
|
|
body, err := ioutil.ReadAll(request.Body)
|
|
checkError(err)
|
|
return string(body), nil
|
|
}
|
|
|
|
//GetDomainRecords : Get DNS records of current domain.
|
|
func GetDomainRecords(domain string) []DNSRecord {
|
|
ret := make([]DNSRecord, 0)
|
|
var page DOResponse
|
|
pageParam := ""
|
|
// 20 is the default page size
|
|
if config.DOPageSize > 0 && config.DOPageSize != 20 {
|
|
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 {
|
|
page = getPage(url)
|
|
ret = append(ret, page.DomainRecords...)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func getPage(url string) DOResponse {
|
|
log.Println(url)
|
|
client := &http.Client{}
|
|
request, err := http.NewRequest("GET", url, nil)
|
|
checkError(err)
|
|
request.Header.Add("Content-type", "Application/json")
|
|
request.Header.Add("Authorization", "Bearer "+config.APIKey)
|
|
response, err := client.Do(request)
|
|
checkError(err)
|
|
defer response.Body.Close()
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
checkError(err)
|
|
// log.Println(string(body))
|
|
var jsonDOResponse DOResponse
|
|
err = json.Unmarshal(body, &jsonDOResponse)
|
|
checkError(err)
|
|
return jsonDOResponse
|
|
}
|
|
|
|
// UpdateRecords : Update DNS records of domain
|
|
func UpdateRecords(domain Domain, ipv4, ipv6 net.IP) {
|
|
log.Printf("%s: %d to update", domain.Domain, len(domain.Records))
|
|
updated := 0
|
|
doRecords := GetDomainRecords(domain.Domain)
|
|
// look for the item to update
|
|
if len(doRecords) < 1 {
|
|
logWarningf("%s: No DNS records found in DigitalOcean", domain.Domain)
|
|
return
|
|
}
|
|
log.Printf("%s: %d DNS records found in DigitalOcean", domain.Domain, len(doRecords))
|
|
for _, toUpdateRecord := range domain.Records {
|
|
if toUpdateRecord.Type != "A" && toUpdateRecord.Type != "AAAA" {
|
|
logWarningf("%s: Unsupported type (Only A and AAAA records supported) for updates %+v", domain.Domain, toUpdateRecord)
|
|
continue
|
|
}
|
|
if ipv4 == nil && toUpdateRecord.Type == "A" {
|
|
logWarningf("%s: You are trying to update an IPv4 A record with no IPv4 address: config: %+v", domain.Domain, toUpdateRecord)
|
|
continue
|
|
}
|
|
if toUpdateRecord.ID > 0 {
|
|
// update the record directly. skip the extra search
|
|
logWarningf("%s: Unable to directly update records yet. Record: %+v", domain.Domain, toUpdateRecord)
|
|
continue
|
|
}
|
|
|
|
var currentIP string
|
|
if toUpdateRecord.Type == "A" {
|
|
currentIP = ipv4.String()
|
|
} else if ipv6 == nil || ipv6.To4() != nil {
|
|
if ipv6 == nil {
|
|
ipv6 = ipv4
|
|
}
|
|
|
|
logWarningf("%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 {
|
|
currentIP = ipv6.String()
|
|
}
|
|
|
|
log.Printf("%s: trying to update `%s` : `%s`", domain.Domain, toUpdateRecord.Type, toUpdateRecord.Name)
|
|
for _, doRecord := range doRecords {
|
|
//log.Printf("%s: checking `%s` : `%s`", domain.Domain, doRecord.Type, doRecord.Name)
|
|
if doRecord.Name == toUpdateRecord.Name && doRecord.Type == toUpdateRecord.Type {
|
|
if doRecord.Data == currentIP && (toUpdateRecord.TTL < 30 || doRecord.TTL == toUpdateRecord.TTL) {
|
|
log.Printf("%s: IP/TTL did not change %+v", domain.Domain, doRecord)
|
|
continue
|
|
}
|
|
log.Printf("%s: updating %+v", domain.Domain, doRecord)
|
|
// set the IP address
|
|
doRecord.Data = currentIP
|
|
if toUpdateRecord.TTL >= 30 && doRecord.TTL != toUpdateRecord.TTL {
|
|
doRecord.TTL = toUpdateRecord.TTL
|
|
}
|
|
update, err := json.Marshal(doRecord)
|
|
checkError(err)
|
|
client := &http.Client{}
|
|
request, err := http.NewRequest("PUT",
|
|
"https://api.digitalocean.com/v2/domains/"+url.PathEscape(domain.Domain)+"/records/"+strconv.FormatInt(int64(doRecord.ID), 10),
|
|
bytes.NewBuffer(update))
|
|
checkError(err)
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Add("Authorization", "Bearer "+config.APIKey)
|
|
response, err := client.Do(request)
|
|
checkError(err)
|
|
defer response.Body.Close()
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
checkError(err)
|
|
log.Printf("%s: DO update response for %s: %s", domain.Domain, doRecord.Name, string(body))
|
|
updated++
|
|
}
|
|
}
|
|
|
|
}
|
|
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() {
|
|
config = GetConfig()
|
|
currentIPv4, currentIPv6 := CheckLocalIPs()
|
|
if currentIPv4 == nil && currentIPv6 == nil {
|
|
logError("Current IP addresses are not valid, or both are disabled in the config. Check your configuration and internet connection.")
|
|
}
|
|
|
|
for _, domain := range config.Domains {
|
|
log.Printf("%s: START", domain.Domain)
|
|
UpdateRecords(domain, currentIPv4, currentIPv6)
|
|
log.Printf("%s: END", domain.Domain)
|
|
}
|
|
}
|