Beefy Boxes and Bandwidth Generously Provided by pair Networks
"be consistent"
 
PerlMonks  

Update reverse DNS zone files by scanning the forward zone files

by araqnid (Beadle)
on Sep 12, 2000 at 02:03 UTC ( [id://31999]=perlcraft: print w/replies, xml ) Need Help??

   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

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others pondering the Monastery: (6)
As of 2024-04-23 13:40 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found