-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathddns
More file actions
executable file
·179 lines (154 loc) · 5.32 KB
/
Copy pathddns
File metadata and controls
executable file
·179 lines (154 loc) · 5.32 KB
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
#!/usr/bin/env python3
import random
import socket
import dns.resolver
import boto3
import click
# import miniupnpc # python3-miniupnpc isn't available on Raspian stable :(
import requests
CONF = {
'aws': {
'credentials_profile': 'ddns'
},
'dns_name': 'h.wooldrige.co.uk.',
'dns_ttl': 60,
'dns_resolvers': [
'8.8.8.8',
'8.8.4.4'
],
'hosted_zone': 'Z1VIBQX07I3Y3K',
'web_services': [
'https://api.ipify.org',
'https://bot.whatismyipaddress.com/',
'https://ifconfig.co/ip',
'https://api.ip.sb/ip'
]
}
def validate_ip(ip):
try:
socket.inet_aton(ip)
except Exception:
click.echo('{0} not recognised as an IP'.format(ip))
raise
"""
# python3-miniupnpc isn't available on Raspian stable :(
def try_upnp():
u = miniupnpc.UPnP(discoverdelay=500)
devices = u.discover()
click.echo(' * Found devices: {0}'.format(devices))
u.selectigd()
click.echo(' * Using IGD: {0} - {1}'.format(u.lanaddr, u.statusinfo()))
ip = u.externalipaddress()
validate_ip(ip)
return ip
"""
def try_webservices():
min_agree = 2
# These web services must respond with a plain text IP
results = []
web_services = CONF['web_services'][:]
# This shuffles in-place :(
random.shuffle(web_services)
for ws in web_services:
try:
click.echo(" * Trying {0}".format(ws))
r = requests.get(ws).text.strip()
validate_ip(r)
click.echo(' - returned {0}'.format(r))
except Exception as e:
click.echo(" * Tried {0} but failed: {1}".format(ws, e), err=True)
results.append(r)
if results.count(r) >= min_agree:
click.echo(' * {0} web services agree on {1} - success'.format(
min_agree, r))
return r
raise Exception('No web services left to try')
def determine_external_ip():
funcs = [
# ('UPnP', try_upnp), # python3-miniupnpc not available on Debian yet
('Web wervices', try_webservices),
# pystun not 3.7 compatible yet :(
]
for name, f in funcs:
click.echo("Trying {0} to find external IP".format(name))
try:
return f()
except Exception as e:
click.echo(" * Could not find addr using {0}: {1}".format(name, e),
err=True)
pass
raise Exception('Could not determine external IP address')
def resolve_dns_name(name):
resolver = dns.resolver.Resolver()
resolver.nameservers = CONF['dns_resolvers']
try:
answer = resolver.query(name)
except Exception as e:
click.echo("Could not resolve {0}".format(name), err=True)
raise
return answer.response.answer[0].items[0].address
def get_current_r53_ip(r53_client):
dn = CONF['dns_name']
rrs = r53_client.list_resource_record_sets(
HostedZoneId=CONF['hosted_zone'],
StartRecordType='A',
StartRecordName=dn
)
rr = [r for r in rrs['ResourceRecordSets'] if r['Name'] == dn][0]
current_ip = rr['ResourceRecords'][0]['Value']
click.echo('Route53 currently thinks IP is {0}'.format(current_ip))
return current_ip
def upsert_r53_record(r53_client, to_ip):
dn = CONF['dns_name']
response = r53_client.change_resource_record_sets(
HostedZoneId=CONF['hosted_zone'],
ChangeBatch={
'Comment': 'Update {0} to {1}'.format(dn, to_ip),
'Changes': [
{
'Action': 'UPSERT',
'ResourceRecordSet': {
'Name': dn,
'Type': 'A',
'TTL': CONF['dns_ttl'],
'ResourceRecords': [{'Value': to_ip }],
}
}
]
}
)
click.echo("Route53 change submitted: {0}".format(response))
change_id = response['ChangeInfo']['Id']
click.echo("Waiting for Route53 to update: {0}".format(change_id))
waiter = r53_client.get_waiter('resource_record_sets_changed')
waiter.wait(
Id=change_id,
WaiterConfig={'Delay': 15, 'MaxAttempts': 120}
)
def update_dns_record(to_ip, dryrun=False, forceupdate=False):
sess = boto3.Session(profile_name=CONF['aws']['credentials_profile'])
r53_client = sess.client('route53')
current_ip = get_current_r53_ip(r53_client)
if (current_ip == to_ip) and not forceupdate:
click.echo("Current Route53 entry already up to date")
return
click.echo("Current Route53 entry {0} does not match {1}".format(
current_ip, to_ip))
if dryrun:
click.echo("Not updating Route53 as this is a dry run")
return
upsert_r53_record(r53_client, to_ip)
click.echo("Route53 update complete")
@click.command()
@click.option('--dryrun', '-d', is_flag=True)
@click.option('--forceupdate', '-f', is_flag=True)
def check_and_update(dryrun, forceupdate):
current_dns_ip = resolve_dns_name(CONF['dns_name'])
click.echo('{0} currently at: {1}'.format(
CONF['dns_name'], current_dns_ip))
current_external_ip = determine_external_ip()
click.echo('Determined external IP: {0}'.format(current_external_ip))
if current_dns_ip != current_external_ip:
update_dns_record(current_external_ip, dryrun, forceupdate)
if __name__ == '__main__':
check_and_update()