forked from dreamins/Route53DDNS-ruby
-
Notifications
You must be signed in to change notification settings - Fork 0
/
route53_ddns.rb
executable file
·241 lines (207 loc) · 7.42 KB
/
route53_ddns.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env ruby
# This script provides a dynamic dns functionality
# for domain names hosted at Amazon Route 53 DNS service
# Launch this with cron each couple of minutes. Keep in mind
# it doesn't make much sense to launch it more frequently than your record TTL
# and author assumes no responsibility if you misuse it and get throttled at Aamazon Route53
# there shall be a file with AWS secrets in JSON format, inspired by dnscurl to hide your secrets
# from command line
# ex:
#{
# "access_key" : "SOME_NON_SECRET",
# "secret_key" : "SOME_SECRET"
#}
# launch smth like
# ./route53_ddns.rb --secrets-file /path/.r53_secrets --hosted-zone [your hosted zone id] --random-sleep
require 'rubygems'
require 'bundler/setup'
# required to do requests to external servers to figure out external IP address
# http://curb.rubyforge.org/
# you might need to install this with gem install curb
require 'curb'
# JSON parser for one of ip_providers and route53 secrets
# you might need to install this with gem install json
require 'json'
require 'optparse'
require 'ostruct'
# Many thanks to
# https://github.com/pcorliss/ruby_route_53
# install with gem install route53
require 'route53'
# Route53 endpoint
$ENDPOINT = 'https://route53.amazonaws.com/'
$API_VERSION = '2012-02-29'
$CONNECT_TIMEOUT_SEC = 3
def get_cli_options args
options = OpenStruct.new
options.secrets_file = ""
options.hosted_zone = ""
options.sleep = false
options.subdomain = ""
opts = OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.on("-s", "--secrets-file [FILENAME]", "AWS access and secret key locations") do
|val| options.secrets_file = val
end
opts.on("-z", "--hosted-zone [HZID]", "Route53 hosted zone id") do
|val| options.hosted_zone = val
end
opts.on("-d", "--subdomain [SUBDOMAIN]", "A record subdomain. If not specified, assumes a single A record in the zone") do
|val| options.subdomain = val
end
opts.on("-b", "--[no-]random-sleep", "Random sleep of up to 1 minute enabled") do
|val| options.sleep = val
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit 0
end
end
begin
opts.parse!(args)
rescue
puts "Cannot parse input parameters"
puts opts
exit 1
end
[ options.secrets_file, options.hosted_zone ].each do |x|
if x.empty?
puts opts
exit 1
end
end
options
end
# if you want to run internal DNS just replace this function with something like
# required to figure out local IP address, one can use info returned form /sbin/ficonfig as well
# example code taken from http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
# require 'socket'
# def get_my_ip
# orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
#
# UDPSocket.open do |s|
# s.connect 'example.com', 1
# s.addr.last
# end
# ensure
# Socket.do_not_reverse_lookup = orig
# end
# define a bunch of services, that provide you with IP
# among with a function, that will help to extract it
# Amazon AWS one shall be enough though
def get_my_ip
ip_providers = [
{
'url' => 'http://www.strewth.org/ip.php',
'method' => lambda { |x| JSON.parse(x)['ipaddress']; },
'validate' => lambda { |x| JSON.parse(x).has_key?('ipaddress') }
},
{
'url' => 'http://checkip.amazonaws.com/',
'method' => lambda { |x| x },
'validate' => lambda { |x| x =~ /^([\d]{1,3}\.){3}[\d]{1,3}$/ }
}
].shuffle
# choose a random ip provider, then iterate ahead from it
ip_good = false
my_ip = nil
ip_providers.each do |provider|
puts "Polling #{provider['url']}"
# do a request
begin
curl = Curl::Easy.new(provider['url'])
curl.timeout = $CONNECT_TIMEOUT_SEC
data = curl.http(:GET)
response = curl.body_str
if provider['validate'].call(response)
my_ip = provider['method'].call(response)
if my_ip =~ /^([\d]{1,3}\.){3}[\d]{1,3}$/
ip_good = true
break
else
warn "Result is not a dotted quad IP"
end
else
warn "Bad response from IP lookup server. Retrying"
end
rescue => e
# assuming first two lines won't throw
warn "Error encountered during http request. " + e.inspect
end
end
if not ip_good
puts "Cannot get current IP from any of external services."
exit 1
end
my_ip.strip
end
def get_A_record (r53, hzid, subdomain)
zones = r53.get_zones
# /hostedzone/[HZID]
the_zone = zones.select { |zone| zone.host_url.split('/')[2] == hzid }
if the_zone.nil? or the_zone.size != 1
puts "Cannot find hosted zone"
exit 1
end
records = the_zone[0].get_records('A')
arecord = ""
if (subdomain.length > 0)
#try to find the A record with the subdomain specified
subrecs = records.select { |record| record.name.start_with? subdomain }
if (subrecs.size() == 0)
puts "A record with name #{subdomain} was not found"
exit 1
elsif (subrecs.size() > 1)
puts "It is assumed that only one A record with name #{subdomain} exists to update"
exit 1
else
arecord = subrecs[0]
end
else
# Assume that there is only one A record in the zone to update
if (records.size() != 1)
puts "It is assumed that only one A record exists in HZ to update"
exit 1
end
arecord = records[0]
end
arecord
end
# Route53 is authoritative source of domain name
# anythig else is just a cache, that might become stale
# or prone to invalidation issues. One request per 5 minutes shall
# not be a problem
def get_previous_ip(r53, hzid, subdomain)
get_A_record(r53,hzid,subdomain).values[0]
end
def update_ip (r53, hzid, subdomain, ip)
get_A_record(r53, hzid, subdomain).update(nil, nil, nil, [ip])
end
options = get_cli_options(ARGV)
# sleep for <60 secs to try to distribute load on Route 53 in case
# if script is too popular see also http://www.stdlib.net/~colmmacc/2009/09/14/period-pain/
if options.sleep
require 'zlib'
require 'socket'
# take hash of hostname, which is supposed to be more or less different
hash = Zlib.crc32(Socket.gethostname,0).to_i
# shall we relax a bit and don't care much about bias?
sleep_secs = hash % 60
puts "Sleeping for #{sleep_secs} seconds before update"
sleep(sleep_secs)
end
my_ip = get_my_ip
puts "IP is #{my_ip}"
# get secrets file
secrets = JSON.parse(File.read(options.secrets_file))
# send update batch assuming only one zone for account for now
r53 = Route53::Connection.new(secrets["access_key"], secrets["secret_key"], $API_VERSION, $ENDPOINT)
previous_ip = get_previous_ip(r53, options.hosted_zone, options.subdomain)
puts "IP was #{previous_ip}"
if previous_ip == my_ip
puts "Nothing to do."
exit 0
end
puts "Updating ip with Route53"
update_ip(r53, options.hosted_zone, options.subdomain, my_ip)
puts "Done"