#!/usr/bin/perl #-------------------------------------------------------------------------- # Copyright 1999 # See the file LICENSE.txt # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA #-------------------------------------------------------------------------- # # this is the first public release, I think it's useful but you might # not... We'll call it version 0.2 # # some concepts and formatting of a zone file go to Miles Lott of # mkdnstab.pl fame... # # I was really surprised that no one had put something like this up on # freshmeat yet. So I will... # # Cheers! use strict; use Getopt::Long; use vars qw / %opt %def %hosts %cnames %mx %mxprefs %reverse %addr_arpa $serial /; use vars qw / $www $mail $mailpref $mail2 $mailpref2 $origin $mailaddr $ns $ns2 $refresh $retry $expire $minimum $master $slave $fh $file $host $ip $mx /; GetOptions( 'www=s', \$opt{www}, 'domain=s', \$opt{domain}, 'mail=s', \$opt{mail}, 'mailpref=i', \$opt{mailpref}, 'mail2=s', \$opt{mail2}, 'mailpref2=i', \$opt{mailpref2}, 'origin=s', \$opt{origin}, 'mailaddr=s', \$opt{mailaddr}, 'ns=s', \$opt{ns}, 'ns2=s', \$opt{ns2}, # 'master=s', \$opt{master}, # 'slave=s', \$opt{slave}, 'refresh=i', \$opt{refresh}, 'retry=i', \$opt{retry}, 'expire=i', \$opt{expire}, 'minimum=i', \$opt{minimum}, 'host=s%', \%hosts, 'cname=s%', \%cnames, 'mx=s%', \%mx, 'mxprefs=s%', \%mxprefs, ); &set_defaults; &test_options; &set_opts_or_defs; &build_zone; &build_reverse if $def{build_reverse}; &write_confs if $def{write_confs}; &write_test_script if $def{test_script}; exit(0); sub set_defaults { # load the defaults # need a better way to do this, I've considered some of the optional # perl conf modules available at cpan, but I'm unwilling to make this # program dependent on non standard modules. do "gendns.conf"; } sub set_opts_or_defs { # set the final options $mail = end_in_dot ($opt{mail}) || end_in_dot ($def{mail}); $mailpref = end_in_dot ($opt{mailpref}) || end_in_dot ($def{mailpref}); $mail2 = end_in_dot ($opt{mail2}) || end_in_dot ($def{mail2}); $mailpref2 = end_in_dot ($opt{mailpref2}) || end_in_dot ($def{mailpref2}); $origin = end_in_dot ($opt{origin}) || end_in_dot ($def{origin}); $mailaddr = end_in_dot ($opt{mailaddr}) || end_in_dot ($def{mailaddr}); $ns = end_in_dot ($opt{ns}) || end_in_dot ($def{ns}); $ns2 = end_in_dot ($opt{ns2}) || end_in_dot ($def{ns2}); $serial=(time); $refresh = $opt{refresh} || $def{refresh}; $retry = $opt{retry} || $def{retry}; $expire = $opt{expire} || $def{expire}; $minimum = $opt{minimum} || $def{minimum}; $master = $opt{master} || $def{master}; $slave = $opt{slave} || $def{slave}; } sub is_ip { # return true(1) if $_[0] is an ip address otherwise return false(0) # currently that means that it's four dotted decimal numbers the # first and last being greater than zero, the middle two being # greater or equal to zero, and all digits being less than 256. my($one,$two,$three,$four) = split /\./, $_[0], 4; if (($one =~ /\D/) || ($two =~ /\D/) || ($three =~ /\D/) || ($four =~ /\D/)) { return 0; } if ( ($one < 256) && ($one > 0) && ($two < 256) && ($two >= 0) && ($three < 256) && ($three >= 0) && ($four < 256) && ($four > 0)) { return 1; } return 0; } sub end_in_dot { # test if $_[0] is a domain name or FQHN and if it is make sure it # ends with a . # I'd like to rewrite this so that it doesn't generate errors when # gendns is called with perl -w. my $return = $_[0]; if (( $_[0] =~ /\./ ) && ( $_[0] !~ /\.$/ )) { $return = $_[0] . '.'; } return $return; } sub test_options { # make sure that we got enough arguments to create a zone file. # make sure that all the ip addresses that we get are valid. # make sure that all fqdn end in '.' # I'd like lots more tests including this one: # # test if critical things like nameserver and mail servers exist # before create records for them, then we'll also need a way to # predefine things that will be created in later runs of gendns... my $die = 0; my $host; my %cleaned_hosts; if ((! defined($opt{domain})) && (! defined($opt{www}))){ $die=1; } elsif (! defined($opt{domain})) { print "You must give a domain name in order to create a zone"; print " file. '--domain=foo.bar'\n"; $die=1; } elsif (! defined($opt{www})) { print "You must give the web servers ip address in order to"; print " create a zone file. '--www=192.168.0.1'.\n"; $die=1; } elsif (! is_ip($opt{www})) { print "The value you gave '--www=$opt{www}' doesn't seem to"; print " contain a valid ip address.\n"; $die=1; } foreach $host (keys(%hosts)) { unless (is_ip($hosts{$host})) { print "The value you gave '--host $host=$hosts{$host}'"; print " dosen't seem to contain a valid ip address.\n"; $die=1; } } &usage if $die; undef $host; foreach $host (keys(%hosts)) { $cleaned_hosts{end_in_dot($host)} = $hosts{$host}; delete $hosts{$host}; } %hosts = %cleaned_hosts; undef %cleaned_hosts; undef $host; foreach $host (keys(%cnames)) { $cleaned_hosts{end_in_dot($host)} = end_in_dot($cnames{$host}); delete $cnames{$host}; } %cnames = %cleaned_hosts; undef %cleaned_hosts; undef $host; foreach $host (keys(%mx)) { $cleaned_hosts{end_in_dot($host)} = end_in_dot($mx{$host}); delete $mx{$host}; } %mx = %cleaned_hosts; undef %cleaned_hosts; undef $host; foreach $host (keys(%mxprefs)) { $cleaned_hosts{end_in_dot($host)} = $mxprefs{$host}; delete $mxprefs{$host}; } %mxprefs = %cleaned_hosts; $opt{domain} = end_in_dot($opt{domain}); } sub usage{ # print out usage info and exit with (1) print "usage $0: --www=192.168.0.1 --domain=foo.bar\n"; print "\t[ --mail=mail.foo.bar ] \t[ --mailpref=10 ]\n"; print "\t[ --mail2=mail2.foo.bar ] [ --mailpref2=20 ]\n"; print "\t[ --ns=ns.foo.bar ] [ --ns2=ns2.foo.bar ]\n"; print "\t[ --origin=foo.bar ] [ --mailaddr=root.foo.bar ]\n"; print "\t[ --refresh=8H ] [ --retry=2H ]\n"; print "\t[ --expire=1W ] \t[ --minimum=1D ]\n"; print "\t[ --host ns=192.168.0.2 ] [ --host ns2=192.168.0.3 ]\n"; print "\t[ --host mail=192.168.0.4 ] [ --host mail2=192.168.0.5 ]\n"; print "\t[ --cname foo2=bar ] [ --mx foo3=foo2 ]\n"; print "\t[ --mxprefs foo3=10 ] [ --mail2=undef ]\n"; exit 1; } sub build_zone { # build a zone file based on the information we've been passed on the # command line. # In the future I'd like this function to calculate the length of each # hostname so that we can put the correct number of tabs in so that # the generated zone files look nice... my ($host,$cname); my $clean_domain = $opt{domain}; chop $clean_domain if ($clean_domain =~ /\.$/); open (ZONE,"> $clean_domain") || die "can't open $clean_domain for writing.\n"; print ZONE "; generated by gendns.pl http://www.wildgear.com/gendns/\n\n"; print ZONE "\@\tIN\tSOA\t$origin $mailaddr\t(\n"; print ZONE "\t\t\t\t$serial\;\tSerial\n"; print ZONE "\t\t\t\t$refresh\t\;\tRefresh\n"; print ZONE "\t\t\t\t$retry\t\;\tRetry\n"; print ZONE "\t\t\t\t$expire\t\;\tExpire\n"; print ZONE "\t\t\t\t$minimum\ )\t\;\tMinimum\n\n"; print ZONE "\t\t\t\tIN\tNS\t$ns\n"; print ZONE "\t\t\t\tIN\tNS\t$ns2\n"; print ZONE "\t\t\t\tIN\tMX\t$mailpref\t$mail\n"; if ((defined($mail2)) && (defined($mailpref2)) && ($mail2 ne 'undef')) { print ZONE "\t\t\t\tIN\tMX\t$mailpref2\t$mail2\n\n"; print ZONE "www\.$opt{domain}\t\tIN\tMX\t$mailpref\t$mail\n"; print ZONE "www\.$opt{domain}\t\tIN\tMX\t$mailpref2\t$mail2\n"; } else { print ZONE "\n"; print ZONE "www\.$opt{domain}\t\tIN\tMX\t$mailpref\t$mail\n"; } print ZONE "$opt{domain}\t\tIN\tA\t$opt{www}\n"; print ZONE "www\.$opt{domain}\t\tIN\tA\t$opt{www}\n"; print ZONE "\n"; foreach $host (keys(%hosts)) { print ZONE "$host\t\tIN\tA\t$hosts{$host}\n"; } foreach $cname (keys(%cnames)) { print ZONE "$cname\t\tIN\tCNAME\t$cnames{$cname}\n"; } foreach $mx (keys(%mx)) { my $pref = $mxprefs{$mx} || '10'; print ZONE "$mx\t\tIN\tMX\t$pref\t$mx{$mx}\n"; } } sub build_reverse { # build new reverse file (in-addr.arpa) if none exists otherwise # append to existing reverse file. # need to add checking to see if ptr already exists within the # current file and not add it if it would be a duplicate entry. $reverse{$opt{www}} = 'www.' . $opt{domain}; foreach $host (keys(%hosts)) { $reverse{$hosts{$host}} = $host . '.' . $opt{domain}; } foreach $ip (keys(%reverse)) { my ($a,$b,$c,$ptr_ip) = split /\./,$ip,4; my $network = $a . '.' . $b . '.' . $c . '.' . '0'; if ($def{reverse_for} =~ $network) { my $in_addr_arpa = $c . '.' . $b . '.' . $a . '.in-addr.arpa'; # # only pick one of the following two lines... # my $file_name = 'db.' . $a . '.' . $b . '.' . $c; my $file_name = $a . '.' . $b . '.' . $c . '.db'; $addr_arpa{$file_name} = $in_addr_arpa; if (! -e $file_name || -z _) { open (REVERSE,"> $file_name") || die "can't open $file_name for writing.\n"; print REVERSE "; generated by gendns.pl http://www.wildgear.com/gendns/\n\n"; print REVERSE "\$ORIGIN $in_addr_arpa.\n"; print REVERSE "\@\tIN\tSOA\t$origin $mailaddr\t(\n"; print REVERSE "\t\t\t\t$serial\;\tSerial\n"; print REVERSE "\t\t\t\t$refresh\t\;\tRefresh\n"; print REVERSE "\t\t\t\t$retry\t\;\tRetry\n"; print REVERSE "\t\t\t\t$expire\t\;\tExpire\n"; print REVERSE "\t\t\t\t$minimum\ )\t\;\tMinimum\n\n"; print REVERSE "\t\t\t\tIN\tNS\t$ns\n"; print REVERSE "\t\t\t\tIN\tNS\t$ns2\n"; print REVERSE "$ptr_ip\t\t\t\tIN\tPTR\t$reverse{$ip}\n"; } else { open (REVERSE,">> $file_name") || die "can't open $file_name for writing.\n"; print REVERSE "$ptr_ip\t\t\t\tIN\tPTR\t$reverse{$ip}\n"; } close REVERSE; } } } sub write_confs { # build new master and slave configuration files or append to # pre-existing files. use vars qw / $newmaster $newslave @master @slave /; if (! -e $def{master} || -z _) { open (MASTER,"> $def{master}") || die "can't open $def{master} for writing.\n"; print_header_stuff(\*MASTER); print MASTER $def{master_junk} if defined($def{master_junk}); print MASTER $def{hint_junk} if defined($def{hint_junk}); $newmaster = 1; } else { open (MASTER, " $def{master}") || die "can't open $def{master} for reading.\n"; @master = ; close MASTER; open (MASTER,">> $def{master}") || die "can't open $def{master} for writing.\n"; } if (! -e $def{slave} || -z _) { open (SLAVE,"> $def{slave}") || die "can't open $def{slave} for writing.\n"; print_header_stuff(\*SLAVE); print SLAVE $def{slave_junk} if defined($def{slave_junk}); print SLAVE $def{hint_junk} if defined($def{hint_junk}); $newslave = 1; } else { open (SLAVE,$def{slave}) || die "can't open $def{slave} for reading.\n"; @slave = ; close SLAVE; open (SLAVE,">> $def{slave}") || die "can't open $def{slave} for writing.\n"; } my $clean_domain = $opt{domain}; chop $clean_domain if ($clean_domain =~ /\.$/); print MASTER "zone \"$clean_domain\" {\n"; print MASTER "\ttype master;\n"; print MASTER "\tfile \"$clean_domain\";\n"; print MASTER "};\n\n"; print SLAVE "zone \"$clean_domain\" {\n"; print SLAVE "\ttype slave;\n"; print SLAVE "\tfile \"$clean_domain\";\n"; print SLAVE "\tmasters { $def{master_ip} ; } ;\n"; print SLAVE "};\n\n"; foreach $file (keys(%addr_arpa)) { unless (grep (/$file/, @master)) { print MASTER "zone \"$addr_arpa{$file}\" {\n"; print MASTER "\ttype master;\n"; print MASTER "\tfile \"$file\";\n"; print MASTER "};\n\n"; } unless (grep (/$file/, @slave)) { print SLAVE "zone \"$addr_arpa{$file}\" {\n"; print SLAVE "\ttype slave;\n"; print SLAVE "\tfile \"$file\";\n"; print SLAVE "\tmasters { $def{master_ip} ; } ;\n"; print SLAVE "};\n\n"; } } close MASTER; close SLAVE; sub print_header_stuff { $fh = shift; print $fh "// generated by gendns.pl "; print $fh "http://www.wildgear.com/gendns/\n\n"; print $fh "options {\n\tdirectory \"$def{zone_dir}\";\n};\n\n"; } } sub write_test_script { if (! -e "test.sh" || -z _) { open (TEST, "> test.sh") || die "can't open test.sh for writing.\n"; } else { open (TEST, ">> test.sh") || die "can't open test.sh for writing.\n"; } my $clean_domain = $opt{domain}; chop $clean_domain if ($clean_domain =~ /\.$/); print TEST "./zonediff.sh $clean_domain ;read\n"; }