source: trunk/bind2hosts@ 804

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

/trunk

Commit 5th archived iteration of bind2hosts in development

  • Property svn:executable set to *
File size: 7.9 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;
22use Data::Dumper;
23
24# push "the directory the script is in" into @INC
25use FindBin;
26use lib "$FindBin::RealBin/";
27
28use DNSDB;
29
30my $dnsdb = new DNSDB;
31my $doimport = 0;
32
33#print Dumper(\%reverse_typemap);
34
35my $zname = shift @ARGV;
36my $rev = 'n';
37my $zid;
38
39my %amap;
40my %namemap;
[801]41my %cmap;
[799]42
[801]43# this bit irrelevant for a hosts file
44#if ($zname =~ /\.arpa\.?$/ || $zname =~ m,^[\d./]+$,) {
45# $rev = 'y';
46# $zname = _zone2cidr($zname) if $zname =~ /\.arpa\.?$/;
47# $zid = $dnsdb->revID($zname,':ANY:');
48# if ($zid) {
49# $zname = new NetAddr::IP $zname;
50# $zname = DNSDB::_ZONE($zname, 'ZONE', 'r', '.').($zname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
51# }
52#} else {
53# $zid = $dnsdb->domainID($zname,':ANY:');
54#}
55#
56#die "zone $zname not on file\n" if !$zid;
[799]57
58# still no sane way to expose a human-friendly view tag on the command line.
59my $view = shift @ARGV;
60$view = '' if !$view;
61
62##fixme: retrieve defttl from SOA record
[801]63#my $zonettl = 900;
64#my $defttl = $zonettl;
65# need an ultimate fallback for this one
66my $defttl = 900;
67my $origin = "$zname."; # to append to unqualified names
68my $curlabel;
69my $prevlabel;
[799]70
[801]71my $i = 0;
72
[799]73# need to spin up a full state machine-ish thing, because BIND zone files are all about context
[801]74while (my $rec = <>) {
75 chomp $rec;
76 next if $rec =~ /^\s*$/;
77 next if $rec =~ /^\s*;/;
78 next if $rec =~ /^\s*\)/; # SOA closing (possibly other records too?)
[800]79 # arguably should do some more targeted voodoo when parsing the SOA details
[801]80#print "$i: ($rec)\n";
81#last if ++$i > 5;
[800]82
[801]83 if (my ($macro,$mdetail) = ($rec =~ /^\s*\$(TTL|ORIGIN|INCLUDE)\s+(.+)/) ) {
[799]84 # macro sort of thing; $TTL and $ORIGIN most common. $INCLUDE is a thing, expect it to be rare in live use tho
85 if ($macro eq 'TTL') {
[800]86 # irrelevant for a hosts file
[799]87 } elsif ($macro eq 'ORIGIN') {
[801]88#print "origin ($mdetail)\n";
[800]89 # $ORIGIN supports cascading/nesting, by watching for fully-qualified names vs partial names.
90 if ($mdetail =~ /\.$/) {
91 $origin = $mdetail;
[799]92 } else {
[800]93 # append current origin to unqualified origin
94 $origin = "$mdetail.$origin";
[799]95 }
96 }
[800]97##fixme: should arguably handle $INCLUDE
98# probably NOT going to handle $GENERATE, since that type of record set is best handled in DNS
[799]99 next;
100 }
[800]101
[801]102 my $origrec = $rec;
[800]103
104##fixme: convert to optional skipfile?
105# skip stale records that have no value
106#next if /^ip-\d+-\d+-\d+/;
107#next if /^ip.pre.fix.\d+.static.colo/;
108
109 # leading whitespace indicates "same label as last record"
[801]110 if ($rec =~ /^\s/) {
[800]111 $curlabel = $prevlabel;
112 } else {
[801]113 ($curlabel) = ($rec =~ /^([\w\@_.-]+)\s/);
[800]114 }
115
116 # magic name!
117 $curlabel = "$zname." if $curlabel eq '@';
118
119 # append $ORIGIN if name is not fully qualified.
120 if ($curlabel !~ /\.$/) {
[801]121 $curlabel .= ".$origin";
[800]122 }
123
124 # check for zone scope. skip bad records.
125 if ($curlabel !~ /$zname.$/) {
126 warn "bad record $origrec, maybe bad \$ORIGIN?\n";
127 next;
128 }
129
[801]130 # trim the label, if any
131 $rec =~ s/^([\w\@_.-]*)\s+//;
132
133#print "r$i ($rec)\n\t$curlabel\n";
134
135 my $nc = 0;
136my $debugid = -1;
137 my %seenatoms;
138# we don't actually use these but we have to recognize them
139my $class = 'IN';
140# not preset as we need to detect whether it's present in the record
141my $ttl;
142#my $ttl = $defttl;
143my $type;
144 my $badrec;
145 my $curatom = 'class';
146##fixme: maybe wrap this in an eval() instead of the warn/badrec/last bits?
[803]147eval {
[801]148 for (; $nc < 3; $nc++) {
[802]149 my ($atom) = ($rec =~ /^([\w\d.]+)\s/);
[801]150 # should be safe?
151 last if !$atom;
152#print "a$nc: l: $rec\n $atom\n" if $i == $debugid;
153 if ($atom =~ /^\d+$/) {
154#print "a$nc: d: atom [$atom]\n $rec\n" if $i == $debugid;
155 if (defined($ttl)) {
[803]156 die "bad record ($origrec)\n";
157# warn "bad record ($origrec)\n";
158# $badrec = 1;
159# last;
[801]160 } else {
161 if ($curatom ne 'class' && $curatom ne 'ttl') {
[803]162 die "bad record ($origrec)\n";
163# warn "bad record ($origrec)\n";
164# $badrec = 1;
165# last;
[801]166 }
167 $curatom = 'ttl';
168 $ttl = $atom;
169 }
[799]170 }
[801]171 elsif ($atom =~ /^IN|CS|CH|HS$/) {
172#print "a$nc: d2: atom [$atom]\n $rec\n" if $i == $debugid;
173 if ($atom =~ /CS|CH|HS/) {
[803]174 die "unsupported class $atom in record ($origrec)\n";
175# warn "unsupported class $atom in record ($origrec)\n";
176# $badrec = 1;
177# last;
[801]178 }
179 $curatom = 'class';
180 $class = $atom;
181 }
182 elsif ($atom =~ /^[A-Z]+/) {
183#print "a$nc: d3a: probable type [$atom]\n" if $i == $debugid;
184 if ($reverse_typemap{$atom}) {
185#print "a$nc: d3b: atom [$atom]\n $rec\n" if $i == $debugid;
186 $type = $atom;
187 } else {
[803]188 die "unknown type $atom in record ($origrec)\n";
189# warn "unknown type $atom in record ($origrec)\n";
190# $badrec = 1;
191# last;
[801]192 }
193 }
194 $rec =~ s/^$atom\s*//;
195#print "a$nc: next: $rec\n" if $i == $debugid;
196 } # class/type/TTL loop
[803]197};
198if ($@) {
199 warn $@;
200 next;
201}
[801]202
[803]203
[801]204#last if $i > 15;
[803]205# next if $badrec;
[801]206
207#print Dumper(\%reverse_typemap);
208$ttl = $defttl if !defined($ttl);
209
210#print "class $class, ttl $ttl, type $type\n";
211#last;
212
[799]213 my $itype = $reverse_typemap{$type};
[801]214# s/([A-Z-]+)\s+//;
215# chomp;
216 my $rdata = $rec;
[799]217
[801]218 $prevlabel = $curlabel;
[799]219
[801]220##fixme: squish this down for this script since SOA records are irrelevant
221 if ($type eq 'SOA') {
222 my ($ns, $adminmail) = ($rdata =~ /([\w.]+)\s+([\w.]+)\s+\(/);
[803]223 die "Can't parse gibberish SOAish record: $origrec\n" if !$ns;
[801]224 $rdata =~ s/([\w.]+)\s+([\w.]+)\s+\(\s*//;
[799]225
[801]226 # There are probably more efficient ways to do this but the SOA record
227 # format is essentially character based, not line-based.
228 # In theory the SOA serial etc may be spread over up to 5 lines, in any combination.
[800]229
[801]230 # Parse fields from $rdata if present
231 my @soabits;
232 my @soafirst = split /\s+/, $rdata;
233 while (my $f = shift @soafirst) {
234 last if $f !~ /^\d/;
235 push @soabits, $f;
236 }
[799]237
[801]238 # Read more lines if we don't have enough SOA fields filled
239 while (scalar(@soabits) < 5) {
240 my $tmp = <>;
241 $tmp =~ s/^\s*//;
242 my @tmpsoa = split /\s+/, $tmp;
243 while (my $f = shift @tmpsoa) {
244 last if $f !~ /^\d/;
245 push @soabits, $f;
246 }
247 if (scalar(@soabits) == 5) {
248 last;
249 }
250 }
251 } # SOA
[799]252
[801]253##fixme: trim dupes if possible
254 elsif ($type eq 'A') {
255# push @{$amap{$curlabel}}, $rdata;
256# push @{$namemap{$rdata}}, $curlabel;
257
258 # need the name->IP map so we can reverse-map the CNAMEs on output
259 $amap{$curlabel}{$rdata}++;
260 $namemap{$rdata}{$curlabel}++;
261
262#print "$origrec\n";
263 } # A record
264
265 elsif ($type eq 'CNAME') {
266# push @{$cmap{$rdata}}, $curlabel;
267##todo: expand $rdata with $origin if unqualified
[802]268
269 $cmap{$curlabel} = $rdata.($rdata =~ /\./ ? '' : ".$origin");
[801]270#print "$origrec\n";
271 } # CNAME record
272
273
274# last if ++$i > 10;
275} # <STDIN>
276
277
[799]278#print Dumper \%amap;
[801]279#foreach my $n (keys %amap) {
280# foreach my $ip (@{$amap{$n}}) {
281##print "$ip\t$n\n";
282# push @{$namemap{$ip}}, $n unless grep $n, @{$namemap{$ip}};
283# }
284#}
[799]285
286#print Dumper \%namemap;
[801]287#foreach my $ip (sort keys %namemap) {
288# print "$ip\t".join(' ', @{$namemap{$ip}})."\n";
289#}
290
291#print Dumper \%cmap;
292
293
294foreach my $cn (keys %cmap) {
295 print "$cn -> $cmap{$cn}\n";
296# warn "CNAME $cn out of zone\n" if !$namemap{$cn};
[799]297}
Note: See TracBrowser for help on using the repository browser.