source: trunk/bind2hosts@ 806

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

/trunk

Fix an edge case with $ORIGIN == '.'

  • Property svn:executable set to *
File size: 9.4 KB
RevLine 
[799]1#!/usr/bin/perl
2# Convert a BIND zone file to a hosts 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;
[805]22use Getopt::Long;
23
[799]24use Data::Dumper;
25
26# push "the directory the script is in" into @INC
27use FindBin;
28use lib "$FindBin::RealBin/";
29
30use DNSDB;
31
[805]32my @skipdefs;
33my $skipfile;
34my $dryrun = 0;
[799]35
[805]36GetOptions(
37 "skip=s" => \@skipdefs,
38 "skipfile=s" => \$skipfile,
39 "test|dry-run" => \$dryrun,
40);
[799]41
42my $zname = shift @ARGV;
[805]43
44my $usage = "usage: bind2hosts zone [--skip pattern [--skip pattern2 ...]] [--skipfile file]
45 zonename < zonefile
46
47 --skip
48 Specify a string to skip in the records. If an IP-like string is
49 used, and the zone is a reverse zone, it will also search for the
50 octet-reversed form. Specify multiple times to skip multiple
51 different record patterns.
52 --skip-file
53 A file containing patterns to skip. Patterns from the file and
54 any --skip arguments are merged.
55 zonename
56 The name of the zone to import. Required.
57
58 Zone data will be read from STDIN.
59";
60if (!$zname) {
61 die $usage;
62}
63
64if ($skipfile) {
65 if (-f $skipfile) {
66 open SKIP, "<$skipfile";
67 while (<SKIP>) {
68 chomp;
69 push @skipdefs, $_;
70 }
71 close SKIP;
72 } else {
73 warn "skipfile $skipfile requested but it doesn't seem to exist. Continuing.\n";
74 }
75}
76
[799]77my $rev = 'n';
78my $zid;
79
80my %amap;
81my %namemap;
[801]82my %cmap;
[799]83
[805]84my $dnsdb = new DNSDB;
[799]85
86##fixme: retrieve defttl from SOA record
[801]87#my $zonettl = 900;
88#my $defttl = $zonettl;
89# need an ultimate fallback for this one
90my $defttl = 900;
91my $origin = "$zname."; # to append to unqualified names
92my $curlabel;
93my $prevlabel;
[799]94
[801]95my $i = 0;
96
[799]97# need to spin up a full state machine-ish thing, because BIND zone files are all about context
[801]98while (my $rec = <>) {
99 chomp $rec;
100 next if $rec =~ /^\s*$/;
101 next if $rec =~ /^\s*;/;
102 next if $rec =~ /^\s*\)/; # SOA closing (possibly other records too?)
[800]103 # arguably should do some more targeted voodoo when parsing the SOA details
[801]104#print "$i: ($rec)\n";
105#last if ++$i > 5;
[800]106
[805]107 my $skipflag = 0;
108 foreach (@skipdefs) {
109#print "skipdbg: $_ =~ $rec\n" if $rec =~ /207/;
110 if ($rec =~ /\Q$_\E/) {
111 $skipflag = 1;
112# print "skip: $rec\n";
113 }
114 }
115 next if $skipflag;
116
[801]117 if (my ($macro,$mdetail) = ($rec =~ /^\s*\$(TTL|ORIGIN|INCLUDE)\s+(.+)/) ) {
[799]118 # macro sort of thing; $TTL and $ORIGIN most common. $INCLUDE is a thing, expect it to be rare in live use tho
119 if ($macro eq 'TTL') {
[800]120 # irrelevant for a hosts file
[799]121 } elsif ($macro eq 'ORIGIN') {
[800]122 # $ORIGIN supports cascading/nesting, by watching for fully-qualified names vs partial names.
123 if ($mdetail =~ /\.$/) {
124 $origin = $mdetail;
[799]125 } else {
[800]126 # append current origin to unqualified origin
127 $origin = "$mdetail.$origin";
[799]128 }
129 }
[800]130##fixme: should arguably handle $INCLUDE
131# probably NOT going to handle $GENERATE, since that type of record set is best handled in DNS
[799]132 next;
133 }
[800]134
[806]135 # yay for special cases
136 $origin = '' if $origin eq '.';
137
[801]138 my $origrec = $rec;
[800]139
140##fixme: convert to optional skipfile?
141# skip stale records that have no value
142#next if /^ip-\d+-\d+-\d+/;
143#next if /^ip.pre.fix.\d+.static.colo/;
144
145 # leading whitespace indicates "same label as last record"
[801]146 if ($rec =~ /^\s/) {
[800]147 $curlabel = $prevlabel;
148 } else {
[801]149 ($curlabel) = ($rec =~ /^([\w\@_.-]+)\s/);
[800]150 }
151
152 # magic name!
153 $curlabel = "$zname." if $curlabel eq '@';
154
155 # append $ORIGIN if name is not fully qualified.
156 if ($curlabel !~ /\.$/) {
[801]157 $curlabel .= ".$origin";
[800]158 }
159
160 # check for zone scope. skip bad records.
161 if ($curlabel !~ /$zname.$/) {
162 warn "bad record $origrec, maybe bad \$ORIGIN?\n";
163 next;
164 }
165
[801]166 # trim the label, if any
167 $rec =~ s/^([\w\@_.-]*)\s+//;
168
169 my $nc = 0;
170 my %seenatoms;
[805]171 # we don't actually use these but we have to recognize them
172 my $class = 'IN';
173 # not preset as we need to detect whether it's present in the record
174 my $ttl;
175 my $type;
[801]176 my $badrec;
177 my $curatom = 'class';
[805]178
179 # now that we've collected and trimmed off the record's label, unpack the class, TTL, and type.
180 # class and TTL may be omitted, and may appear in either class,TTL or TTL,class order.
181 eval {
182 for (; $nc < 3; $nc++) {
183 last if $type; # short-circuit if we've got a type, further data is record-specific.
184 my ($atom) = ($rec =~ /^([\w\d.]+)\s/);
185 # should be safe?
186 last if !$atom;
187 if ($atom =~ /^\d+$/) {
188 if (defined($ttl)) {
189 # we already have a TTL, so another all-numeric field is invalid.
[803]190 die "bad record ($origrec)\n";
[805]191 } else {
192 if ($curatom ne 'class' && $curatom ne 'ttl') {
193 die "bad record ($origrec)\n";
194 }
195 $curatom = 'ttl';
196 $ttl = $atom;
[801]197 }
198 }
[805]199 elsif ($atom =~ /^IN|CS|CH|HS$/) {
200 if ($atom =~ /CS|CH|HS/) {
201 die "unsupported class $atom in record ($origrec)\n";
202 }
203 $curatom = 'class';
204 $class = $atom;
[801]205 }
[805]206 elsif ($atom =~ /^[A-Z]+/) {
207 # check against dnsadmin's internal list of known DNS types.
208 if ($reverse_typemap{$atom}) {
209 $type = $atom;
210 } else {
211 die "unknown type $atom in record ($origrec)\n";
212 }
213 $curatom = 'type';
[801]214 }
[805]215 $rec =~ s/^$atom\s*//;
216 } # class/type/TTL loop
217 };
218 if ($@) {
219 warn $@;
220 next;
221 }
[801]222
[803]223
[805]224 $ttl = $defttl if !defined($ttl);
[801]225
[799]226 my $itype = $reverse_typemap{$type};
[801]227 my $rdata = $rec;
[799]228
[801]229 $prevlabel = $curlabel;
[799]230
[801]231##fixme: squish this down for this script since SOA records are irrelevant
232 if ($type eq 'SOA') {
233 my ($ns, $adminmail) = ($rdata =~ /([\w.]+)\s+([\w.]+)\s+\(/);
[805]234 die "Can't parse gibberish SOAish record: '$rdata'/'$origrec'\n" if !$ns;
[801]235 $rdata =~ s/([\w.]+)\s+([\w.]+)\s+\(\s*//;
[799]236
[801]237 # There are probably more efficient ways to do this but the SOA record
238 # format is essentially character based, not line-based.
239 # In theory the SOA serial etc may be spread over up to 5 lines, in any combination.
[800]240
[801]241 # Parse fields from $rdata if present
242 my @soabits;
243 my @soafirst = split /\s+/, $rdata;
244 while (my $f = shift @soafirst) {
245 last if $f !~ /^\d/;
246 push @soabits, $f;
247 }
[799]248
[801]249 # Read more lines if we don't have enough SOA fields filled
250 while (scalar(@soabits) < 5) {
251 my $tmp = <>;
252 $tmp =~ s/^\s*//;
253 my @tmpsoa = split /\s+/, $tmp;
254 while (my $f = shift @tmpsoa) {
255 last if $f !~ /^\d/;
256 push @soabits, $f;
257 }
258 if (scalar(@soabits) == 5) {
259 last;
260 }
261 }
262 } # SOA
[799]263
[801]264##fixme: trim dupes if possible
[805]265
[801]266 elsif ($type eq 'A') {
267 # need the name->IP map so we can reverse-map the CNAMEs on output
[805]268# $amap{$curlabel}{$rdata}++;
269 push @{$amap{$curlabel}}, $rdata;
270# why doesn't this work? causes ALL cases of multi-named IPs to get skipped, not just duplicates. O_o
271# push @{$namemap{$rdata}}, $curlabel unless grep $curlabel, @{$namemap{$rdata}};
272# push @{$namemap{$rdata}}, $curlabel;# unless grep $curlabel, @{$namemap{$rdata}};
[801]273 $namemap{$rdata}{$curlabel}++;
274
275 } # A record
276
277 elsif ($type eq 'CNAME') {
278##todo: expand $rdata with $origin if unqualified
[802]279 $cmap{$curlabel} = $rdata.($rdata =~ /\./ ? '' : ".$origin");
[801]280 } # CNAME record
281
[805]282 # all other record types are irrelevant for a hosts file
[801]283
284} # <STDIN>
285
286
[805]287
288
289#print Dumper \%cmap;
290
291while (my ($cn, $targ) = each %cmap) {
292#print "dbg: ".Dumper($targ);
293 if (!$amap{$targ}) {
294 if ($cmap{$targ}) {
295warn "chained cname $cn => $targ\n";
296 my $tmpcn = $targ;
297 $targ = $cmap{$tmpcn};
298warn " chain target $cn => $tmpcn => $targ\n";
299# next if !$amap{$targ};
300 if (!$amap{$targ}) {
301 if ($cmap{$targ}) {
302#print " second chain?\n";
303 $tmpcn = $targ;
304 $targ = $cmap{$tmpcn};
305 } else {
306#print "not found\n";
307next;
308 }
309 }
310 } else {
311 # skip depth-3 (?) CNAMES; any such zone does not belong as a hosts file anyway
312 warn "CNAME $cn => $targ not found\n";
313 next;
314 }
315 }
316# print Dumper (\%{$amap{$cmap{$cn}}});
317# print "$cn -> $cmap{$cn}\n";
318# $amap{$cmap{$cn}}{$cn}++ if $cmap{$cn} =~ /$zname.$/ && $amap{$cmap{$cn}};
319# print "dangling CNAME $cn\n" if !$namemap{$cmap{$cn}};
320# print "$cn -> $cmap{$cn}\n";
321# warn "CNAME $cn out of zone\n" if !$namemap{$cn};
322 my $targip = $amap{$targ}[0];
323#print "$cn => $targ\n" if $targ =~ /(webftp|landing)/;
324#print $targip;
325# push @{$namemap{$targip}}, $targ unless grep $targ, @{$namemap{$targip}};
326 $namemap{$targip}{$cn}++;# unless grep $targ, @{$namemap{$targip}};
327}
328
[799]329#print Dumper \%amap;
[801]330#foreach my $n (keys %amap) {
[805]331# foreach my $ip (keys %{$amap{$n}}) {
332#print "$ip\t$n\n";
[801]333# push @{$namemap{$ip}}, $n unless grep $n, @{$namemap{$ip}};
[805]334# $namemap{$ip}{$n}++;
[801]335# }
336#}
[799]337
338#print Dumper \%namemap;
[805]339foreach my $ip (sort keys %namemap) {
340 print "$ip\t".join(' ', sort keys %{$namemap{$ip}})."\n";
[799]341}
Note: See TracBrowser for help on using the repository browser.