source: trunk/bind-import@ 815

Last change on this file since 815 was 815, checked in by Kris Deugau, 4 years ago

/trunk

Eigth sampled iteration of bind-import

File size: 10.6 KB
RevLine 
[808]1#!/usr/bin/perl
2# Import a BIND zone file
3##
4# Copyright 2020 Kris Deugau <kdeugau@deepnet.cx>
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18##
19
20use strict;
21use warnings;
22use Data::Dumper;
23
24use lib '.';
25use DNSDB;
26
27my $dnsdb = new DNSDB;
[810]28my $doimport = 0;
[808]29
30#print Dumper(\%reverse_typemap);
31
32my $zname = shift @ARGV;
[813]33die "usage: bind-import zonename\n" if !$zname;
[808]34my $rev = 'n';
35my $zid;
36
[810]37my %amap;
38my %namemap;
[811]39my %cmap;
[810]40
[815]41##fixme: this is wrong, BIND zone files are generally complete and we're adding. merging records is an entire fridge full of worms.
[809]42if ($zname =~ /\.arpa\.?$/ || $zname =~ m,^[\d./]+$,) {
[808]43 $rev = 'y';
[809]44 $zname = _zone2cidr($zname) if $zname =~ /\.arpa\.?$/;
[808]45 $zid = $dnsdb->revID($zname,':ANY:');
[809]46 if ($zid) {
47 $zname = new NetAddr::IP $zname;
48 $zname = DNSDB::_ZONE($zname, 'ZONE', 'r', '.').($zname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
49 }
[808]50} else {
51 $zid = $dnsdb->domainID($zname,':ANY:');
52}
53
54die "zone $zname not on file\n" if !$zid;
55
[810]56# still no sane way to expose a human-friendly view tag on the command line.
57my $view = shift @ARGV;
58$view = '' if !$view;
59
[808]60##fixme: retrieve defttl from SOA record
61my $zonettl = 900;
[810]62my $defttl = $zonettl;
[815]63my $origin = $zname; # to append to unqualified names
[808]64
[810]65# need to spin up a full state machine-ish thing, because BIND zone files are all about context
[812]66# see ch4, p56-72 in the grasshopper book
67my $prevlabel = '';
68my $curlabel = '';
69
[813]70my $i = 0;
71
[808]72while (<>) {
[810]73 chomp;
[808]74 next if /^\s*$/;
[813]75 next if /^\s*;/; # comments
[815]76 next if /^\s*\)/; # SOA closing. arguably should do some more targeted voodoo when parsing the SOA details
[813]77
[815]78print "($_)\n";
[812]79 if (my ($macro,$mdetail) = (/^\s*\$(TTL|ORIGIN|INCLUDE|GENERATE)\s+(.+)/) ) {
[810]80 # macro sort of thing; $TTL and $ORIGIN most common. $INCLUDE is a thing, expect it to be rare in live use tho
81 if ($macro eq 'TTL') {
[815]82 $mdetail =~ s/\s*;.+$//;
[810]83 if ($mdetail =~ /^\d+$/) {
84 $defttl = $mdetail;
85 } else {
86 warn "invalid \$TTL: $_\n";
87 }
88 } elsif ($macro eq 'ORIGIN') {
89##fixme: going to skip the stupid case of "$ORIGIN com." and the like that lie
90# between . and the root domain we were told we're importing; anyone using such
91# a mess outside the root servers is clearly insane
[815]92
93# $ORIGIN supports cascading/nesting, by watching for fully-qualified names vs partial names.
94
95print "origin ($mdetail)\n";
96 if ($mdetail =~ /\.$/) {
97 $origin = $mdetail;
[810]98 } else {
[815]99 # append current origin to unqualified origin
100 $origin = "$mdetail.$origin";
[810]101 }
[815]102
103# if ($mdetail eq '.' || $mdetail =~ /$zname\.$/ || $zname =~ /$mdetail\.$/) {
104# $origin = $mdetail;
105# } else {
106# # if we continue, we either use an $ORIGIN that's out of zone, or ignore it and potentially publish incorrect records.
107# die "bad \$ORIGIN: $_\n";
108# }
109
[810]110 }
[812]111 # not handling $INCLUDE or $GENERATE (altho the latter seems to be mostly a less-flexible version of the template types)
[810]112 next;
113 }
[815]114
[813]115 my $origrec = $_;
[815]116
[810]117 # skip stale records that have no value
118 next if /^ip-192-168-1(12|20)-\d+/;
119 next if /ip.add.re.\d+\s*$/;
[813]120
[815]121 # leading whitespace indicates "same label as last record"
122 if (/^\s/) {
123 $curlabel = $prevlabel;
124 } else {
125 ($curlabel) = /^([\w\@_.-]+)\s/;
126 }
127
128 # magic name!
129 $curlabel = "$zname." if $curlabel eq '@';
130
131 # append $ORIGIN if name is not fully qualified.
132 if ($curlabel !~ /\.$/) {
133 $curlabel .= $origin;
134 }
135
136 # check for zone scope. skip bad records.
137 if ($curlabel !~ /$zname.$/) {
138 warn "bad record $origrec, maybe bad \$ORIGIN?\n";
139 next;
140 }
141
142# # records must begin in the first column, no leading whitespace
143# my ($name) = /^([\w\@_.-]+)\s/;
144
[813]145# foo IN A 1.2.3.4
146# IN A 2.3.4.5
147# =
148# foo.zone. IN A 1.2.3.4
149# foo.zone. IN A 2.3.4.5
150
[815]151# # "empty" label records inherit the previous label
152# # RRs start in the first column by definition, so leading whitespace indicates an inherited label
153# if (/^\s+/) {
154# # fatal error. if there is no previous label, we can by definition not set
155# # the current label based on it. this can only happen on the very first
156# # record, following records will *ALWAYS* have a previous label
157# die "bad first record ($_): no previous label\n" if !$prevlabel;
158# $name = $prevlabel;
159# }
[813]160
[815]161print "$i ($_)\n\t$curlabel";
[814]162
[813]163
[814]164
165
[815]166# # append zone name to record name if missing AND not dot-terminated;
167# # this happens automagically for forward zones, but not reverse because Reasons. (fixme?)
168# # suck up and deal with the error if the dot-termiated name is out of zone; should be
169# # impossible with valid BIND zone file but...
170# if ($name !~ /\.$/) {
171# $name .= ".$zname" if $name !~ /$zname$/;
172# } else {
173# warn "skipping out-of-zone record:\n\t($_)\n" if $name !~ /$zname\.$/;
174# next;
175# }
[813]176
[815]177last if ++$i > 5;
[813]178
179
[812]180 s/^([\w\@_.-]+)\s+//;
[813]181
[815]182my $nc = 0;
183my %seenatoms;
184my $badrec;
185my $curatom = 'class';
186##fixme: maybe wrap this in an eval() instead of the warn/badrec/last bits?
187for (my ($atom) = /^([\w\d]+)\s/; $nc < 3; $nc++) {
188 if ($atom =~ /^\d+$/) {
189 if (defined($seenatoms{ttl})) {
190 warn "bad record ($origrec)\n";
191 $badrec = 1;
192 last;
193 } else {
194 if ($curatom ne 'class' && $curatom ne 'ttl') {
195 warn "bad record ($origrec)\n";
196 $badrec = 1;
197 last;
198 }
199 $curatom = 'ttl';
200 $seenatoms{ttl} = $atom;
201 }
202 }
203 if ($atom =~ /^IN|CS|CH|HS$/) {
204 if ($atom =~ /CS|CH|HS/) {
205 warn "unsupported class $atom in record ($origrec)\n";
206 $badrec = 1;
207 last;
208 }
209 $curatom = 'class';
210 }
211 if ($reverse_typemap{$atom}) {
212 print "dbg: type $atom\n";
213 }
214# my $itype = $reverse_typemap{$type};
215}
216
217next if $badrec;
218
219
[814]220##fixme: drop curlabel? not sure it's needed
[815]221#$curlabel = $name;
[813]222$prevlabel = $curlabel;
[814]223##todo: BIND conflates a repeated label with repeating the TTL too. Matter of opinion whether that's really correct or not.
224
225
[815]226
[812]227# by convention the optional TTL leads the optional class, but they're apparently swappable.
228 my ($ttl) = /^(\d+)?\s/;
229 if (defined $ttl) {
230 # TTL may be zero
231 s/(\d+)?\s+//;
232 } else {
233 # Fall back to zone default TTL
234 $ttl = $zonettl;
235 }
236 my ($class) = /^(IN|CS|CH|HS|\d+)\s/;
237 if (defined $class) {
238 if ($class =~ /\d+/) {
239
240 }
[808]241 if ($class ne 'IN') {
[813]242 warn "Non-Internet class ($class) records not supported:\n\t$origrec\n";
[808]243 next;
244 }
245 s/(IN|CS|CH|HS)\s+//;
246 } else {
[812]247 $class = 'IN';
[808]248 }
249 my ($type) = /([A-Z-]+)\s/;
250 if (!$reverse_typemap{$type}) {
[813]251 warn "Unknown type $type, skipping\n\t($_)\n";
[808]252 next;
253 }
254 my $itype = $reverse_typemap{$type};
255 s/([A-Z-]+)\s+//;
256 chomp;
257 my $rdata = $_;
258
[812]259 # SOA is the only type that may span multiple lines. Probably. Note even AXFRed zones write multiline SOA records:
260 #@ IN SOA test.example.invalid. test.example.invalid. (2020082500 7200 900 604800 3600)
261 # IN NS olddns.example.com.
262 # IN MX 1 fred.foo.bar.invalid.
263 #foo IN A 192.168.16.45
264 # AXFR'ed zone file gets written as
265 #$ORIGIN .
266 #$TTL 3600 ; 1 hour
267 #example.invalid IN SOA test.example.invalid. test.example.invalid. (
268 # 2020082500 ; serial
269 # 7200 ; refresh (2 hours)
270 # 900 ; retry (15 minutes)
271 # 604800 ; expire (1 week)
272 # 3600 ; minimum (1 hour)
273 # )
274 # NS olddns.example.com.
275 # MX 1 fred.foo.bar.invalid.
276 #$ORIGIN example.invalid.
277 #foo A 192.168.16.45
278
279 if ($type eq 'SOA') {
[813]280 my ($ns, $adminmail) = ($rdata =~ /([\w.]+)\s+([\w.]+)\s+\(/);
281 die "Can't parse gibberish SOAish record: $_\n" if !$ns;
282 $rdata =~ s/([\w.]+)\s+([\w.]+)\s+\(\s*//;
[812]283
[813]284 # There are probably more efficient ways to do this but the SOA record
285 # format is essentially character based, not line-based.
286 # In theory the SOA serial etc may be spread over up to 5 lines, in any combination.
[812]287
[813]288 # Parse fields from $rdata if present
289 my @soabits;
290 my @soafirst = split /\s+/, $rdata;
291 while (my $f = shift @soafirst) {
292 last if $f !~ /^\d/;
293 push @soabits, $f;
294 }
295
296 # Read more lines if we don't have enough SOA fields filled
297 while (scalar(@soabits) < 5) {
298 my $tmp = <>;
299 $tmp =~ s/^\s*//;
300 my @tmpsoa = split /\s+/, $tmp;
301 while (my $f = shift @tmpsoa) {
302 last if $f !~ /^\d/;
303 push @soabits, $f;
304 }
305 if (scalar(@soabits) == 5) {
306 last;
307 }
308 }
309 } # SOA
310
[808]311 # Quotes may arguably be syntactically required, but they're not actually part of the record data
[813]312 elsif ($type eq 'TXT') {
[808]313 $rdata =~ s/^"//;
314 $rdata =~ s/"$//;
315 }
316
[811]317# temp hack for hosts file
[813]318elsif ($type eq 'A') {
[810]319# if ($amap{$name}) {
320# print "urp: dupe name $name $rdata\n";
321# } else {
[815]322 push @{$amap{$curlabel}}, $rdata;
[810]323# }
[815]324 push @{$namemap{$rdata}}, $curlabel;
[810]325}
[813]326elsif ($type eq 'CNAME') {
[815]327 push @{$cmap{$rdata}}, $curlabel;
[811]328}
[810]329
[808]330no warnings qw(uninitialized);
[810]331#print "parsed: '$name' '$class' '$ttl' '$type'->'$itype' '$rdata'\n";
[808]332#print;
333#;imap IN 900 CNAME deepnet.cx.
334##fixme: not sure how to handle the case where someone leaves off the class.
[810]335 if ($doimport) {
336 my ($code, $msg);
337 if ($rev eq 'n') {
[815]338 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$curlabel, \$itype, \$rdata, $ttl);
[810]339 } else {
[815]340 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$rdata, \$itype, \$curlabel, $ttl);
[810]341 }
342 print "$code: $msg\n";
[809]343 }
[813]344 $i++;
[808]345}
[810]346
347
348#print Dumper \%amap;
[811]349#print Dumper \%namemap;
350#print Dumper \%cmap;
351
[810]352foreach my $n (keys %amap) {
353 foreach my $ip (@{$amap{$n}}) {
354#print "$ip $n\n";
355 push @{$namemap{$ip}}, $n unless grep $n, @{$namemap{$ip}};
356 }
357}
358
[811]359foreach my $c (keys %cmap) {
360 if ($amap{$c}) {
361 print Dumper(\@{$amap{$c}});
362 }
363# print $amap{$c};
[810]364}
[811]365
366# cname targ -> IP
367
368#foreach my $ip (sort keys %namemap) {
369# print "$ip ".join(' ', @{$namemap{$ip}})."\n";
370#}
371
Note: See TracBrowser for help on using the repository browser.