-
Notifications
You must be signed in to change notification settings - Fork 27
/
lambda_function.py
231 lines (206 loc) · 10.2 KB
/
lambda_function.py
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
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may not
# use this file except in compliance with the License. A copy of the License is
# located at
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.
# -*- coding: utf-8 -*-
#
# lambda_function.py
#
# AWS Lambda function to mirror an on-premises DNS to Route 53 private hosted zone
# Supports both forward and reverse zones for replications to Route 53.
# These imports are bundled local to the lambda function
import dns.query
import dns.zone
import lookup_rdtype
from dns.rdataclass import *
from dns.rdatatype import *
# libraries that are available on Lambda
import os
import sys
import boto3
# If you need to use a proxy server to access the Internet then hard code it
# the details below, otherwise comment out or remove.
#os.environ["http_proxy"] = "10.10.10.10:3128" # My on-premises proxy server
#os.environ["https_proxy"] = "10.10.10.10:3128"
#os.environ["no_proxy"] = "169.254.169.254" # Don't proxy for meta-data service as Lambda needs to get IAM credentials
# setup the boto3 client to talk to AWS APIs
route53 = boto3.client('route53')
# Function to create, update, delete records in Route 53
def update_resource_record(zone_id, host_name, hosted_zone_name, rectype, changerec, ttl, action):
if not (rectype == 'NS' and host_name == '@'):
print 'Updating as %s for %s record %s TTL %s in zone %s with %s ' % (
action, rectype, host_name, ttl, hosted_zone_name, changerec)
if rectype != 'SOA':
if host_name == '@':
host_name = ''
elif host_name[-1] != '.':
host_name += '.'
# Make Route 53 change record set API call
dns_changes = {
'Comment': 'Managed by Lambda Mirror DNS',
'Changes': [
{
'Action': action,
'ResourceRecordSet': {
'Name': host_name + hosted_zone_name,
'Type': rectype,
'ResourceRecords': [],
'TTL': ttl
}
}
]
}
for value in changerec: # Build the recordset
if (rectype != 'CNAME' and rectype != 'SRV' and rectype != 'MX' and rectype!= 'NS') or (str(value)[-1] == '.'):
dns_changes['Changes'][0]['ResourceRecordSet']['ResourceRecords'].append({'Value': str(value)})
else:
dns_changes['Changes'][0]['ResourceRecordSet']['ResourceRecords'].append({'Value': str(value) + '.' + hosted_zone_name + '.'})
try: # Submit API request to Route 53
route53.change_resource_record_sets(HostedZoneId=zone_id, ChangeBatch=dns_changes)
except BaseException as e:
print e
sys.exit('ERROR: Unable to update zone %s' % hosted_zone_name)
return True
# Perform a diff against the two zones and return difference set
def diff_zones(zone1, zone2, ignore_ttl):
differences = []
for node in zone1: # Process delete for records which are not in the new zone
node1 = zone1.get_node(node)
node2 = zone2.get_node(node)
if not node2:
for record1 in node1:
changerec = []
for value1 in record1:
changerec.append(value1)
change = (str(node), record1.rdtype, changerec, record1.ttl, 'DELETE')
if change not in differences:
differences.append(change)
else:
for record1 in node1:
record2 = node2.get_rdataset(record1.rdclass, record1.rdtype)
if record1 != record2: # update record to new zone
changerec = []
if record2:
action = 'UPSERT'
for value2 in record2:
changerec.append(value2)
else:
action = 'DELETE'
for value1 in record1:
changerec.append(value1)
change = (str(node), record1.rdtype, changerec, record1.ttl, action)
if change and change not in differences:
differences.append(change)
for node in zone2: # Process records in master zone
node1 = zone1.get_node(node)
node2 = zone2.get_node(node)
if not node1: # Entry doesn't exist, lets create it
for record2 in node2:
changerec = []
for value2 in record2:
changerec.append(value2)
change = (str(node), record2.rdtype, changerec, record2.ttl, 'CREATE')
if change and change not in differences:
differences.append(change)
else: # Check for updates to record for existing entries
for record2 in node2:
record1 = node1.get_rdataset(record2.rdclass, record2.rdtype)
if record2.rdtype == dns.rdatatype.SOA:
continue
elif not record1: # Create new record
changerec = []
for value2 in record2:
changerec.append(value2)
change = (str(node), record2.rdtype, changerec, record2.ttl, 'UPSERT')
if change and change not in differences:
differences.append(change)
elif record1 != record2: # update record to new zone
changerec = []
for value2 in record2:
changerec.append(value2)
change = (str(node), record2.rdtype, changerec, record2.ttl, 'UPSERT')
if change and change not in differences:
differences.append(change)
if record2.rdtype == dns.rdatatype.SOA or not record1:
continue
elif not ignore_ttl and record2.ttl != record1.ttl: # Check if the TTL has been updated
changerec = []
for value2 in record2:
changerec.append(value2)
change = (str(node), record2.rdtype, changerec, record2.ttl, 'UPSERT')
if change and change not in differences:
differences.append(change)
elif record2.ttl != record1.ttl:
print 'Ignoring TTL update for %s' % node
return differences
# Main Handler for lambda function
def lambda_handler(event, context):
# Setup configuration based on JSON formatted event data
try:
domain_name = event['Domain']
master_ip = event['MasterDns']
route53_zone_id = event['ZoneId']
if event['IgnoreTTL'] == 'True':
ignore_ttl = True # Ignore TTL changes in records
else:
ignore_ttl = False # Update records even if the change is just the TTL
except BaseException as e:
print 'Error in setting up the environment, exiting now (%s) ' % e
sys.exit('ERROR: check JSON file is complete:', event)
# Transfer the master zone file from the DNS server via AXFR
print 'Transferring zone %s from server %s ' % (domain_name, master_ip)
try:
master_zone = dns.zone.from_xfr(dns.query.xfr(master_ip, domain_name))
except BaseException as e:
print 'Problem transferring zone'
print e
sys.exit('ERROR: Unable to retrieve zone %s from %s' % (domain_name, master_ip))
soa = master_zone.get_rdataset('@', 'SOA')
serial = soa[0].serial # What's the current zone version on-prem
# Read the zone from Route 53 via API and populate into zone object
vpc_zone = dns.zone.Zone(origin=domain_name)
print 'Getting VPC SOA serial from Route 53' # Get the SOA from Route 53 by API to avoid getting stale records
try:
vpc_recordset = route53.list_resource_record_sets(HostedZoneId=route53_zone_id)['ResourceRecordSets']
for record in vpc_recordset:
# Change the record name so that it doesn't have the domain name appended
recordname = record['Name'].replace(domain_name + '.', '')
if recordname == '':
recordname = '@'
else:
recordname = recordname.rstrip('.')
rdataset = vpc_zone.find_rdataset(recordname, rdtype=str(record['Type']), create=True)
for value in record['ResourceRecords']:
rdata = dns.rdata.from_text(1, rdataset.rdtype, value['Value'].replace(domain_name + '.', ''))
rdataset.add(rdata, ttl=int(record['TTL']))
except BaseException as e:
print e
sys.exit('ERROR: Unable to retrieve VPC Zone via API (%s)' % e)
# Compare the master and VPC Route 53 zone file
vpc_soa = vpc_zone.get_rdataset('@', 'SOA')
vpc_serial = vpc_soa[0].serial
if not (vpc_serial > serial):
print 'Comparing SOA serial %s with %s ' % (vpc_serial, serial)
differences = diff_zones(vpc_zone, master_zone, ignore_ttl)
for host, rdtype, record, ttl, action in differences:
if rdtype != dns.rdatatype.SOA:
update_resource_record(route53_zone_id, host, domain_name, lookup_rdtype.recmap(rdtype), record, ttl,
action)
# Update the VPC SOA to reflect the version just processed
vpc_soa[0].serial = serial
try:
soarecord = [str(vpc_soa[0])]
update_resource_record(route53_zone_id, '', domain_name, 'SOA', soarecord, vpc_soa[0].minimum, 'UPSERT')
except BaseException as e:
print e
sys.exit('ERROR: Failed to update SOA to %s on Route 53 VPC Zone' % str(serial))
else:
sys.exit('ERROR: Route 53 VPC serial %s for domain %s is greater than existing serial %s' % (str(vpc_serial), domain_name, str(serial)))
return 'SUCCESS: %s mirrored to Route 53 VPC serial %s' % (domain_name, str(serial))