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: |