Skip to content

Commit ec85a03

Browse files
committed
IPv6, better DNS lookup and config file validation
1 parent 25140ab commit ec85a03

File tree

8 files changed

+253
-58
lines changed

8 files changed

+253
-58
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Travis (master)](https://travis-ci.com/dschanoeh/hover-ddns.svg?branch=master)](https://travis-ci.com/dschanoeh/hover-ddns)
44

5-
hover-ddns is a DDNS client that will update a DNS A record at hover with the current public IP address of the machine.
5+
hover-ddns is a DDNS client that will update a DNS A and/or AAAA record at hover with the current public IP address(es) of the machine.
66

77
This is an unofficial client using the non-supported Hover API.
88

@@ -16,6 +16,7 @@ It doesn't do anything beyond that and if you need more features or different se
1616

1717
## Features
1818

19+
* IPv4 and IPv6 supported
1920
* Supports public IP lookup by:
2021
* Using the ipify API
2122
* Issuing OpenDNS DNS queries

example.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ password: "your Hover password"
33
# Set the relative(!) hostname to be updated
44
hostname: "host"
55
domain_name: "example.com"
6+
disable_ipv4: false
7+
disable_ipv6: false
8+
# The DNS server to be used to get the current DNS records
9+
dns_server: "8.8.8.8:53"
610
# Set to true to update even if the IP already is up to date
711
force_update: false
812
public_ip_provider:
9-
service: ipify
13+
service: opendns

hover-ddns.go

Lines changed: 171 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"errors"
45
"flag"
56
"fmt"
67
"io/ioutil"
@@ -9,6 +10,7 @@ import (
910

1011
"github.com/dschanoeh/hover-ddns/hover"
1112
"github.com/dschanoeh/hover-ddns/publicip"
13+
"github.com/miekg/dns"
1214
log "github.com/sirupsen/logrus"
1315
"gopkg.in/yaml.v2"
1416
)
@@ -17,9 +19,12 @@ type Config struct {
1719
Username string
1820
Password string
1921
Hostname string
22+
DisableV4 bool `yaml:"disable_ipv4"`
23+
DisableV6 bool `yaml:"disable_ipv6"`
2024
DomainName string `yaml:"domain_name"`
2125
ForceUpdate bool `yaml:"force_update"`
2226
PublicIPProvider publicip.LookupProviderConfig `yaml:"public_ip_provider"`
27+
DNSServer string `yaml:"dns_server"`
2328
}
2429

2530
var (
@@ -30,12 +35,15 @@ var (
3035
)
3136

3237
func main() {
38+
config := Config{}
3339
var verbose = flag.Bool("verbose", false, "Turns on verbose information on the update process. Otherwise, only errors cause output.")
3440
var debug = flag.Bool("debug", false, "Turns on debug information")
3541
var dryRun = flag.Bool("dry-run", false, "Perform lookups but don't actually update the DNS info")
3642
var configFile = flag.String("config", "", "Config file")
37-
var manualIPAddress = flag.String("ip-address", "", "Specify the IP address to be submitted instead of looking it up")
43+
var manualV4 = flag.String("manual-ipv4", "", "Specify the IP address to be submitted instead of looking it up")
44+
var manualV6 = flag.String("manual-ipv6", "", "Specify the IP address to be submitted instead of looking it up")
3845
var versionFlag = flag.Bool("version", false, "Prints version information of the hover-ddns binary")
46+
var onlyValidateConfig = flag.String("validate-config", "", "Only check if the provided config file is valid")
3947

4048
flag.Parse()
4149

@@ -52,19 +60,32 @@ func main() {
5260
log.SetLevel(log.ErrorLevel)
5361
}
5462

63+
if *onlyValidateConfig != "" {
64+
err := loadConfig(*onlyValidateConfig, &config)
65+
if err != nil {
66+
log.Error("Could not load config file: ", err)
67+
os.Exit(1)
68+
}
69+
if !validateConfig(&config) {
70+
os.Exit(1)
71+
}
72+
os.Exit(0)
73+
}
74+
5575
if *configFile == "" {
5676
log.Error("Please provide a config file to read")
5777
flag.Usage()
5878
os.Exit(1)
5979
}
6080

61-
config := Config{}
62-
6381
err := loadConfig(*configFile, &config)
6482
if err != nil {
6583
log.Error("Could not load config file: ", err)
6684
os.Exit(1)
6785
}
86+
if !validateConfig(&config) {
87+
os.Exit(1)
88+
}
6889

6990
var provider publicip.LookupProvider
7091
provider, err = publicip.NewLookupProvider(&config.PublicIPProvider)
@@ -73,49 +94,100 @@ func main() {
7394
os.Exit(1)
7495
}
7596

76-
ip := net.IP{}
77-
if *manualIPAddress == "" {
78-
log.Info("Getting public IP...")
79-
ip, err = provider.GetPublicIP()
97+
publicV4 := net.IP{}
98+
publicV6 := net.IP{}
8099

81-
if err != nil {
82-
log.Error("Failed to get public ip: ", err)
83-
os.Exit(1)
100+
if !config.DisableV4 {
101+
if *manualV4 == "" {
102+
log.Info("Getting public IPv4...")
103+
publicV4, err = provider.GetPublicIP()
104+
105+
if err != nil {
106+
log.Warn("Failed to get public ip: ", err)
107+
publicV4 = nil
108+
}
109+
110+
log.Info("Received public IP " + publicV4.String())
111+
} else {
112+
publicV4 = net.ParseIP(*manualV4)
113+
log.Info("Using manually provied public IPv4 " + *manualV4)
114+
115+
if publicV4 == nil {
116+
log.Error("Provided IP '" + *manualV4 + "' is not a valid IP address.")
117+
os.Exit(1)
118+
}
84119
}
85120

86-
log.Info("Received public IP " + ip.String())
87-
} else {
88-
ip = net.ParseIP(*manualIPAddress)
89-
log.Info("Using manually provied public IP " + *manualIPAddress)
121+
log.Info("Resolving current IPv4...")
122+
currentV4, err := performDNSLookup(config.Hostname+"."+config.DomainName, config.DNSServer, dns.TypeA)
123+
if err != nil {
124+
log.Warn("Failed to resolve the current IPv4: ", err)
125+
}
126+
if currentV4 != nil {
127+
log.Info("Received current IPv4 " + currentV4.String())
128+
}
90129

91-
if ip == nil {
92-
log.Error("Provided IP '" + *manualIPAddress + "' is not a valid IP address.")
93-
os.Exit(1)
130+
if currentV4 != nil && currentV4.Equal(publicV4) {
131+
if !config.ForceUpdate {
132+
log.Info("v4 DNS entry already up to date - nothing to do.")
133+
publicV4 = nil
134+
} else {
135+
log.Info("v4 DNS entry already up to date, but update forced...")
136+
}
137+
} else {
138+
log.Info("v4 IPs differ - update required...")
94139
}
140+
} else {
141+
publicV4 = nil
95142
}
96143

97-
log.Info("Resolving current IP...")
98-
currentIP, err := resolveCurrentIP(config.Hostname + "." + config.DomainName)
144+
if !config.DisableV6 {
145+
if *manualV6 == "" {
146+
log.Info("Getting public IPv6...")
147+
publicV6, err = provider.GetPublicIPv6()
99148

100-
if err != nil {
101-
log.Error("Failed to resolve the current ip: ", err)
102-
os.Exit(1)
103-
}
104-
log.Info("Received current IP " + currentIP.String())
149+
if err != nil {
150+
log.Warn("Failed to get public ip: ", err)
151+
publicV6 = nil
152+
}
105153

106-
if currentIP.Equal(ip) {
107-
if !config.ForceUpdate {
108-
log.Info("DNS entry already up to date - nothing to do.")
109-
os.Exit(0)
154+
log.Info("Received public IP " + publicV6.String())
110155
} else {
111-
log.Info("DNS entry already up to date, but update forced...")
156+
publicV6 = net.ParseIP(*manualV6)
157+
log.Info("Using manually provied public IPv6 " + *manualV6)
158+
159+
if publicV6 == nil {
160+
log.Error("Provided IP '" + *manualV6 + "' is not a valid IP address.")
161+
os.Exit(1)
162+
}
163+
}
164+
165+
log.Info("Resolving current IPv6...")
166+
currentV6, err := performDNSLookup(config.Hostname+"."+config.DomainName, config.DNSServer, dns.TypeAAAA)
167+
if err != nil {
168+
log.Warn("Failed to resolve the current IPv6: ", err)
169+
}
170+
if currentV6 != nil {
171+
log.Info("Received current IPv6 " + currentV6.String())
172+
}
173+
174+
if currentV6 != nil && currentV6.Equal(publicV6) {
175+
if !config.ForceUpdate {
176+
log.Info("v6 DNS entry already up to date - nothing to do.")
177+
publicV6 = nil
178+
} else {
179+
log.Info("v6 DNS entry already up to date, but update forced...")
180+
}
181+
} else {
182+
log.Info("v6 IPs differ - update required...")
112183
}
113184
} else {
114-
log.Info("IPs differ - update required...")
185+
publicV6 = nil
115186
}
116187

117-
if !*dryRun {
118-
err = hover.Update(config.Username, config.Password, config.DomainName, config.Hostname, ip)
188+
// No update if we are doing a dry-run or both entries were marked as irrelevant
189+
if !*dryRun && !(publicV4 == nil && publicV6 == nil) {
190+
err = hover.Update(config.Username, config.Password, config.DomainName, config.Hostname, publicV4, publicV6)
119191
if err != nil {
120192
os.Exit(1)
121193
}
@@ -138,15 +210,76 @@ func loadConfig(filename string, config *Config) error {
138210
return nil
139211
}
140212

141-
func resolveCurrentIP(hostname string) (net.IP, error) {
142-
ips, err := net.LookupIP(hostname)
143-
if err != nil {
213+
func validateConfig(config *Config) bool {
214+
if config.DNSServer == "" {
215+
log.Error("Invalid config: A DNS server must be provided")
216+
return false
217+
}
218+
219+
if config.DomainName == "" {
220+
log.Error("Invalid config: A domain name must be provided")
221+
return false
222+
}
223+
224+
if config.Hostname == "" {
225+
log.Error("Invalid config: A host name must be provided")
226+
return false
227+
}
228+
229+
if config.Password == "" {
230+
log.Error("Invalid config: A password must be provided")
231+
return false
232+
}
233+
234+
if config.Username == "" {
235+
log.Error("Invalid config: A user name must be provided")
236+
return false
237+
}
238+
239+
if config.PublicIPProvider.Service == "" {
240+
log.Error("Invalid config: A public IP service must be selected")
241+
return false
242+
}
243+
244+
if config.PublicIPProvider.Service == "local-interface" && config.PublicIPProvider.InterfaceName == "" {
245+
log.Error("Invalid config: When selecting the local-interface provider, an interface name must be provided")
246+
return false
247+
}
248+
249+
return true
250+
}
251+
252+
func performDNSLookup(hostname string, dnsServer string, dnsType uint16) (net.IP, error) {
253+
client := dns.Client{}
254+
message := dns.Msg{}
255+
message.SetQuestion(hostname+".", dnsType)
256+
257+
res, _, err := client.Exchange(&message, dnsServer)
258+
if res == nil {
144259
return nil, err
145260
}
146261

147-
if len(ips) > 1 {
148-
log.Warn("Received more than one IP address. Using the first one...")
262+
if res.Rcode != dns.RcodeSuccess {
263+
return nil, errors.New("Invalid DNS answer")
264+
}
265+
266+
if len(res.Answer) == 0 {
267+
return nil, errors.New("Didn't get any results for the query")
268+
}
269+
270+
if len(res.Answer) > 1 {
271+
log.Warn("Received more than one IPs - just returning the first one")
272+
}
273+
274+
record := res.Answer[0]
275+
switch dnsType {
276+
case dns.TypeA:
277+
aRecord := record.(*dns.A)
278+
return aRecord.A, nil
279+
case dns.TypeAAAA:
280+
aRecord := record.(*dns.AAAA)
281+
return aRecord.AAAA, nil
149282
}
150283

151-
return ips[0], nil
284+
return nil, errors.New("No valid record type selected")
152285
}

0 commit comments

Comments
 (0)