#!/usr/local/bin/perl -w ###################################################################### # # DNS/Config/File/Nsd.pm # # $Id: Nsd.pm,v 1.3 2003/02/16 10:15:32 awolf Exp $ # $Revision: 1.3 $ # $Author: awolf $ # $Date: 2003/02/16 10:15:32 $ # # Copyright (C)2003 Bruce Campbell. All rights reserved. # Base Class (Bind9) (C)2001-2003 Andy Wolf. All rights reserved. # # This library is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # ###################################################################### package DNS::Config::File::Nsd; no warnings 'portable'; use 5.6.0; use strict; use warnings; use vars qw(@ISA); use DNS::Config; use DNS::Config::Server; use DNS::Config::Statement; @ISA = qw(DNS::Config::File); my $VERSION = '0.66'; my $REVISION = sprintf("%d.%02d", q$Revision: 1.3 $ =~ /(\d+)\.(\d+)/); # FILE is the nsd.zones file. sub new { my($pkg, $file, $config) = @_; my $class = ref($pkg) || $pkg; my $self = { 'FILE' => $file }; $self->{'CONFIG'} = $config if($config); bless $self, $class; return $self; } # NSD has an additional config file for nsdc. Return the filename # if it has been defined. sub nsdc { my( $self, $file ) = (@_); if( defined( $file ) ){ $self->{'NSDC'} = $file; } return( $self->{'NSDC'} ); } # NSD has a directory for TSIG keys. Return the directory if it # has been defined. sub nsdkeysdir { my( $self, $dir ) = (@_); if( defined( $dir ) ){ $self->{'NSDKEYSDIR'} = $dir; } return( $self->{'NSDKEYSDIR'} ); } sub do_gettsig { my $self = shift; my $tsigdir = shift; my $keyname = shift; # This really should be a subroutine in DNS::Config::Statement::Keys. my %algs = ( "157", "hmac-md5", ); # Get what it is. my $t_type = undef; my $t_zone = undef; my $t_ip = undef; if( $keyname =~ /^\s*(zi)\-(\S+)\-([^\-]+)\s*$/i ){ $t_type = lc( $1 ); $t_zone = $2; $t_ip = $3; }elsif( $keyname =~ /^\s*(ip|zo)\-(\S+)\s*$/i ){ # either zo-$zone or ip-$ip $t_type = lc($1); $t_zone = $2; if( $t_type eq "ip" ){ $t_ip = $t_zone; $t_zone = undef; } }else{ $t_type = "unknown"; } # We return a string (or maybe not) that should be inserted into # the main stream. Usually we define a key, or a server statement, # but only if we haven't already done so for this key or server. my $retmain = undef; my $retkey = undef; if( ! defined( $self->{'TSIG'} ) ){ %{$self->{'TSIG'}} = (); } if( ! defined( ${$self->{'TSIG'}}{$keyname} ) ){ # We need to read in the file. my $t_file = "$tsigdir" . "/" . "$keyname" . ".tsiginfo"; if( -f "$t_file" ){ # if( open( TSIGINPUT, "$t_file" ) ){ # Server IP address. my $t_addr = ; my $t_name = ; my $t_alg = ; my $t_sec = undef; while( my $line = ){ chomp( $line ); $t_sec .= $line; } close( TSIGINPUT ); chomp( $t_addr ); chomp( $t_name ); chomp( $t_alg ); chomp( $t_sec ); # print STDERR "Blot - $t_addr $t_name $t_alg $t_sec\n"; # Store it here, and elsewhere. ${$self->{'TSIG'}}{$keyname}{'ip'} = $t_addr; ${$self->{'TSIG'}}{$keyname}{'name'} = $t_name; ${$self->{'TSIG'}}{$keyname}{'algorithm-num'} = $t_alg; if( defined( $algs{$t_alg} ) ){ ${$self->{'TSIG'}}{$keyname}{'algorithm'} = $algs{$t_alg}; }else{ ${$self->{'TSIG'}}{$keyname}{'algorithm'} = $t_alg; } ${$self->{'TSIG'}}{$keyname}{'secret'} = $t_sec; ${$self->{'TSIG'}}{$keyname}{'realname'} = "___$t_name"; } } } # See if we've got this one. if( defined( ${$self->{'TSIG'}}{$keyname} ) ){ # We've got it. Whats the actual keyname? my $t_real = ${$self->{'TSIG'}}{$keyname}{'realname'}; # If its not there, copy it. if( ! defined( ${$self->{'TSIG'}}{$t_real} ) ){ ${$self->{'TSIG'}}{$t_real}{'ip'} = ${$self->{'TSIG'}}{$keyname}{'ip'}; ${$self->{'TSIG'}}{$t_real}{'name'} = ${$self->{'TSIG'}}{$keyname}{'name'}; ${$self->{'TSIG'}}{$t_real}{'secret'} = ${$self->{'TSIG'}}{$keyname}{'secret'}; ${$self->{'TSIG'}}{$t_real}{'algorithm'} = ${$self->{'TSIG'}}{$keyname}{'algorithm'}; ${$self->{'TSIG'}}{$t_real}{'algorithm-num'} = ${$self->{'TSIG'}}{$keyname}{'algorithm-num'}; } # Whats the name of this one? $retkey = ${$self->{'TSIG'}}{$t_real}{'name'}; # Do we need to define this one? if( ! defined( ${$self->{'TSIG'}}{$t_real}{'done'} ) ){ # We do need to define it. $retmain .= " key " . ${$self->{'TSIG'}}{$t_real}{'name'} . " {"; $retmain .= " algorithm " . ${$self->{'TSIG'}}{$t_real}{'algorithm'} . ";"; $retmain .= " secret \"" . ${$self->{'TSIG'}}{$t_real}{'secret'} . "\";"; $retmain .= " };"; # Say that we've defined this one. ${$self->{'TSIG'}}{$t_real}{'done'}++; } if( $t_type eq "ip" && ! defined( ${$self->{'TSIG'}}{$keyname}{'done'} ) && defined( $t_ip ) ){ # We now need to use this key with the server. $retmain .= " server $t_ip { keys { " . ${$self->{'TSIG'}}{$keyname}{'name'} . "; }; };"; ${$self->{'TSIG'}}{$keyname}{'done'}++; # print STDERR "Foo - $retmain\n"; } } return( $retmain, $retkey ); } sub parse { my($self, $file) = @_; $file = $file || $self->{'FILE'}; my @lines = $self->read($file); my @nsdc = $self->read( $self->nsdc() ); # This space left blank for any includes. return undef unless(scalar @lines); $self->{'CONFIG'} = new DNS::Config() if(!$self->{'CONFIG'}); my $result; my %nsdc_h = ( "namedxfer", "CP:named-xfer VAL;", "nsdzonesdir", "CP:directory VAL;", "nsdflags", "SPECIAL", "nsdkeysdir", "SPECIAL", ); $result .= " options {"; # Loop through the nsdc lines. my $nsdkeysdir = undef; for my $line (@nsdc) { next unless( $line =~ /^\s*(\S+)\s*=\s*\"(.*)\"\s*(\#.*)?$/ ); my $name = lc( $1 ); my $fill = $2; next unless( defined( $nsdc_h{$name} ) ); my $tval = $nsdc_h{$name}; if( $tval =~ /^CP:(\S+.*)\s*$/ ){ $tval = $1; $tval =~ s/VAL/$fill/g; $result .= " $tval"; }elsif( $tval eq 'SPECIAL' && $name eq 'nsdflags' ){ # Special processing required. my @tsplit = split( /\s+/, $fill ); my $curflag = undef; my @addys = (); my $port = 53; foreach my $kkey( @tsplit ){ if( $kkey =~ /^\s*\-[ap]\s*$/ ){ $curflag = $kkey; }elsif( defined($curflag) ){ if( $curflag eq '-a' ){ push @addys, $kkey; }elsif( $curflag eq '-p' ){ $port = $kkey; } } } $result .= " listen-on port $port {"; foreach my $kkey( @addys ){ $result .= " $kkey;"; } $result .= " };"; }elsif( $tval eq 'SPECIAL' && $name eq 'nsdkeysdir' ){ $nsdkeysdir = $fill; }else{ next; } } $nsdkeysdir = $self->nsdkeysdir( $nsdkeysdir ); $result .= " };"; # tsig stuff. Wheee. my %tsigs = (); # Loop through the lines in nsd.zones. for my $line (@lines) { # replace lots of space with one space. $line =~ s/\s+/ /g; # Remove '//' style comments. $line =~ s/\/\/.*$//g; # Remove '#' style comments. $line =~ s/\#.*$//g; # nsd.zones only has lines beginning with 'zone'. next unless( $line =~ /^\s*zone\s+(\S+)\s+(\S+)\s*(\S*.*)\s*$/ ); my $this_zone=$1; my $this_file=$2; my $this_rest=$3; # We rework the string into Bind9-style, as the code for # dealing with this is nice and solid. # Set up a temporary line first. We may need to insert # stuff into the stream beforehand (With BIND, you need to # define keys before you use them. By inserting stuff in # this stream before we use them, we hopefully stop people # shooting themselves in the foot if they generate a named.conf # file by simply dumping the config out. my $tmpresult = " zone \"$this_zone\" in { file \"$this_file\";"; my $tmptype = "master"; if( $this_rest =~ /masters\s*((\s+(\d+\.){3,3}\d+|\s+(([0-9a-f]*:){1,15}(:[0-9a-f]+){1,15}))){1,}\s*(notify|$)/ ){ my @tmpres3 = split( / /, $1 ); $tmpresult .= " masters {"; foreach my $tval ( @tmpres3 ){ $tmpresult .= " $tval"; if( defined( $nsdkeysdir ) ){ if( -f $nsdkeysdir . "/ip-" . $tval . ".tsiginfo" ){ # print STDERR "Got dir $nsdkeysdir\n"; # we need to predefine a key. my ($tmpstr, $keyname) = $self->do_gettsig( $nsdkeysdir, "ip-$tval" ); $result .= $tmpstr if( defined( $tmpstr ) ); } if( -f $nsdkeysdir . "/zi-" . $this_zone . "-" . $tval . ".tsiginfo" ){ # This key gets used for this # one. my ($tmpstr, $keyname) = $self->do_gettsig( $nsdkeysdir, "zi-$this_zone-$tval" ); $result .= $tmpstr if( defined( $tmpstr ) ); $tmpresult .= " key $keyname" if( defined( $keyname ) ); }elsif( -f $nsdkeysdir . "/zo-" . $this_zone . ".tsiginfo" ){ # This key gets used for this # one. my ($tmpstr, $keyname) = $self->do_gettsig( $nsdkeysdir, "zo-$this_zone" ); $result .= $tmpstr if( defined( $tmpstr ) ); $tmpresult .= " key $keyname" if( defined( $keyname ) ); } } $tmpresult .= ";"; } $tmpresult .= " };"; $tmptype = "slave"; } if( $this_rest =~ /notify\s*((\s+(\d+\.){3,3}\d+|\s+(([0-9a-f]*:){1,15}(:[0-9a-f]+){1,15}))){1,}\s*(masters|$)/ ){ my @tmpres3 = split( / /, $1 ); $tmpresult .= " also-notify {"; foreach my $tval ( @tmpres3 ){ $tmpresult .= " $tval;"; } $tmpresult .= " };" } # We need to check for tsig keys now. if( defined( $nsdkeysdir ) ){ if( -f $nsdkeysdir . "/zo-" . $this_zone . ".tsiginfo" ){ my ($tmpstr, $keyname) = $self->do_gettsig( $nsdkeysdir, "zo-$this_zone" ); $result .= $tmpstr if( defined( $tmpstr ) ); $tmpresult .= "allow-transfer { key $keyname;};" if( defined( $keyname ) ); } } # Now that we've put tsig stuff beforehand, put in the zone. $result .= " $tmpresult type $tmptype;"; # and end it. $result .= " };"; } my $tree = &analyze_brackets($result); my @res = &analyze_statements(@$tree); foreach my $temp (@res) { my @temp = @$temp; my $type = shift @temp; my $statement; eval { my $tmp = 'DNS::Config::Statement::' . ucfirst(lc $type); if ( eval "require $tmp" ){ $statement = $tmp->new(); $statement->parse_tree(@temp); }else{ # Doesn't exist. print STDERR "Require of $tmp failed\n"; } }; if($@) { #warn $@; $statement = DNS::Config::Statement->new(); $statement->parse_tree($type, @temp); } $self->{'CONFIG'}->add($statement); } return $self; } # This routine only dumps the nsd.zones file. sub dump_nsd_zones { my($self, $file) = @_; $file = $file || $self->{'FILE'}; return undef unless($file); return undef unless($self->{'CONFIG'}); my $config = $self->config; my @statements = $config->statements; my $infile = 0; my $old_fh = undef; if($file) { if(open(FILE, ">$file")) { $old_fh = select(FILE); $infile = 1; }else{ return( undef ); } } # We need to iterate through the config outselves foreach my $statement ( @statements ){ my $tmpref = ref $statement; # Dump only the zone mentions. next unless( $tmpref =~ /^DNS::Config::Statement::Zone$/ ); # ; zone^Iname^I^Ifilename^I^I[ masters/notify ip-address ]$ # zone^I.^I^Iprimary/root.zone^Inotify 128.9.0.107 192.33.4.12 128.8.10.90$ # zone^Iww.net^I^Iprimary/ww.net$ # zone^Inlnetlabs.nl^Isecondary/nlnetlabs.nl^Imasters 213.53.69.1$ print "zone\t" . $statement->{'NAME'} . "\t\t" . $statement->{'FILE'}; my @masters = $statement->masters(); my @anotify = $statement->also_notifys(); if( ( scalar @masters ) > 0 ){ print "\tmasters"; foreach my $kkey( @masters ){ my @foo = @{$kkey}; foreach my $kkey2( @foo ){ print " $kkey2"; } } } if( ( scalar @anotify ) > 0 ){ print "\tnotify"; foreach my $kkey( @anotify ){ my @foo = @{$kkey}; foreach my $kkey2( @foo ){ print " $kkey2"; } } } print "\n"; # print "Foo " . $statement->master . "\n"; } # If we're in a file, select() back. if( $infile ){ # map { $_->dump() } $self->config()->statements(); select($old_fh); close FILE; $infile = 0; } return $self; } sub dump_nsdc { my($self, $file) = @_; $file = $file || $self->{'FILE'}; return undef unless($file); return undef unless($self->{'CONFIG'}); my $config = $self->config; my @statements = $config->statements; my $infile = 0; my $old_fh = undef; if($file) { if(open(FILE, ">$file")) { $old_fh = select(FILE); $infile = 1; }else{ return( undef ); } } # We need to iterate through the config outselves foreach my $statement ( @statements ){ my $tmpref = ref $statement; # Dump only the option mentions. next unless( $tmpref =~ /^DNS::Config::Statement::Options$/ ); # Where named-xfer is. if( defined( $statement->{'NAMED-XFER'} ) ){ print "NAMEDXFER=\"" . $statement->{'NAMED-XFER'} . "\"\n"; } # Where NSDZONES is. if( defined( $statement->{'DIRECTORY'} ) ){ print "NSDZONES=\"" . $statement->{'DIRECTORY'} . "\"\n"; } # Where the NSDKEYSDIR is. # nsdkeysdir isn't expected to be in the Options statement. if( defined( $self->nsdkeysdir() ) ){ print "NSDKEYSDIR=\"" . $self->nsdkeysdir() . "\"\n"; }elsif( defined( $statement->{'NSDKEYSDIR'} ) ){ print "NSDKEYSDIR=\"" . $statement->{'NSDKEYSDIR'} . "\"\n"; }elsif( defined( $statement->{'DIRECTORY'} ) ){ print "NSDKEYSDIR=\"" . $statement->{'DIRECTORY'} . "\"\n"; } # Now for the flags. Oh my. if( defined( $statement->{'LISTEN-ON'} ) ){ print "NSDFLAGS=\""; my @tsplit = @{ $statement->{'LISTEN-ON'} }; foreach my $kkey( @tsplit ){ if( ! ref( $kkey ) ){ if( $kkey =~ /port/i ){ print " -p"; }else{ print " $kkey"; } }else{ my @tref1 = @{$kkey}; foreach my $kkey2( @tref1 ){ if( ref( $kkey2 ) ){ push @tref1, @{$kkey2}; next; } if( $kkey2 =~ /any/ ){ # NSD doesn't handle # multiple interfaces # correctly. This is # a hack to deal with # these cases. print " \`ifconfig -a | perl -e \'while(<>){ next unless(m/^\\s*inet(4|6)?(\\s+addr:)?\\s*(((\\d+\\.){3,3}\\d+)|(([0-9a-f]*:){1,15}(:[0-9a-f]+){1,15}))(\\/\\d+)?\\s+/); print \" -a \$3\"; }\'\`"; }else{ print " -a $kkey2"; } } } } print "\"\n"; } print "\n"; } # If we're in a file, select() back. if( $infile ){ # map { $_->dump() } $self->config()->statements(); select($old_fh); close FILE; $infile = 0; } return $self; } sub dump_tsig() { my($self, $dir) = @_; $dir = $dir || $self->nsdkeysdir(); return( undef ) unless( defined( $dir ) ); # Make sure that its useful. return( undef ) unless( -d $dir ); return( undef ) unless( -r $dir ); return( undef ) unless( -w $dir ); return( undef ) unless( -x $dir ); # Map the algorithms. # Should really be invoking DNS::Config::Statement::Key for this. my %algs = ( "157", "hmac-md5", "hmac-md5", "157", ); # Run through the statements. my $config = $self->config; my @statements = $config->statements; my %keys = (); my %keys_written = (); my %want_keys = (); foreach my $statement( @statements ){ my $tref = ref( $statement ); # We only want Key, Zone or Server statements. next unless( $tref =~ /^DNS::Config::Statement::(Key|Zone|Server)$/ ); my $this_ref = $1; if( $this_ref eq 'Key' ){ my $tname = $statement->name(); my $talg = $statement->algorithm(); my $tsecret = $statement->secret(); if( $talg =~ /\D/ ){ $talg = $algs{$talg}; } $keys{$tname}{'name'} = $tname; $keys{$tname}{'algorithm'} = $talg; $keys{$tname}{'secret'} = $tsecret; }elsif( $this_ref eq 'Server' ){ my $tname = $statement->name(); my @tkeys = $statement->keys(); my %usekeys = (); foreach my $kkey( @tkeys ){ if( ref( $kkey ) ){ push @tkeys, @{$kkey}; }else{ $usekeys{"$kkey"}++; } } foreach my $kkey( keys %usekeys ){ my $tstr = "ip-$tname.tsiginfo"; $want_keys{$tstr} = $kkey; } }elsif( $this_ref eq 'Zone' ){ my $tname = $statement->name(); my @masters = $statement->masters(); my $loop = 0; # This is possibly multiple levels of array, that # should be the sequence of things in the 'masters' # field of the zone statement. We *should* have # 'ip', 'port', 'port_num', 'key', 'key_id', 'ip' (etc) # with the 'port', 'port_num' and 'key', 'key_id' # sequences optional. while( $loop < scalar @masters ){ my $kkey = $masters[$loop]; if( ref( $kkey ) ){ push @masters, @{$kkey}; $loop++; }else{ # $ip key $keyname my $tip = $kkey; $loop++; my $tport = undef; my $tkey = undef; while( ( $loop + 2 ) < ( scalar @masters ) && ! ref( $masters[$loop] ) && ! ref( $masters[$loop+1] ) && $masters[$loop] =~ /(port|key)/i ){ my $twhat=$1; if( $twhat =~ /key/i ){ $tkey = $masters[$loop+1]; $loop++; }elsif( $twhat =~ /port/i ){ $tport = $masters[$loop+1]; $loop++; } $loop++; } # We found a key for this zone. Yay! if( defined( $tkey ) ){ my $tstr = "zi-$tname-$tip.tsiginfo"; $want_keys{$tstr} = $tkey; } } } } } # Now write out all of the keys. foreach my $kkey( keys %want_keys ){ my $tkey = $want_keys{$kkey}; print STDERR "Key - $kkey - $tkey\n"; next if( defined( $keys_written{$kkey} ) ); next if( ! defined( $keys{$tkey}{'name'} ) ); # Wheres the IP address? my $tip = "IPADDRESS"; # zi-$zone-$ip.tsiginfo if( $kkey =~ /^zi-\S+-([^\-]+).tsiginfo$/ ){ $tip=$1; # ip-$ip.tsiginfo }elsif( $kkey =~/^ip-(\S+).tsiginfo$/ ){ $tip=$1; } # Write out the file. if( open( TSIGOUT, "> $dir/$kkey" ) ){ print TSIGOUT "$tip\n"; print TSIGOUT $keys{$tkey}{'name'} . "\n"; print TSIGOUT $keys{$tkey}{'algorithm'} . "\n"; # Deal with the secret. my $toutsec = undef; if( ref( $keys{$tkey}{'secret'} ) ){ $toutsec = join( ' ', @{$keys{$tkey}{'secret'}} ) ; }else{ $toutsec = $keys{$tkey}{'secret'}; } $toutsec =~ s/^"//g; $toutsec =~ s/"$//g; print TSIGOUT "$toutsec"; print TSIGOUT "\n"; close( TSIGOUT ); $keys_written{$kkey}++; } } return( $self ); } sub dump { my($self, $file) = @_; # Eventually this could dump all of it, but you need to specify # multiple files. return( $self->dump_nsd_zones( $file ) ); } sub config { my($self) = @_; return($self->{'CONFIG'}); } sub analyze_brackets { my($string) = @_; my @chars = split //, $string; my $tree = []; my @chunks; my @stack; my %matching = ( '(' => ')', '[' => ']', '<' => '>', '{' => '}', ); for my $char (@chars) { if(grep {$char eq $_} keys(%matching)) { my $temp = []; push @$tree, $temp; push @chunks, $tree; push @stack, $matching{$char}; $tree = $temp; } elsif(grep {$char eq $_} values(%matching)) { my $expected = pop @stack; die "Invalid order !\n" if((!defined $expected) || ($char ne $expected)); $tree = pop @chunks; die "Unmatched closing !\n" if(!ref($tree)); } else { my $noe = scalar(@$tree); if((!$noe) || (ref($$tree[$noe-1]) eq 'ARRAY')) { push @$tree, ($char); } else { $$tree[$noe-1] .= $char; } } } die "Unbalanced !\n" if(scalar @stack); return($tree); } sub analyze_statements { my(@array) = @_; my @result; my $full; for my $line (@array) { if(!ref($line)) { $line =~ s/\s*\;\s*/\;/g; my(@parts) = split /;/, $line, -1; shift @parts if(!$parts[0]); if($parts[$#parts-1] eq '') { $full = 1; pop @parts; } else { $full = 0; } for my $temp (@parts) { if($temp) { $temp =~ s/^\s*//g; my @chunks = split / /, $temp; push @result, (\@chunks); } } } else { my @statements = &analyze_statements(@$line); my @temp; if(!$full) { my $temp = pop @result; @temp = @$temp; } push @temp, (\@statements); push @result, (\@temp); } } return(@result); } 1; __END__ =pod =head1 NAME DNS::Config::File::Nsd - Concrete adaptor class =head1 SYNOPSIS use DNS::Config::File::Nsd; my $file = new DNS::Config::File::Nsd($nsd.zones_file); # Read in an additional config file (needed before invoking ->parse() ) $file->nsdc( $nsdc.conf_file ); # Set the nsdkeysdir (tsig keys in files) $file->nsdkeysdir( $tsigdir ); # Parse nsd.zones, nsdc.conf, and any TSIG files $file->parse(); # Dump the nsd.zones file (also $file->dump_nsd_zones() ) $file->dump(); # Dump the nsdc.conf file $file->dump_nsdc(); # Dump the tsig files. $file->dump_tsig( $tsigdir ); # Debug the output. $file->debug(); $file->config(new DNS::Config()); =head1 ABSTRACT This class represents a set of configuration files for NLNetLab's NSD (Name Server Daemon), an authoritative-only nameserver sponsored by the RIPE NCC. =head1 DESCRIPTION This class, the Nsd file adaptor, knows how to read and write the information to a file in the NSD daemon specific formats. Note that NSD has three places for configuration information, being: nsd.zones - Zone name and zone file specifications for zonec(1), the notify servers for nsd-notify(1) and the master servers for nsdc and named-xfer. nsdc.conf - Special (shell-script) configuration for nsdc. NSDKEYSDIR - A directory where TSIG keys can be found, for usage by nsdc and named-xfer. =head1 AUTHOR Copyright (C)2003 Bruce Campbell. All rights reserved. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. Please address bug reports and comments to: bxc@users.sourceforge.net =head1 SEE ALSO L, L, L =cut