source: trunk/bind-import@ 812

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

/trunk

Fifth sampled iteration of bind-import

File size: 6.8 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;
33my $rev = 'n';
34my $zid;
35
36my %amap;
37my %namemap;
38my %cmap;
39
40if ($zname =~ /\.arpa\.?$/ || $zname =~ m,^[\d./]+$,) {
41 $rev = 'y';
42 $zname = _zone2cidr($zname) if $zname =~ /\.arpa\.?$/;
43 $zid = $dnsdb->revID($zname,':ANY:');
44 if ($zid) {
45 $zname = new NetAddr::IP $zname;
46 $zname = DNSDB::_ZONE($zname, 'ZONE', 'r', '.').($zname->{isv6} ? '.ip6.arpa' : '.in-addr.arpa');
47 }
48} else {
49 $zid = $dnsdb->domainID($zname,':ANY:');
50}
51
52die "zone $zname not on file\n" if !$zid;
53
54# still no sane way to expose a human-friendly view tag on the command line.
55my $view = shift @ARGV;
56$view = '' if !$view;
57
58##fixme: retrieve defttl from SOA record
59my $zonettl = 900;
60my $defttl = $zonettl;
61my $recbase = $zname; # to append to unqualified names
62
63# need to spin up a full state machine-ish thing, because BIND zone files are all about context
64# see ch4, p56-72 in the grasshopper book
65my $prevlabel = '';
66my $curlabel = '';
67
68while (<>) {
69 chomp;
70 next if /^\s*$/;
71 next if /^\s*;/;
72 if (my ($macro,$mdetail) = (/^\s*\$(TTL|ORIGIN|INCLUDE|GENERATE)\s+(.+)/) ) {
73 # macro sort of thing; $TTL and $ORIGIN most common. $INCLUDE is a thing, expect it to be rare in live use tho
74 if ($macro eq 'TTL') {
75 if ($mdetail =~ /^\d+$/) {
76 $defttl = $mdetail;
77 } else {
78 warn "invalid \$TTL: $_\n";
79 }
80 } elsif ($macro eq 'ORIGIN') {
81##fixme: going to skip the stupid case of "$ORIGIN com." and the like that lie
82# between . and the root domain we were told we're importing; anyone using such
83# a mess outside the root servers is clearly insane
84# handled cases:
85# $ORIGIN .
86# $ORIGIN [zonedomain].
87# $ORIGIN [subdomain.zonedomain].
88 if ($mdetail eq '.' || $mdetail =~ /$zname\.$/ || $zname =~ /$mdetail\.$/) {
89 $recbase = $mdetail;
90 } else {
91 # if we continue, we either use an $ORIGIN that's out of zone, or ignore it and potentially publish incorrect records.
92 die "bad \$ORIGIN: $_\n";
93 }
94 }
95 # not handling $INCLUDE or $GENERATE (altho the latter seems to be mostly a less-flexible version of the template types)
96 next;
97 }
98 # skip stale records that have no value
99 next if /^ip-192-168-1(12|20)-\d+/;
100 next if /ip.add.re.\d+\s*$/;
101 # records must begin in the first column, no leading whitespace
102 my ($name) = /^([\w\@_.-]+)\s/;
103 # append zone name to record name if missing AND not dot-terminated;
104 # this happens automagically for forward zones, but not reverse because Reasons. (fixme?)
105 # suck up and deal with the error if the dot-termiated name is out of zone; should be
106 # impossible with valid BIND zone file but...
107 $name .= ".$zname" if $name !~ /$zname$/ && $zname !~ /\.$/;
108$name = $zname if /^\s*IN/;
109$name = $zname if /^\@/;
110 s/^([\w\@_.-]+)\s+//;
111# by convention the optional TTL leads the optional class, but they're apparently swappable.
112 my ($ttl) = /^(\d+)?\s/;
113 if (defined $ttl) {
114 # TTL may be zero
115 s/(\d+)?\s+//;
116 } else {
117 # Fall back to zone default TTL
118 $ttl = $zonettl;
119 }
120 my ($class) = /^(IN|CS|CH|HS|\d+)\s/;
121 if (defined $class) {
122 if ($class =~ /\d+/) {
123
124 }
125 if ($class ne 'IN') {
126 print "Non-Internet class records not supported, you weirdo\n";
127 next;
128 }
129 s/(IN|CS|CH|HS)\s+//;
130 } else {
131 $class = 'IN';
132 }
133 my ($type) = /([A-Z-]+)\s/;
134 if (!$reverse_typemap{$type}) {
135 print "Unknown type $type, skipping\n ($_)\n";
136 next;
137 }
138 my $itype = $reverse_typemap{$type};
139 s/([A-Z-]+)\s+//;
140 chomp;
141 my $rdata = $_;
142
143 # SOA is the only type that may span multiple lines. Probably. Note even AXFRed zones write multiline SOA records:
144 #@ IN SOA test.example.invalid. test.example.invalid. (2020082500 7200 900 604800 3600)
145 # IN NS olddns.example.com.
146 # IN MX 1 fred.foo.bar.invalid.
147 #foo IN A 192.168.16.45
148 # AXFR'ed zone file gets written as
149 #$ORIGIN .
150 #$TTL 3600 ; 1 hour
151 #example.invalid IN SOA test.example.invalid. test.example.invalid. (
152 # 2020082500 ; serial
153 # 7200 ; refresh (2 hours)
154 # 900 ; retry (15 minutes)
155 # 604800 ; expire (1 week)
156 # 3600 ; minimum (1 hour)
157 # )
158 # NS olddns.example.com.
159 # MX 1 fred.foo.bar.invalid.
160 #$ORIGIN example.invalid.
161 #foo A 192.168.16.45
162
163 if ($type eq 'SOA') {
164
165 }
166
167 # Quotes may arguably be syntactically required, but they're not actually part of the record data
168 if ($itype == 16) {
169 $rdata =~ s/^"//;
170 $rdata =~ s/"$//;
171 }
172
173# temp hack for hosts file
174if ($type eq 'A') {
175# if ($amap{$name}) {
176# print "urp: dupe name $name $rdata\n";
177# } else {
178 push @{$amap{$name}}, $rdata;
179# }
180 push @{$namemap{$rdata}}, $name;
181}
182if ($type eq 'CNAME') {
183 push @{$cmap{$rdata}}, $name;
184}
185
186no warnings qw(uninitialized);
187#print "parsed: '$name' '$class' '$ttl' '$type'->'$itype' '$rdata'\n";
188#print;
189#;imap IN 900 CNAME deepnet.cx.
190##fixme: not sure how to handle the case where someone leaves off the class.
191 if ($doimport) {
192 my ($code, $msg);
193 if ($rev eq 'n') {
194 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$name, \$itype, \$rdata, $ttl);
195 } else {
196 ($code,$msg) = $dnsdb->addRec('n', $rev, $zid, \$rdata, \$itype, \$name, $ttl);
197 }
198 print "$code: $msg\n";
199 }
200}
201
202
203#print Dumper \%amap;
204#print Dumper \%namemap;
205#print Dumper \%cmap;
206
207foreach my $n (keys %amap) {
208 foreach my $ip (@{$amap{$n}}) {
209#print "$ip $n\n";
210 push @{$namemap{$ip}}, $n unless grep $n, @{$namemap{$ip}};
211 }
212}
213
214foreach my $c (keys %cmap) {
215 if ($amap{$c}) {
216 print Dumper(\@{$amap{$c}});
217 }
218# print $amap{$c};
219}
220
221# cname targ -> IP
222
223#foreach my $ip (sort keys %namemap) {
224# print "$ip ".join(' ', @{$namemap{$ip}})."\n";
225#}
226
Note: See TracBrowser for help on using the repository browser.