1
1
package main
2
2
3
3
import (
4
+ "errors"
4
5
"flag"
5
6
"fmt"
6
7
"io/ioutil"
@@ -9,6 +10,7 @@ import (
9
10
10
11
"github.com/dschanoeh/hover-ddns/hover"
11
12
"github.com/dschanoeh/hover-ddns/publicip"
13
+ "github.com/miekg/dns"
12
14
log "github.com/sirupsen/logrus"
13
15
"gopkg.in/yaml.v2"
14
16
)
@@ -17,9 +19,12 @@ type Config struct {
17
19
Username string
18
20
Password string
19
21
Hostname string
22
+ DisableV4 bool `yaml:"disable_ipv4"`
23
+ DisableV6 bool `yaml:"disable_ipv6"`
20
24
DomainName string `yaml:"domain_name"`
21
25
ForceUpdate bool `yaml:"force_update"`
22
26
PublicIPProvider publicip.LookupProviderConfig `yaml:"public_ip_provider"`
27
+ DNSServer string `yaml:"dns_server"`
23
28
}
24
29
25
30
var (
@@ -30,12 +35,15 @@ var (
30
35
)
31
36
32
37
func main () {
38
+ config := Config {}
33
39
var verbose = flag .Bool ("verbose" , false , "Turns on verbose information on the update process. Otherwise, only errors cause output." )
34
40
var debug = flag .Bool ("debug" , false , "Turns on debug information" )
35
41
var dryRun = flag .Bool ("dry-run" , false , "Perform lookups but don't actually update the DNS info" )
36
42
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" )
38
45
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" )
39
47
40
48
flag .Parse ()
41
49
@@ -52,19 +60,32 @@ func main() {
52
60
log .SetLevel (log .ErrorLevel )
53
61
}
54
62
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
+
55
75
if * configFile == "" {
56
76
log .Error ("Please provide a config file to read" )
57
77
flag .Usage ()
58
78
os .Exit (1 )
59
79
}
60
80
61
- config := Config {}
62
-
63
81
err := loadConfig (* configFile , & config )
64
82
if err != nil {
65
83
log .Error ("Could not load config file: " , err )
66
84
os .Exit (1 )
67
85
}
86
+ if ! validateConfig (& config ) {
87
+ os .Exit (1 )
88
+ }
68
89
69
90
var provider publicip.LookupProvider
70
91
provider , err = publicip .NewLookupProvider (& config .PublicIPProvider )
@@ -73,49 +94,100 @@ func main() {
73
94
os .Exit (1 )
74
95
}
75
96
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 {}
80
99
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
+ }
84
119
}
85
120
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
+ }
90
129
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..." )
94
139
}
140
+ } else {
141
+ publicV4 = nil
95
142
}
96
143
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 ()
99
148
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
+ }
105
153
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 ())
110
155
} 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..." )
112
183
}
113
184
} else {
114
- log . Info ( "IPs differ - update required..." )
185
+ publicV6 = nil
115
186
}
116
187
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 )
119
191
if err != nil {
120
192
os .Exit (1 )
121
193
}
@@ -138,15 +210,76 @@ func loadConfig(filename string, config *Config) error {
138
210
return nil
139
211
}
140
212
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 {
144
259
return nil , err
145
260
}
146
261
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
149
282
}
150
283
151
- return ips [ 0 ], nil
284
+ return nil , errors . New ( "No valid record type selected" )
152
285
}
0 commit comments