http://qs321.pair.com?node_id=31999

   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