@@ -9,44 +9,50 @@ import traceback
9
9
10
10
from blessings import Terminal
11
11
from random import randint
12
- from time import sleep
12
+ from time import sleep , strftime
13
13
14
14
# TODO: Package properly
15
- VERSION = 'v1.1 '
15
+ VERSION = 'release-1.2 '
16
16
17
- def header (ns1_name , ns2_name ):
17
+ def header (ns1_name , ns1_port , ns2_name , ns2_port ):
18
18
global seen_header
19
19
20
+ std_port = socket .getservbyname ('domain' )
21
+ t_stamp = strftime ('%Y-%m-%d %H:%M:%S %z' )
22
+
23
+ # http://superuser.com/questions/710019/
20
24
if not seen_header :
21
- print ("--- %s\n +++ %s" % (ns1_name , ns2_name ))
22
- seen_header = True
25
+ if ns1_port != std_port or ns2_port != std_port :
26
+ print ("--- %s:%s\t %s\n +++ %s:%s\t \t %s" %
27
+ (ns1_name , ns1_port , t_stamp , ns2_name , ns2_port , t_stamp ))
28
+ else :
29
+ print ("--- %s\t %s\n +++ %s\t \t %s" %
30
+ (ns1_name , t_stamp , ns2_name , t_stamp ))
23
31
24
- def added (answer , color = False ):
25
- # answer contains multiple records and does not end in newline
26
- answer = '+' + str (answer [0 ]).replace ('\n ' , '\n +' )
32
+ seen_header = True
27
33
28
- if color :
29
- print (term .green + answer + term .normal )
30
- else :
31
- print (answer )
34
+ def added (answer , color = False ):
35
+ for rrset in answer :
36
+ if color :
37
+ print (term .green + '+' + rrset + term .normal )
38
+ else :
39
+ print ('+' + rrset )
32
40
33
41
def removed (answer , color = False ):
34
- # answer contains multiple records and does not end in newline
35
- answer = '-' + str (answer [0 ]).replace ('\n ' , '\n -' )
36
-
37
- if color :
38
- print (term .red + answer + term .normal )
39
- else :
40
- print (answer )
42
+ for rrset in answer :
43
+ if color :
44
+ print (term .red + '-' + rrset + term .normal )
45
+ else :
46
+ print ('-' + rrset )
41
47
42
- def get_dns_response (query , ns_addr , ns_name , rname , rdtype ):
48
+ def get_response (query , ns_addr , ns_name , rname , rdtype , port ):
43
49
response = None
44
50
successful = False
45
51
t = 2
46
52
47
53
while not successful :
48
54
try :
49
- response = dns .query .udp (query , ns_addr , timeout = t )
55
+ response = dns .query .udp (query , ns_addr , timeout = t , port = port )
50
56
successful = True
51
57
except dns .exception .Timeout :
52
58
# Retry with backoff
@@ -58,6 +64,32 @@ def get_dns_response(query, ns_addr, ns_name, rname, rdtype):
58
64
59
65
return response
60
66
67
+ def extract_rrset (response , ns_name , rdtype , ignore_ttl = False ):
68
+ # TODO: handle SERVFAIL, REFUSED, NOTIMP, etc?
69
+ # Return only well-undestood cases so that this fails loudly
70
+
71
+ # Successful query response
72
+ if (response .rcode () == dns .rcode .NOERROR
73
+ and response .answer ):
74
+ if ignore_ttl :
75
+ response .answer [0 ].ttl = 0
76
+ return sorted (str (response .answer [0 ]).split ('\n ' ))
77
+
78
+ # rrsets for NS records will be in the AUTHORITY section of the dns.message
79
+ # object instead of the ANSWER section
80
+ if (rdtype == 'NS'
81
+ and response .rcode () == dns .rcode .NOERROR
82
+ and not response .answer
83
+ and not ns_name in response .authority [0 ]):
84
+ if ignore_ttl :
85
+ response .authority [0 ].ttl = 0
86
+ return sorted (str (response .authority [0 ]).split ('\n ' ))
87
+
88
+ # AWS Alias record with NXDOMAIN target
89
+ if (response .rcode () == dns .rcode .NOERROR
90
+ and not response .answer ):
91
+ return None
92
+
61
93
def main ():
62
94
progname = os .path .basename (sys .argv [0 ])
63
95
parser = argparse .ArgumentParser (prog = progname )
@@ -73,26 +105,22 @@ def main():
73
105
metavar = 'SECONDS' ,
74
106
help = "maximum number of seconds of delay to introduce" +
75
107
" between each request" )
76
- '''
77
- # TODO: exclusions
78
- parser.add_argument("-n", "--ignore-ns", action="store_true",
79
- help="Ignore changes to NS records at the apex")
80
- parser.add_argument("-s", "--ignore-soa", action="store_true",
81
- help="Ignore changes to SOA records")
82
- parser.add_argument("-t", "--ignore-ttl", action="store_true",
83
- help="Ignore changes to TTL values")
84
- '''
108
+
109
+ parser .add_argument ("-t" , "--ignore-ttl" , dest = "ttl_flag" ,
110
+ action = "store_true" ,
111
+ help = "ignore changes to TTL values" )
112
+
85
113
parser .add_argument ("-f" , "--zonefile" , dest = "filename" , required = True ,
86
- help = "FILENAME is expected to be a valid zone master " +
87
- " file \n " +
114
+ help = "FILENAME is expected to be a valid " +
115
+ "zonefile exported from NAMESERVER1 \n " +
88
116
"https://tools.ietf.org/html/rfc1035#section-5" )
89
117
90
118
parser .add_argument ("--from-ns" , dest = "ns1_name" , required = True ,
91
- metavar = "NAMESERVER1" ,
119
+ metavar = "NAMESERVER1[:PORT] " ,
92
120
help = "compare responses to NAMESERVER2" )
93
121
94
122
parser .add_argument ("--to-ns" , dest = "ns2_name" , required = True ,
95
- metavar = "NAMESERVER2" ,
123
+ metavar = "NAMESERVER2[:PORT] " ,
96
124
help = "compare responses to NAMESERVER1" )
97
125
98
126
args = parser .parse_args ()
@@ -104,13 +132,24 @@ def main():
104
132
try :
105
133
zone = dns .zone .from_file (args .filename , allow_include = False ,
106
134
relativize = False )
107
-
108
135
except dns .exception .DNSException :
109
136
sys .stderr .write ("%s: Unable to import %s.\n " %
110
137
(progname , args .filename ))
111
138
traceback .print_exc (file = sys .stderr )
112
139
sys .exit (1 )
113
140
141
+ # Default port
142
+ ns1_port = ns2_port = 53
143
+
144
+ # Support for alternate port
145
+ if ':' in args .ns1_name :
146
+ ns1_port = int (args .ns1_name .split (':' )[1 ])
147
+ args .ns1_name = args .ns1_name .split (':' )[0 ]
148
+
149
+ if ':' in args .ns2_name :
150
+ ns2_port = int (args .ns2_name .split (':' )[1 ])
151
+ args .ns2_name = args .ns2_name .split (':' )[0 ]
152
+
114
153
# TODO: IPv6 support with socket.getaddrinfo()
115
154
ns1_addr = socket .gethostbyname (args .ns1_name )
116
155
ns2_addr = socket .gethostbyname (args .ns2_name )
@@ -122,63 +161,57 @@ def main():
122
161
rdtype = dns .rdatatype .to_text (rdataset .rdtype )
123
162
query = dns .message .make_query (rname , rdtype )
124
163
125
- # Attempt to evade rate limits
126
- if args .delay_max :
127
- sleep (randint (0 , args .delay_max ))
164
+ # TODO: Improve accuracy for response pools. Execute each query
165
+ # multiple times until most/all records are exposed and gathered.
128
166
129
- r1 = get_dns_response (query , ns1_addr , args .ns1_name , rname , rdtype )
130
- if not r1 :
131
- sys .stderr .write ("%s: SERVFAIL from %s for: %s %s\n " %
132
- (progname , args .ns1_name , rname , rdtype ))
167
+ r1 = get_response (query , ns1_addr , args .ns1_name , rname , rdtype , ns1_port )
133
168
134
- sys .stderr .write ("Try using or increasing --delay-max\n " %
135
- (progname , args .ns1_name , rname , rdtype ))
169
+ if not r1 :
170
+ sys .stderr .write ("%s: Connection timed out to: '%s'\n " %
171
+ (progname , args .ns1_name ))
172
+ sys .stderr .write ("Try using or increasing --delay-max\n " )
136
173
sys .exit (1 )
137
174
138
- r2 = get_dns_response (query , ns2_addr , args .ns2_name , rname , rdtype )
139
- if not r2 :
140
- sys .stderr .write ("%s: SERVFAIL from %s for: %s %s\n " %
141
- (progname , args .ns2_name , rname , rdtype ))
175
+ r2 = get_response (query , ns2_addr , args .ns2_name , rname , rdtype , ns2_port )
142
176
143
- sys .stderr .write ("Try using or increasing --delay-max\n " %
144
- (progname , args .ns2_name , rname , rdtype ))
177
+ if not r2 :
178
+ sys .stderr .write ("%s: Connection timed out to: '%s'\n " %
179
+ (progname , args .ns2_name ))
180
+ sys .stderr .write ("Try using or increasing --delay-max\n " )
145
181
sys .exit (1 )
146
182
147
- # TODO: Fix accuracy in case of round-robin pool. Execute each query
148
- # multiple times until most/all records are exposed. --hard, --hunt,
149
- # or --pool option?
150
-
151
- # BUG: .answer is an empty list if NXDOMAIN or NS record. NS record
152
- # will always have empty .answer, check for existance of .authority
153
- # instead, search for --from-ns if necessary
154
-
155
- # BUG: cache and sort .answer to avoid false positive for round-robin
156
- # response
157
-
158
- if r1 .answer and r2 .answer :
159
- # Answers from both servers, now compare records
160
- if r1 .answer [0 ] != r2 .answer [0 ]:
161
- header (args .ns1_name , args .ns2_name )
162
- removed (r1 .answer , color = args .color_flag )
163
- added (r2 .answer , color = args .color_flag )
183
+ # FIXME: Please report any issues with answer extraction
184
+ a1 = extract_rrset (r1 , args .ns1_name , rdtype , ignore_ttl = args .ttl_flag )
185
+ a2 = extract_rrset (r2 , args .ns2_name , rdtype , ignore_ttl = args .ttl_flag )
186
+
187
+ if a1 and a2 :
188
+ # Answers from both servers, compare rrsets
189
+ if a1 != a2 :
190
+ header (args .ns1_name , ns1_port , args .ns2_name , ns2_port )
191
+ removed (a1 , color = args .color_flag )
192
+ added (a2 , color = args .color_flag )
164
193
# else:
165
194
# # Records both exist and match
166
195
# pass
167
- elif r1 . answer and not r2 . answer :
168
- # exists in r1 but NXDOMAIN in r2
169
- header (args .ns1_name , args .ns2_name )
170
- removed (r1 . answer , color = args .color_flag )
171
- elif r2 . answer and not r1 . answer :
172
- # exists in r2 but NXDOMAIN in r1
173
- header (args .ns1_name , args .ns2_name )
174
- added (r2 . answer , color = args .color_flag )
196
+ elif a1 and not a2 :
197
+ # Added to ns1 removed from ns2
198
+ header (args .ns1_name , ns1_port , args .ns2_name , ns2_port )
199
+ removed (a1 , color = args .color_flag )
200
+ elif a2 and not a1 :
201
+ # Added to ns2 removed from ns1
202
+ header (args .ns1_name , ns1_port , args .ns2_name , ns2_port )
203
+ added (a2 , color = args .color_flag )
175
204
else :
176
- # exists in zonefile but NXDOMAIN on both servers
177
- sys .stderr .write ("%s: NXDOMAIN from both nameservers for: %s %s \n " %
178
- (progname , rname , rdtype ))
205
+ # Exists in zonefile but not on either server
206
+ sys .stderr .write ("%s: Record \" %s \" exists in zonefile but not on either server. \n " %
207
+ (progname , query . question [ 0 ] ))
179
208
sys .stderr .write ("Export zonefile from %s and try again\n " %
180
209
args .ns1_name )
181
210
sys .exit (1 )
182
211
212
+ # Attempt to evade rate limits
213
+ if args .delay_max :
214
+ sleep (randint (0 , args .delay_max ))
215
+
183
216
if __name__ == '__main__' :
184
217
main ()
0 commit comments