source: trunk/bind-import@ 814

Last change on this file since 814 was 814, checked in by Kris Deugau, 3 years ago

/trunk

Seventh sampled iteration of bind-import

File size: 8.9 KB
Line 
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;
28my $doimport = 0;
29
30#print Dumper(\%reverse_typemap);
31
32my $zname = shift @ARGV;
33die "usage: bind-import zonename\n" if !$zname;
34my $rev = 'n';
35my $zid;
36
37my %amap;
38my %namemap;
39my %cmap;
40
41if ($zname =~ /\.arpa\.?$/ || $zname =~ m,^[\d./]+$,) {
42 $rev = 'y';
43 $zname = _zone2cidr($zname) if $zname =~ /\.arpa\.?$/;
44 $zid = $dnsdb->revID($zname,':ANY:');
45 if ($zid) {
46 $zname = new NetAddr::IP $zname;
47 $zname = DNSDB::_ZONE($zname, 'ZONE', 'r', '.').($zname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
48 }
49} else {
50 $zid = $dnsdb->domainID($zname,':ANY:');
51}
52
53die "zone $zname not on file\n" if !$zid;
54
55# still no sane way to expose a human-friendly view tag on the command line.
56my $view = shift @ARGV;
57$view = '' if !$view;
58
59##fixme: retrieve defttl from SOA record
60my $zonettl = 900;
61my $defttl = $zonettl;
62my $recbase = $zname; # to append to unqualified names
63
64# need to spin up a full state machine-ish thing, because BIND zone files are all about context
65# see ch4, p56-72 in the grasshopper book
66my $prevlabel = '';
67my $curlabel = '';
68
69my $i = 0;
70
71while (<>) {
72 chomp;
73 next if /^\s*$/;
74 next if /^\s*;/; # comments
75 next if /^\s*\(/; # SOA closing. arguably should do some more targeted voodoo when parsing the SOA details
76
77 if (my ($macro,$mdetail) = (/^\s*\$(TTL|ORIGIN|INCLUDE|GENERATE)\s+(.+)/) ) {
78 # macro sort of thing; $TTL and $ORIGIN most common. $INCLUDE is a thing, expect it to be rare in live use tho
79 if ($macro eq 'TTL') {
80 if ($mdetail =~ /^\d+$/) {
81 $defttl = $mdetail;
82 } else {
83 warn "invalid \$TTL: $_\n";
84 }
85 } elsif ($macro eq 'ORIGIN') {
86##fixme: going to skip the stupid case of "$ORIGIN com." and the like that lie
87# between . and the root domain we were told we're importing; anyone using such
88# a mess outside the root servers is clearly insane
89# handled cases:
90# $ORIGIN .
91# $ORIGIN [zonedomain].
92# $ORIGIN [subdomain.zonedomain].
93 if ($mdetail eq '.' || $mdetail =~ /$zname\.$/ || $zname =~ /$mdetail\.$/) {
94 $recbase = $mdetail;
95 } else {
96 # if we continue, we either use an $ORIGIN that's out of zone, or ignore it and potentially publish incorrect records.
97 die "bad \$ORIGIN: $_\n";
98 }
99 }
100 # not handling $INCLUDE or $GENERATE (altho the latter seems to be mostly a less-flexible version of the template types)
101 next;
102 }
103 my $origrec = $_;
104 # skip stale records that have no value
105 next if /^ip-192-168-1(12|20)-\d+/;
106 next if /ip.add.re.\d+\s*$/;
107 # records must begin in the first column, no leading whitespace
108 my ($name) = /^([\w\@_.-]+)\s/;
109
110# foo IN A 1.2.3.4
111# IN A 2.3.4.5
112# =
113# foo.zone. IN A 1.2.3.4
114# foo.zone. IN A 2.3.4.5
115
116 # "empty" label records inherit the previous label
117 # RRs start in the first column by definition, so leading whitespace indicates an inherited label
118 if (/^\s+/) {
119 # fatal error. if there is no previous label, we can by definition not set
120 # the current label based on it. this can only happen on the very first
121 # record, following records will *ALWAYS* have a previous label
122 die "bad first record ($_): no previous label\n" if !$prevlabel;
123 $name = $prevlabel;
124 }
125
126print "$i ($_)\n\t$name";
127
128 # magic name!
129 $name = $zname if $name eq '@';
130
131
132
133 # append zone name to record name if missing AND not dot-terminated;
134 # this happens automagically for forward zones, but not reverse because Reasons. (fixme?)
135 # suck up and deal with the error if the dot-termiated name is out of zone; should be
136 # impossible with valid BIND zone file but...
137 if ($name !~ /\.$/) {
138 $name .= ".$zname" if $name !~ /$zname$/;
139 } else {
140 warn "skipping out-of-zone record:\n\t($_)\n" if $name !~ /$zname\.$/;
141 next;
142 }
143
144
145last if $i > 5;
146
147 s/^([\w\@_.-]+)\s+//;
148
149##fixme: drop curlabel? not sure it's needed
150$curlabel = $name;
151$prevlabel = $curlabel;
152##todo: BIND conflates a repeated label with repeating the TTL too. Matter of opinion whether that's really correct or not.
153
154
155# by convention the optional TTL leads the optional class, but they're apparently swappable.
156 my ($ttl) = /^(\d+)?\s/;
157 if (defined $ttl) {
158 # TTL may be zero
159 s/(\d+)?\s+//;
160 } else {
161 # Fall back to zone default TTL
162 $ttl = $zonettl;
163 }
164 my ($class) = /^(IN|CS|CH|HS|\d+)\s/;
165 if (defined $class) {
166 if ($class =~ /\d+/) {
167
168 }
169 if ($class ne 'IN') {
170 warn "Non-Internet class ($class) records not supported:\n\t$origrec\n";
171 next;
172 }
173 s/(IN|CS|CH|HS)\s+//;
174 } else {
175 $class = 'IN';
176 }
177 my ($type) = /([A-Z-]+)\s/;
178 if (!$reverse_typemap{$type}) {
179 warn "Unknown type $type, skipping\n\t($_)\n";
180 next;
181 }
182 my $itype = $reverse_typemap{$type};
183 s/([A-Z-]+)\s+//;
184 chomp;
185 my $rdata = $_;
186
187 # SOA is the only type that may span multiple lines. Probably. Note even AXFRed zones write multiline SOA records:
188 #@ IN SOA test.example.invalid. test.example.invalid. (2020082500 7200 900 604800 3600)
189 # IN NS olddns.example.com.
190 # IN MX 1 fred.foo.bar.invalid.
191 #foo IN A 192.168.16.45
192 # AXFR'ed zone file gets written as
193 #$ORIGIN .
194 #$TTL 3600 ; 1 hour
195 #example.invalid IN SOA test.example.invalid. test.example.invalid. (
196 # 2020082500 ; serial
197 # 7200 ; refresh (2 hours)
198 # 900 ; retry (15 minutes)
199 # 604800 ; expire (1 week)
200 # 3600 ; minimum (1 hour)
201 # )
202 # NS olddns.example.com.
203 # MX 1 fred.foo.bar.invalid.
204 #$ORIGIN example.invalid.
205 #foo A 192.168.16.45
206
207 if ($type eq 'SOA') {
208 my ($ns, $adminmail) = ($rdata =~ /([\w.]+)\s+([\w.]+)\s+\(/);
209 die "Can't parse gibberish SOAish record: $_\n" if !$ns;
210 $rdata =~ s/([\w.]+)\s+([\w.]+)\s+\(\s*//;
211
212 # There are probably more efficient ways to do this but the SOA record
213 # format is essentially character based, not line-based.
214 # In theory the SOA serial etc may be spread over up to 5 lines, in any combination.
215
216 # Parse fields from $rdata if present
217 my @soabits;
218 my @soafirst = split /\s+/, $rdata;
219 while (my $f = shift @soafirst) {
220 last if $f !~ /^\d/;
221 push @soabits, $f;
222 }
223
224 # Read more lines if we don't have enough SOA fields filled
225 while (scalar(@soabits) < 5) {
226 my $tmp = <>;
227 $tmp =~ s/^\s*//;
228 my @tmpsoa = split /\s+/, $tmp;
229 while (my $f = shift @tmpsoa) {
230 last if $f !~ /^\d/;
231 push @soabits, $f;
232 }
233 if (scalar(@soabits) == 5) {
234 last;
235 }
236 }
237 } # SOA
238
239 # Quotes may arguably be syntactically required, but they're not actually part of the record data
240 elsif ($type eq 'TXT') {
241 $rdata =~ s/^"//;
242 $rdata =~ s/"$//;
243 }
244
245# temp hack for hosts file
246elsif ($type eq 'A') {
247# if ($amap{$name}) {
248# print "urp: dupe name $name $rdata\n";
249# } else {
250 push @{$amap{$name}}, $rdata;
251# }
252 push @{$namemap{$rdata}}, $name;
253}
254elsif ($type eq 'CNAME') {
255 push @{$cmap{$rdata}}, $name;
256}
257
258no warnings qw(uninitialized);
259#print "parsed: '$name' '$class' '$ttl' '$type'->'$itype' '$rdata'\n";
260#print;
261#;imap IN 900 CNAME deepnet.cx.
262##fixme: not sure how to handle the case where someone leaves off the class.
263 if ($doimport) {
264 my ($code, $msg);
265 if ($rev eq 'n') {
266 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$name, \$itype, \$rdata, $ttl);
267 } else {
268 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$rdata, \$itype, \$name, $ttl);
269 }
270 print "$code: $msg\n";
271 }
272 $i++;
273}
274
275
276#print Dumper \%amap;
277#print Dumper \%namemap;
278#print Dumper \%cmap;
279
280foreach my $n (keys %amap) {
281 foreach my $ip (@{$amap{$n}}) {
282#print "$ip $n\n";
283 push @{$namemap{$ip}}, $n unless grep $n, @{$namemap{$ip}};
284 }
285}
286
287foreach my $c (keys %cmap) {
288 if ($amap{$c}) {
289 print Dumper(\@{$amap{$c}});
290 }
291# print $amap{$c};
292}
293
294# cname targ -> IP
295
296#foreach my $ip (sort keys %namemap) {
297# print "$ip ".join(' ', @{$namemap{$ip}})."\n";
298#}
299
Note: See TracBrowser for help on using the repository browser.