1 #! /usr/bin/perl -w |
|
2 # $Id$ |
|
3 my $USAGE = <<'#'; |
|
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 |
|
42 use strict; |
|
43 use File::Basename; |
|
44 use IO::Handle; |
|
45 use Fcntl qw/:flock/; |
|
46 use File::Path; |
|
47 use Getopt::Long; |
|
48 use Unix::Syslog qw/:macros :subs/; |
|
49 |
|
50 my $ME = basename $0; |
|
51 |
|
52 |
|
53 my %auth = ( |
|
54 "212.80.235.130" => "pu.schlittermann.de", |
|
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.proton24.de", |
|
62 # "194.162.141.17" => "dalx1.nacamar.de", |
|
63 ); |
|
64 |
|
65 $SIG{__DIE__} = sub { syslog(LOG_ERR, $_[0]); exit -1; }; |
|
66 $SIG{__WARN__} = sub { syslog(LOG_WARNING, $_[0]); }; |
|
67 $SIG{TERM} = $SIG{INT} = sub { exit 0; }; |
|
68 |
|
69 my %seen; |
|
70 |
|
71 my $opt_help = 0; |
|
72 my $opt_pidfile = "/var/run/$ME.pid"; |
|
73 my $opt_logfile = "/var/log/syslog"; |
|
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 |
|
81 my $naptime = 60; |
|
82 |
|
83 |
|
84 sub updateFile($$$); |
|
85 sub debug($;@) { syslog(LOG_DEBUG, "DEBUG " . shift @_, @_) if $opt_debug; } |
|
86 |
|
87 END { |
|
88 open(PID, $opt_pidfile); |
|
89 my $pid = <PID>; |
|
90 close(PID); |
|
91 unlink $opt_pidfile if $$ == $pid; |
|
92 } |
|
93 |
|
94 MAIN: { |
|
95 |
|
96 |
|
97 openlog($ME, LOG_PID | LOG_PERROR, LOG_DAEMON); |
|
98 syslog(LOG_NOTICE, "starting"); |
|
99 |
|
100 GetOptions( |
|
101 "help" => \$opt_help, |
|
102 "logfile=s" => \$opt_logfile, |
|
103 "follow!" => \$opt_follow, |
|
104 "update!" => \$opt_update, |
|
105 "debug!" => \$opt_debug, |
|
106 "daemon!" => \$opt_daemon, |
|
107 "pidfile=s" => \$opt_pidfile, |
|
108 "version!" => \$opt_version, |
|
109 "zonesdir=s" => \$opt_zonesdir) |
|
110 or die "$ME: Bad Usage\n"; |
|
111 |
|
112 if ($opt_help) { |
|
113 print eval "\"$USAGE\""; |
|
114 exit 0; |
|
115 } |
|
116 |
|
117 if ($opt_version) { |
|
118 print "$ME Version: ", '$Id$', "\n"; |
|
119 exit 0; |
|
120 } |
|
121 |
|
122 |
|
123 # Create the PID-File |
|
124 { |
|
125 open(PID, $_ = ">$opt_pidfile.$$") or die "Can't open $opt_pidfile: $!\n"; |
|
126 print PID "$$\n"; |
|
127 close(PID); |
|
128 |
|
129 if (!link($_ = "$opt_pidfile.$$", $opt_pidfile)) { |
|
130 unlink "$opt_pidfile.$$"; |
|
131 die "There's another $ME running. Bad. Stop."; |
|
132 } |
|
133 unlink "$opt_pidfile.$$"; |
|
134 } |
|
135 |
|
136 if ($opt_daemon) { |
|
137 my $pid = fork(); |
|
138 |
|
139 if ($pid < 0) { |
|
140 die "Can't fork: $!\n"; |
|
141 } |
|
142 |
|
143 if ($pid) { |
|
144 open(PID, $_ = ">$opt_pidfile") or die "Can't open $_: $!\n"; |
|
145 print PID "$pid\n"; |
|
146 close(PID); |
|
147 exit 0; |
|
148 } |
|
149 |
|
150 close(STDIN); close(STDOUT); close(STDERR); |
|
151 } |
|
152 |
|
153 |
|
154 open (LOGFILE, $_ = "<$opt_logfile") or die "Can't open $_: $!\n"; |
|
155 |
|
156 for (;;) { |
|
157 my (%masters, %missing, %nomasters); |
|
158 while (<LOGFILE>) { |
|
159 |
|
160 my ($domain, $ip); |
|
161 |
|
162 # NOTIFY-Zeilen |
|
163 ($domain, $ip) = /NOTIFY.*?\((\S+?),.*?\[([\d.]+)\]/ and do { |
|
164 if (not exists $auth{$ip}) { |
|
165 warn "notify for $domain from unauthorized ip $ip\n"; |
|
166 next; |
|
167 }; |
|
168 # also in die Liste (hier als Key eines Hashes wegen der möglichen |
|
169 # Dopplungen) der Master für diese Domain aufnehmen. |
|
170 debug("Master für $domain: $ip\n"); |
|
171 $masters{$domain}->{$ip} = 1; |
|
172 next; |
|
173 }; |
|
174 |
|
175 # Das müssen wir doch garnicht machen, da wir ja sowieso nach |
|
176 # dem Master-Files gucken... |
|
177 # NOTIFY for vergessene |
|
178 /NOTIFY for "(\S+?)".*not one of our zones/ and do { |
|
179 my $domain = $1; |
|
180 if (not exists $masters{$domain}) { |
|
181 debug "skipping $domain (not authorized)\n"; |
|
182 next; |
|
183 } |
|
184 # Also in die Liste der vergessenen Domains (für die wir garkeine |
|
185 # Konfigurations-Datei haben) |
|
186 next if exists $missing{$domain}; # schon erledigt |
|
187 |
|
188 debug("Missing file for $domain\n"); |
|
189 $missing{$domain} = 1; |
|
190 next; |
|
191 }; |
|
192 |
|
193 # Wenn wir ein "... loaded" finden, dann fehlt das File nicht gänzlich! |
|
194 /slave zone "(\S+?)" .*loaded/ and do { |
|
195 my $domain = $1; |
|
196 next if not exists $missing{$domain}; # ist noch nicht vermißt worden |
|
197 |
|
198 debug("Missing file for $domain is not longer missing\n"); |
|
199 delete $missing{$domain}; |
|
200 next; |
|
201 }; |
|
202 |
|
203 /\[([\d.]+)\] not authoritative for (\S+), SOA/ and do { |
|
204 my ($master, $domain) = ($1, $2); |
|
205 next if exists $nomasters{$domain}->{$master}; |
|
206 |
|
207 debug "$master isn't a auth. master for $domain\n"; |
|
208 $masters{$domain}->{$master} = 1; # sieht blöd aus, wird aber gebraucht, |
|
209 # weil wir nur die bearbeiten, die einen |
|
210 # master haben |
|
211 $nomasters{$domain}->{$master} = 1; |
|
212 next; |
|
213 }; |
|
214 } |
|
215 |
|
216 # Jetzt sind wir erstmal durch und verarbeiten alles |
|
217 my $changed = 0; |
|
218 foreach my $domain (sort ($opt_update ? keys %masters : keys %missing)) { |
|
219 $changed += updateFile($domain, [keys %{$masters{$domain}}], [keys %{$nomasters{$domain}}]); |
|
220 delete $masters{$domain}; |
|
221 delete $missing{$domain} if exists $missing{$domain}; |
|
222 delete $nomasters{$domain} if exists $nomasters{$domain}; |
|
223 } |
|
224 |
|
225 debug "$changed changes."; |
|
226 if ($changed) { |
|
227 debug("bind reload required\n"); |
|
228 open(ALL, $_ = ">/etc/bind/zones.all") or die "Can't open $_: $!\n"; |
|
229 foreach (</etc/bind/zones.d/*>) { |
|
230 open(IN, $_) or die "Can't open $_: $!\n"; |
|
231 print ALL <IN>; |
|
232 close(IN); |
|
233 } |
|
234 system qw(ndc reload); |
|
235 } |
|
236 |
|
237 last if !$opt_follow; |
|
238 syslog LOG_INFO, "Sleeping for $naptime seconds\n"; |
|
239 sleep $naptime; |
|
240 if ((LOGFILE->stat())[1] != (stat($opt_logfile))[1]) { |
|
241 # new file to follow |
|
242 syslog(LOG_NOTICE, "Logfile changed, re-open it.\n"); |
|
243 open(LOGFILE, $_ = "<$opt_logfile") |
|
244 or die "Can't open $_: $!\n"; |
|
245 } else { |
|
246 LOGFILE->clearerr(); |
|
247 } |
|
248 } |
|
249 } |
|
250 |
|
251 sub updateFile($$$) |
|
252 { |
|
253 local $_; |
|
254 my $domain = $_[0]; |
|
255 my @new_masters = @{$_[1]}; |
|
256 my @no_masters = @{$_[2]}; |
|
257 |
|
258 my %masters = (); |
|
259 my $masters; |
|
260 |
|
261 my $file = "$opt_zonesdir/$domain"; |
|
262 |
|
263 debug "updateFile: $domain, @new_masters, @no_masters\n"; |
|
264 |
|
265 if (-f $file) { |
|
266 # Das File ist also schon da, wir müssen nur mal gucken, ob die Master, |
|
267 # von denen wir ein NOTIFY erhalten haben, auch in unserer Datei stehen. |
|
268 # |
|
269 open (F, $_ = "+<$file") or die "Can't open $_: $!\n"; |
|
270 flock(F, LOCK_EX); seek(F, 0, 0); |
|
271 |
|
272 $_ = join "", <F>; |
|
273 |
|
274 # Liste der Master raussuchen, darus noch die löschen, die uns |
|
275 # die Mitarbeit verweigert haben.. |
|
276 /^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)(\s*\/\/.*?\n)/ims; |
|
277 %masters = map { $_, 1 } split/\s*;\s*/, $2; |
|
278 $masters = %masters; # für den späteren Vergleich |
|
279 |
|
280 # noch unsere neuen hinzufügen... |
|
281 @masters{@new_masters} = map { 1 } @new_masters; |
|
282 |
|
283 # nun die weg, die sich nicht zuständig fühlen |
|
284 delete @masters{@no_masters}; |
|
285 |
|
286 # Wenn sich nach alldem nichts verändert hat, haben wir fertig. |
|
287 if ($masters eq %masters) { |
|
288 debug("File is up-to-date for $domain\n"); |
|
289 syslog(LOG_NOTICE, "No changes made for $domain (no \"loaded\" seen, defective master?)\n") |
|
290 unless $opt_update; |
|
291 close(F); |
|
292 return 0; |
|
293 } |
|
294 |
|
295 if (not %masters) { |
|
296 syslog LOG_NOTICE, "REMOVING $file (empty masters list)\n"; |
|
297 close F; |
|
298 unlink $file; |
|
299 return 1; |
|
300 } |
|
301 |
|
302 $masters = join ";", keys %masters; |
|
303 syslog(LOG_NOTICE, "Updated masters ($masters) list for $domain\n"); |
|
304 s/^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)/$1$masters;$3/ims; |
|
305 |
|
306 truncate(F, 0); |
|
307 seek(F, 0, 0); |
|
308 print F; |
|
309 close F; |
|
310 |
|
311 return 1; |
|
312 } |
|
313 |
|
314 |
|
315 my $date = localtime(); |
|
316 my %new_masters = map { $_, 1 } @new_masters; |
|
317 delete @new_masters{@no_masters}; |
|
318 |
|
319 if (not %new_masters) { |
|
320 syslog LOG_INFO, "not creating $file (empty masters list)\n"; |
|
321 return 0; |
|
322 } |
|
323 |
|
324 $masters = join "; ", @new_masters; |
|
325 |
|
326 -d $opt_zonesdir or mkpath($opt_zonesdir, 0, 0755); |
|
327 |
|
328 syslog(LOG_NOTICE, "Creating $file for $domain"); |
|
329 open(OUT, $_ = ">$file") or die "Can't open $_: $!\n"; |
|
330 |
|
331 print OUT <<_EOF_; |
|
332 // Autogenerated by $ME: $date |
|
333 zone "$domain" { |
|
334 type slave; |
|
335 masters { $masters; }; // $date |
|
336 file "/etc/bind/slave/$domain"; |
|
337 allow-query { any; }; |
|
338 allow-transfer { none; }; |
|
339 allow-update { none; }; |
|
340 }; |
|
341 |
|
342 _EOF_ |
|
343 close OUT; |
|
344 |
|
345 return 1; |
|
346 } |
|
347 |
|
348 |
|
349 # vim:sts=4 sw=4 aw ai sm: |
|