1: #!/usr/bin/perl -w
2:
3: # Copyright (C) Steven Haslam 2000
4: # This is free software, distributable under the same terms as Perl
5: # itself - see the Perl source distribution for details.
6:
7: # Generate reverse DNS zone files from forward zone files.
8: # e.g.:
9: # make_reverse_zones db.london.excite.com
10: # --> updates db.194.216.238
11:
12: require 5;
13: use strict;
14: use IO::File;
15:
16: sub read_zonefile {
17: my $filename = shift;
18: my $zoneobj = shift;
19:
20: my $stream = IO::File->new($filename) or die "Unable to read $filename: $!\n";
21: my $origin;
22:
23: my $line = 0;
24: my $current;
25:
26: #print "$filename:debug: reading zone\n";
27:
28: while ($_ = $stream->getline) {
29: ++$line;
30: if (/^\$(\S+)\s+(.+)/) {
31: my($keyword, $data) = (uc($1), $2);
32: if ($keyword eq 'ORIGIN') {
33: $origin = $data;
34: #print "$filename:$line:debug: setting ORIGIN to \"$origin\"\n";
35: }
36: elsif ($keyword eq 'TTL') {
37: next;
38: }
39: else {
40: warn "$filename:$line:warning: unknown directive \"\$$keyword\"\n";
41: }
42: }
43: my @tokens = split(/\s+/);
44: next unless (@tokens);
45: my $domain = shift @tokens;
46: if ($domain eq '@') {
47: #print "$filename:$line:debug: Using origin ($origin)\n";
48: $current = $origin;
49: shift @tokens;
50: }
51: elsif ($domain eq '') {
52: #print "$filename:$line:debug: Sticking with current domain ($current)\n";
53: }
54: else {
55: if ($domain =~ /\.$/) {
56: $current = $domain;
57: }
58: else {
59: # Error to not have passed a $ORIGIN statement at this point
60: if (!defined($origin)) {
61: die "$filename:$line: No \$ORIGIN encountered by this point\n";
62: }
63: # Skip "localhost" entries.
64: next if (lc($domain) eq 'localhost');
65: $current = "$domain.$origin";
66: }
67: }
68: if ($tokens[0] eq 'IN') {
69: shift @tokens;
70: }
71: my $type = uc(shift @tokens);
72: # Only interested in A types
73: # But SOA types need special handling for this hacked-together parser
74: # For later: AAAA types
75: if ($type eq 'SOA') {
76: while (!/\)/) {
77: $_ = $stream->getline;
78: ++$line;
79: }
80: next;
81: }
82: elsif ($type ne 'A') {
83: next;
84: }
85: my $ipaddr = shift @tokens;
86: my $restofline = join(' ', @tokens);
87: if ($restofline =~ /;.*:norev:/i) {
88: next; # Admin said to skip this line
89: }
90: #print "$filename:$line:debug: $current $ipaddr\n";
91: if ($ipaddr !~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
92: warn "$filename:$line:warning: Bad IP address \"$ipaddr\"\n";
93: next;
94: }
95: # "What's the point of this?" - eradicate any variations in formatting
96: # that might have slipped through the regex above- leading zeroes being
97: # an example
98: $ipaddr = sprintf("%d.%d.%d.%d", $1, $2, $3, $4);
99: if (exists($$zoneobj{$ipaddr})) {
100: warn "$filename:$line:warning: IP address \"$ipaddr\" already used ($$zoneobj{$ipaddr})- ignoring \"$current IN A $ipaddr\"\n";
101: next;
102: }
103: $$zoneobj{$ipaddr} = $current;
104: }
105:
106: $stream->close;
107: }
108:
109: sub bump_serial {
110: my $oldserial = shift;
111: my($sec,$min,$hour,$mday,$mon,$year) = gmtime(time);
112: my $newserial = sprintf("%04d%02d%02d%02d", $year + 1900, $mon + 1, $mday, 1);
113: if (($newserial + 100) < $oldserial) {
114: die "Unable to bump old serial number ($oldserial ==> $newserial): something's broken\n";
115: }
116: while ($newserial <= $oldserial) {
117: ++$newserial;
118: }
119: return $newserial;
120: }
121:
122: sub update_revzonefile {
123: my $filename = shift;
124: my $nodes = shift;
125: my $tempfilename = "$filename.$$.tmp";
126:
127: my $instream = IO::File->new($filename);
128:
129: # Open like this because we're likely to run as root and our
130: # tempfile naming scheme isn't really very safe
131: my $outstream = IO::File->new($tempfilename, O_WRONLY|O_CREAT|O_EXCL);
132:
133: my $found_serial = 0;
134:
135: my $updated = 0;
136:
137: my %foundoldnodes;
138:
139: while ($_ = $instream->getline) {
140: # REQUIRE the serial number to be recognisable as:
141: # 2000091101 ; Serial number
142: if (s/(\d+)(\s+; Serial number)/&bump_serial($1).$2/e) {
143: $found_serial++;
144: $outstream->print($_);
145: next;
146: }
147: elsif (/^(\d+)\s+(IN\s+)?PTR\s+(\S+)$/) {
148: # Found a reverse entry
149: my($oldnode, $oldhost) = ($1, $3);
150: #print "debug: old-reverse: $oldnode = $oldhost\n";
151: $foundoldnodes{$oldnode} = 1;
152:
153: # Has it changed?
154:
155: # Override: if the admin says keep it, they know what they're doing :}
156: if (/;.*:keep:/) {
157: $outstream->print($_);
158: next;
159: }
160:
161: if (!exists($$nodes{$oldnode})) {
162: #print "debug: $oldnode is to be removed\n";
163: $updated = 1;
164: }
165: elsif (lc($$nodes{$oldnode}) ne lc($oldhost)) {
166: #print "debug: data for $oldnode has changed ($oldhost ==> $$nodes{$oldnode})\n";
167: $updated = 1;
168: }
169: next; # Filter out these lines...
170: }
171: $outstream->print($_);
172: }
173:
174: while (my($node, $host) = each %$nodes) {
175: if (!$foundoldnodes{$node}) {
176: #print "debug: $node is new\n";
177: $updated = 1;
178: }
179: $outstream->print("$node\tIN\tPTR\t$host\n");
180: }
181:
182: $instream->close;
183: $outstream->close;
184:
185: if ($updated) {
186: if ($found_serial) {
187: print " Updating $filename\n";
188: rename($tempfilename, $filename) or warn "rename($tempfilename, $filename): $!\n";
189: }
190: else {
191: print " Unable to update $filename: no serial number found\n";
192: }
193: }
194: else {
195: print " No changes.\n";
196: unlink($tempfilename) or warn "Unable to remove temp file (\"$tempfilename\"): $!\n";
197: }
198: }
199:
200: use vars qw(%addrs %nets);
201:
202: if (!@ARGV) {
203: die <<EOF;
204: Syntax: $0 forward-zonefile...
205:
206: This script will scan the DNS zone files named on the command line,
207: and update reverse zone files as necessary.
208:
209: For more details, see the POD documentation.
210:
211: EOF
212: }
213:
214: foreach (@ARGV) {
215: read_zonefile($_, \%addrs);
216: }
217:
218: # OK, now have ip-addr => hostname mapping. So bin all the hosts into /24s
219:
220: while (my($ipaddr, $domain) = each %addrs) {
221: my($net, $node) = ($ipaddr =~ /^(\d+\.\d+\.\d+)\.(\d+)$/);
222: if (!defined($net) || !defined($node)) {
223: die "Hm, regexp failed on $ipaddr: this REALLY shouldn't happen!\n";
224: }
225: $nets{$net}->{$node} = $domain;
226: }
227:
228: # For each /24, update the zone file as applicable
229:
230: while (my($net, $nodes) = each %nets) {
231: my $filename = "db.$net";
232: if (! -f $filename) {
233: print "*** Zone file for $net/24 ($filename) does not exist\n";
234: }
235: else {
236: print "Processing $net/24...\n";
237: update_revzonefile($filename, $nodes);
238: }
239: }
240:
241: =head1 NAME
242:
243: make_reverse_zones - Update reverse DNS zone files from the forward DNS zone files
244:
245: =head1 SYNOPSIS
246:
247: make_reverse_zones forward_zonefile...
248:
249: =head1 DESCRIPTION
250:
251: Reads the forward DNS zone files named on the command line and uses
252: them to update reverse DNS zone files. Warnings will be emitted when
253: two domains are specified to have the same IP address- this can be
254: overridden in the zone file when necessary.
255:
256: The forward zone files may be named in any fashion. The reverse zone
257: files B<must> be named as C<db.NNN.NNN.NNN> where each NNN is an IP
258: address component. This program only supports generating reverse zones
259: in /24 blocks.
260:
261: If the reverse zone file does not already exist, it is B<not>
262: created. This program cannot determine the correct information to put
263: in the SOA/NS records- create a "blank" reverse zone file yourself and
264: rerun this program.
265:
266: =head2 Syntax of the forward file
267:
268: Currently, this program does not handle entries with TTLs specified.
269:
270: The basic entry looked for is of the form:
271:
272: domain IN A 172.18.1.2 ; comments...
273:
274: CNAME etc. records are discarded. Entries where the domain is
275: "localhost" are discarded. The C<$ORIGIN> directive is respected- and
276: is required unless every domain in the zone file is fully-qualified.
277:
278: If the "comments..." section contains ":norev:" then the line is
279: ignored. This allows you to override the reverse DNS generation when
280: you know what you're doing (e.g. for round-robin DNS entries).
281:
282: =head2 Syntax of the reverse file
283:
284: The reverse file B<must> have a serial number line looking like this:
285:
286: 2000110901 ; Serial number
287:
288: The comment B<is> required.
289:
290: When processing the reverse file, all existing "IN PTR" records are
291: removed. However, you can make the program leave them alone by putting
292: ":keep:" in a comment. This is useful if there are some addresses in
293: your reverse domain that you do not have the forward zone files for.
294:
295: =head1 EXAMPLE
296:
297: bash$ ./make_reverse_zones db.london.excite.com
298: Processing 194.216.238/24...
299: Updating db.194.216.238
300:
301: =head1 BUGS
302:
303: The zone file parsers are janky. Particularly the reverse zone file
304: reader's requirement for identifying the serial number, and the
305: forward file reader's failure to recognise TTL values.
306:
307: IPv6 not supported.
308:
309: =head1 AUTHOR
310:
311: Steve Haslam <araqnid@debian.org>
312:
313: =cut