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
|
---|
Back to
Craft