dns-autoslave
changeset 10 8f7ba479860c
parent 9 6f5ef6fb479d
child 11 d1dd256c037a
equal deleted inserted replaced
9:6f5ef6fb479d 10:8f7ba479860c
     1 #! /usr/bin/perl -w
     1 #! /usr/bin/perl
     2 # $Id$
     2 # $Id$
     3 my $USAGE = <<'#';
     3 # $URL$
     4 Usage: $ME [options]
       
     5        -l --logfile=s   Name of the logfile we've to read [$opt_logfile]
       
     6        -z --zonesdir=s  Where the named.conf's are expected [$opt_zonesdir]
       
     7        -u --[no]update  Update the \"masters\"-entries [$opt_update] 
       
     8        -f --[no]follow  Follow the end of the logfile [$opt_follow]
       
     9        -d --[no]debug   extra debug output [$opt_debug]
       
    10        -h --help        This text [$opt_help]
       
    11           --daemon      go into background [$opt_daemon]
       
    12        -p --pidfile=s   file to store the pid [$opt_pidfile]
       
    13        -v --version     print version [$opt_version]
       
    14 #
       
    15 # Es wird ein Verzeichnis geben, in diesem Verzeichnis liegt für 
       
    16 # *jede* Zone eine eigene Konfigurations-Datei.
       
    17 # Diese ganzen Konfigurationsdateien werden dann zusammengefaßt
       
    18 # und diese zusammengefaßte wird dem bind per "include" mitgeteilt.
       
    19 #
       
    20 # Wir durchsuchen den Syslog nach
       
    21 # NOTIFY von Master-Servern.
       
    22 #   o Wenn der Master nicht authorisiert ist -> ENDE GELÄNDE
       
    23 #   o Andernfalls merken wir uns diesen Master für diese Domain
       
    24 # Wenn dann mal zu lesen ist "not of our zones" und wir uns diesen
       
    25 # als einen unserer Master gemerkt haben, dann vermerken wir uns, daß
       
    26 # für diese Zone das Konfig-File fehlt.
       
    27 #
       
    28 # Sollte irgendwann mal ein "slave zone...loaded" auftauchen, ist das Konfig-File
       
    29 # inzwischen vorhanden und kein Grund zur Panik.  Wir entfernen es aus der Liste
       
    30 # der fehlenden Files.
       
    31 #
       
    32 # Sollte dieser Text ausbleiben, müssen wir ein File anlegen (wahrscheinlich).
       
    33 # -> Sollte trotzdem schon eins da sein, dann konnten wir aus irgendwelchen
       
    34 # Gründen nix laden (deshalb fehlt ja der "...loaded"-Text).  Das kann z.B. sein, weil ein 
       
    35 # Master mal keinen Bock hat oder nicht authoritativ ist.
       
    36 #
       
    37 # Etwas anders sieht's im update-Modus aus.  Hier wird *jeder* Master für jede Domain gemerkt und
       
    38 # geprüft, ob der in dem entsprechenden Konfig-File enthalten ist.
       
    39 #
       
    40 # Im täglichen Einsatz sollte es ohne Update-Modus ganz gut funktionieren.
       
    41 
     4 
    42 use strict;
     5 use strict;
       
     6 use warnings;
       
     7 use Unix::Syslog qw(:macros :subs);
    43 use File::Basename;
     8 use File::Basename;
    44 use IO::Handle;
     9 use Net::Pcap;
    45 use Fcntl qw/:flock/;
    10 use Net::DNS::Packet;
    46 use File::Path;
    11 use AppConfig;
    47 use Getopt::Long;
       
    48 use Unix::Syslog qw/:macros :subs/;
       
    49 
    12 
    50 my $ME = basename $0;
    13 # Es kommen Zeilen
       
    14 # add slave domain.com
       
    15 use constant ME => basename $0;
       
    16 use constant CF_FILE => "/etc/".ME.".conf";
       
    17 
       
    18 use constant CONFIG => (
       
    19     { CASE => 1 },
       
    20     dev =>	{ ARGS => "=s", DEFAULT => "eth0", ALIAS => "interface" },
       
    21     filter =>	{ ARGS => "=s", DEFAULT => "udp and src host (%accept) and dst port domain" },
       
    22     accept =>	{ ARGS => "=s" },
       
    23 
       
    24     cmd =>	{ ARGS => "=s", DEFAULT => "/usr/local/sbin/add-slave" },
       
    25 );
       
    26 
       
    27 sub process($$$);
       
    28 sub createZone($$$);
       
    29 
       
    30 openlog(ME, LOG_PERROR | LOG_NDELAY | LOG_PID, LOG_DAEMON);
       
    31 $SIG{__DIE__} = sub { die @_ if $^S; syslog(LOG_ERR, shift, @_); exit 1; };
       
    32 $SIG{__WARN__} = sub { syslog(LOG_WARNING, shift, @_); };
       
    33 
       
    34 my $Cf = new AppConfig CONFIG or die;
       
    35    $Cf->file(CF_FILE) or die if -f CF_FILE;
       
    36    $Cf->args or die;
       
    37 
       
    38 my %Seen;
       
    39 
       
    40 MAIN: {
       
    41 
       
    42     $_ = $Cf->filter;
       
    43     s/%accept/join " or ", split " ", $Cf->accept/e;
       
    44     $Cf->filter($_);
       
    45 
       
    46     my $err;
       
    47     my ($net, $mask);
       
    48     my ($pcap, $filter);
       
    49 
       
    50     0 == Net::Pcap::lookupnet($Cf->dev, \($net, $mask, $err)) or die $err;
       
    51     $pcap = Net::Pcap::open_live($Cf->dev, 1500, 0, 10, \$err) or die $err;
       
    52     0 == Net::Pcap::compile($pcap, \$filter, $Cf->filter, 1, $mask) or die $@;
       
    53     Net::Pcap::setfilter($pcap, $filter);
       
    54 
       
    55     Net::Pcap::loop($pcap, -1, \&process, undef);
       
    56 }
    51 
    57 
    52 
    58 
    53 my %auth = (
    59 sub process($$$) {
    54     "212.80.235.130" => "pu.schlittermann.de",
    60     my (undef, $hdr, $data) = @_;
    55     "212.80.235.132" => "mango.compot.com",
       
    56     "145.253.160.50" => "bastion.actech.de",
       
    57     "62.144.175.34" => "ns.add-on.de",	    
       
    58     "195.145.19.34" => "ns.datom.de",
       
    59     "62.157.194.1" => "ns.mueritzcomp.de",
       
    60     "212.80.235.137" => "ns.flaemingnet.de omni.flaemingnet.de",
       
    61     "212.80.235.152" => "www.nestwerk.de",
       
    62     # "194.162.141.17" => "dalx1.nacamar.de",
       
    63 );
       
    64 
    61 
    65 $SIG{__DIE__} = sub { syslog(LOG_ERR, $_[0]); exit -1; };
    62     {
    66 $SIG{__WARN__} = sub { syslog(LOG_WARNING, $_[0]); };
    63 	# Link-Level: 
    67 $SIG{TERM} = $SIG{INT} = sub { exit 0; };
    64 	# 	Dest	6 byte
       
    65 	# 	Src	6 byte
       
    66 	# 	type	2 byte
       
    67 	my $type = unpack("x12 n", $data);
       
    68 	if ($type != 0x800) {
       
    69 		warn "unexpected link layer type: ", sprintf("0x%x", $type), "\n";
       
    70 		return;
       
    71 	}
       
    72 	# Link layer abschneiden
       
    73 	$data = substr($data, 14);
       
    74     }
    68 
    75 
    69 my %seen;
    76     my $src;
       
    77     {
       
    78 	# IP-Header
       
    79 	# im ersten Byte stecken Version+Header-LÃnge
       
    80 	@_ = unpack("C x11 C4", $data);
       
    81 	my $hlen = ($_[0] & 0xf) * 4;
       
    82 	$src = join ".",@_[1..4];
    70 
    83 
    71 my $opt_help = 0;
    84 	# IP-Header abschneiden
    72 my $opt_pidfile = "/var/run/$ME.pid";
    85 	$data = substr($data, $hlen);
    73 my $opt_logfile = "/var/log/syslog";
    86     }
    74 my $opt_zonesdir = "/etc/bind/zones.d";
       
    75 my $opt_follow = 0;
       
    76 my $opt_update = 0;
       
    77 my $opt_debug = 0;
       
    78 my $opt_daemon = 1;
       
    79 my $opt_version = 0;
       
    80 
    87 
    81 my $naptime = 60;
    88     my $dns;
       
    89     {
       
    90 	# UDP: SRC-Port	16 bit
       
    91 	#      DST-Port 16 bit
       
    92 	#      Len      16 bit
       
    93 	#      Chksum   16 bit  => 8 byte
       
    94 	$data = substr($data, 8);
       
    95 	$dns = new Net::DNS::Packet(\$data) or do {
       
    96 	    warn "Can't decode packet\n";
       
    97 	    return;
       
    98 	}
       
    99     }
    82 
   100 
       
   101     return unless $dns->header->opcode eq "NS_NOTIFY_OP";
    83 
   102 
    84 sub updateFile($$$);
   103     foreach my $q ($dns->question) {
    85 sub debug($;@) { syslog(LOG_DEBUG, "DEBUG " . shift @_, @_) if $opt_debug; }
   104 	next unless $q->qtype eq "SOA";
    86 
   105 
    87 END {
   106 	my $domain = $q->qname;
    88     if (open(PID, $opt_pidfile)) {
   107 	print "NOTIFY from $src about $domain";
    89 	my $pid = <PID>;
   108 
    90 	close(PID);
   109 	print " OK\n" and next if exists $Seen{$domain};
    91 	unlink $opt_pidfile if $$ == $pid;
   110 	print " OK(new)\n" and next if -f $Cf->zones . "/$domain" and $Seen{$domain} = time;
       
   111 
       
   112 	print " Create....\n";
       
   113 	$ENV{ZONE} = $domain;
       
   114 	$ENV{SOURCE} = $src;
       
   115 	$ENV{DATE} = scalar localtime;
       
   116 	system $Cf->cmd;
    92     }
   117     }
    93 }
   118 }
    94 
   119 
    95 MAIN: {
       
    96 
       
    97 
       
    98     openlog($ME, LOG_PID | LOG_PERROR, LOG_DAEMON);
       
    99     syslog(LOG_NOTICE, "starting");
       
   100 
       
   101     GetOptions(
       
   102 	"help" => \$opt_help,
       
   103 	"logfile=s" => \$opt_logfile,
       
   104 	"follow!" => \$opt_follow,
       
   105 	"update!" => \$opt_update,
       
   106 	"debug!" => \$opt_debug,
       
   107 	"daemon!" => \$opt_daemon,
       
   108 	"pidfile=s" => \$opt_pidfile,
       
   109 	"version!" => \$opt_version,
       
   110 	"zonesdir=s" => \$opt_zonesdir)
       
   111     or die "$ME: Bad Usage\n";
       
   112 
       
   113     if ($opt_help) {
       
   114 	print eval "\"$USAGE\"";
       
   115 	exit 0;
       
   116     }
       
   117 
       
   118     if ($opt_version) {
       
   119 	print "$ME Version: ", '$Id$', "\n";
       
   120 	exit 0;
       
   121     }
       
   122 
       
   123 
       
   124     # Create the PID-File
       
   125     {
       
   126 	open(PID, $_ = ">$opt_pidfile.$$") or die "Can't open $opt_pidfile: $!\n";
       
   127 	print PID "$$\n";
       
   128 	close(PID);
       
   129 
       
   130 	if (!link($_ = "$opt_pidfile.$$", $opt_pidfile)) {
       
   131 	    unlink "$opt_pidfile.$$";
       
   132 	    die "There's another $ME running.  Bad. Stop.";
       
   133 	}
       
   134 	unlink "$opt_pidfile.$$";
       
   135     }
       
   136 
       
   137     if ($opt_daemon) {
       
   138 	my $pid = fork();
       
   139 
       
   140 	if ($pid < 0) {
       
   141 	    die "Can't fork: $!\n";
       
   142 	} 
       
   143 
       
   144 	if ($pid) {
       
   145 	    open(PID, $_ = ">$opt_pidfile") or die "Can't open $_: $!\n";
       
   146 	    print PID "$pid\n";
       
   147 	    close(PID);
       
   148 	    exit 0;
       
   149 	}
       
   150 
       
   151 	close(STDIN); close(STDOUT); close(STDERR);
       
   152     }
       
   153 
       
   154 
       
   155     open (LOGFILE, $_ = "<$opt_logfile") or die "Can't open $_: $!\n";
       
   156 
       
   157     for (;;) {
       
   158 	my (%masters, %missing, %nomasters);
       
   159 	while (<LOGFILE>) {
       
   160 	    
       
   161 	    my ($domain, $ip);
       
   162 	    
       
   163 	    # NOTIFY-Zeilen
       
   164 	    ($domain, $ip) = /NOTIFY.*?\((\S+?),.*?\[([\d.]+)\]/ and do {
       
   165 		if (not exists $auth{$ip}) {
       
   166 		    warn "notify for $domain from unauthorized ip $ip\n";
       
   167 		    next;
       
   168 		};
       
   169 		# also in die Liste (hier als Key eines Hashes wegen der möglichen
       
   170 		# Dopplungen) der Master für diese Domain aufnehmen.
       
   171 		debug("Master für $domain: $ip\n");
       
   172 		$masters{$domain}->{$ip} = 1;
       
   173 		next;
       
   174 	    };
       
   175 
       
   176 	    # Das müssen wir doch garnicht machen, da wir ja sowieso nach 
       
   177 	    # dem Master-Files gucken...
       
   178 	    # NOTIFY for vergessene 
       
   179 	    /NOTIFY for "(\S+?)".*not one of our zones/ and do {
       
   180 		my $domain = $1;
       
   181 		if (not exists $masters{$domain}) {
       
   182 		    debug "skipping $domain (not authorized)\n";
       
   183 		    next;
       
   184 		}
       
   185 		# Also in die Liste der vergessenen Domains (für die wir garkeine
       
   186 		# Konfigurations-Datei haben)
       
   187 		next if exists $missing{$domain};	# schon erledigt
       
   188 
       
   189 		debug("Missing file for $domain\n");
       
   190 		$missing{$domain} = 1;
       
   191 		next;
       
   192 	    };
       
   193 
       
   194 	    # Wenn wir ein "... loaded" finden, dann fehlt das File nicht gänzlich!
       
   195 	    /slave zone "(\S+?)" .*loaded/ and do {
       
   196 		my $domain = $1;
       
   197 		next if not exists $missing{$domain};	# ist noch nicht vermißt worden
       
   198 
       
   199 		debug("Missing file for $domain is not longer missing\n");
       
   200 		delete $missing{$domain};
       
   201 		next;
       
   202 	    };
       
   203 
       
   204 	    /\[([\d.]+)\] not authoritative for (\S+), SOA/ and do {
       
   205 		my ($master, $domain) = ($1, $2);
       
   206 		next if exists $nomasters{$domain}->{$master};
       
   207 
       
   208 		debug "$master isn't a auth. master for $domain\n";
       
   209 		$masters{$domain}->{$master} = 1;	# sieht blöd aus, wird aber gebraucht,
       
   210 							# weil wir nur die bearbeiten, die einen
       
   211 							# master haben
       
   212 		$nomasters{$domain}->{$master} = 1;
       
   213 		next;
       
   214 	    };
       
   215 	}
       
   216 
       
   217 	# Jetzt sind wir erstmal durch und verarbeiten alles
       
   218 	my $changed = 0;
       
   219 	foreach my $domain (sort ($opt_update ? keys %masters : keys %missing)) {
       
   220 	    $changed += updateFile($domain, [keys %{$masters{$domain}}], [keys %{$nomasters{$domain}}]);
       
   221 	    delete $masters{$domain};
       
   222 	    delete $missing{$domain} if exists $missing{$domain};
       
   223 	    delete $nomasters{$domain} if exists $nomasters{$domain};
       
   224 	}
       
   225 
       
   226 	debug "$changed changes."; 
       
   227 	if ($changed) {
       
   228 	    debug("bind reload required\n");
       
   229 	    open(ALL, $_ = ">/etc/bind/zones.all") or die "Can't open $_: $!\n";
       
   230 	    foreach (</etc/bind/zones.d/*>) {
       
   231 		open(IN, $_) or die "Can't open $_: $!\n";
       
   232 		print ALL <IN>;
       
   233 		close(IN);
       
   234 	    }
       
   235 	    system qw(ndc reload);
       
   236 	}
       
   237 
       
   238 	last if !$opt_follow;
       
   239 	syslog LOG_INFO, "Sleeping for $naptime seconds\n";
       
   240 	sleep $naptime;
       
   241 	if ((LOGFILE->stat())[1] != (stat($opt_logfile))[1]) {
       
   242 	    # new file to follow
       
   243 	    syslog(LOG_NOTICE, "Logfile changed, re-open it.\n");
       
   244 	    open(LOGFILE, $_ = "<$opt_logfile") 
       
   245 		or die "Can't open $_: $!\n";
       
   246 	} else {
       
   247 	    LOGFILE->clearerr();
       
   248 	}
       
   249     }
       
   250 }
       
   251 
       
   252 sub updateFile($$$)
       
   253 {
       
   254     local $_;
       
   255     my $domain = $_[0];
       
   256     my @new_masters = @{$_[1]};
       
   257     my @no_masters = @{$_[2]};
       
   258 
       
   259     my %masters = ();
       
   260     my $masters;
       
   261 
       
   262     my $file = "$opt_zonesdir/$domain";
       
   263 
       
   264     debug "updateFile: $domain, @new_masters, @no_masters\n";
       
   265 
       
   266     if (-f $file) {
       
   267 	# Das File ist also schon da, wir müssen nur mal gucken, ob die Master,
       
   268 	# von denen wir ein NOTIFY erhalten haben, auch in unserer Datei stehen.
       
   269 	#
       
   270 	open (F, $_ = "+<$file") or die "Can't open $_: $!\n";
       
   271 	flock(F, LOCK_EX); seek(F, 0, 0);
       
   272 
       
   273 	$_ = join "", <F>;
       
   274 
       
   275 	# Liste der Master raussuchen, darus noch die löschen, die uns
       
   276 	# die Mitarbeit verweigert haben..
       
   277 	/^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)(\s*\/\/.*?\n)/ims;
       
   278 	%masters = map { $_, 1 } split/\s*;\s*/, $2;
       
   279 	$masters = %masters;	# für den späteren Vergleich
       
   280 
       
   281 	# noch unsere neuen hinzufügen...
       
   282 	@masters{@new_masters} = map { 1 } @new_masters;
       
   283 
       
   284 	# nun die weg, die sich nicht zuständig fühlen
       
   285 	delete @masters{@no_masters};
       
   286 
       
   287 	# Wenn sich nach alldem nichts verändert hat, haben wir fertig.
       
   288 	if ($masters eq %masters) {
       
   289 	    debug("File is up-to-date for $domain\n");
       
   290 	    syslog(LOG_NOTICE, "No changes made for $domain (no \"loaded\" seen, defective master?)\n")
       
   291 		unless $opt_update;
       
   292 	    close(F);
       
   293 	    return 0;
       
   294 	}
       
   295 
       
   296 	if (not %masters) {
       
   297 	    syslog LOG_NOTICE, "REMOVING $file (empty masters list)\n";
       
   298 	    close F;
       
   299 	    unlink $file;
       
   300 	    return 1;
       
   301 	}
       
   302 
       
   303 	$masters = join ";", keys %masters;
       
   304 	syslog(LOG_NOTICE, "Updated masters ($masters) list for $domain\n");
       
   305 	s/^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)/$1$masters;$3/ims;
       
   306 
       
   307 	truncate(F, 0);
       
   308 	seek(F, 0, 0);
       
   309 	print F;
       
   310 	close F;
       
   311 
       
   312 	return 1;
       
   313     } 
       
   314 
       
   315 
       
   316     my $date = localtime();
       
   317     my %new_masters = map { $_, 1 } @new_masters;
       
   318     delete @new_masters{@no_masters};
       
   319 
       
   320     if (not %new_masters) {
       
   321 	syslog LOG_INFO, "not creating $file (empty masters list)\n";
       
   322 	return 0;
       
   323     }
       
   324 
       
   325     $masters = join "; ", @new_masters;
       
   326 
       
   327     -d $opt_zonesdir or mkpath($opt_zonesdir, 0, 0755);
       
   328 
       
   329     syslog(LOG_NOTICE, "Creating $file for $domain");
       
   330     open(OUT, $_ = ">$file") or die "Can't open $_: $!\n";
       
   331 
       
   332 	print OUT <<_EOF_;
       
   333 // Autogenerated by $ME: $date
       
   334 zone "$domain" {
       
   335     type slave;
       
   336     masters { $masters; };  // $date
       
   337     file "/etc/bind/slave/$domain";
       
   338     allow-query { any; };
       
   339     allow-transfer { none; };
       
   340     allow-update { none; };
       
   341 };
       
   342 
       
   343 _EOF_
       
   344     close OUT;
       
   345 
       
   346     return 1;
       
   347 }
       
   348 	
       
   349 
       
   350 # vim:sts=4 sw=4 aw ai sm:
   120 # vim:sts=4 sw=4 aw ai sm: