File Coverage

blib/lib/Zonemaster/Engine/Zone.pm
Criterion Covered Total %
statement 135 156 86.5
branch 43 54 79.6
condition 29 33 87.8
subroutine 22 22 100.0
pod 5 5 100.0
total 234 270 86.6


line stmt bran cond sub pod time code
1             package Zonemaster::Engine::Zone;
2              
3 26     26   168 use version; our $VERSION = version->declare("v1.1.3");
  26         57  
  26         188  
4              
5 26     26   2750 use 5.014002;
  26         95  
6 26     26   129 use strict;
  26         54  
  26         593  
7 26     26   125 use warnings;
  26         59  
  26         727  
8              
9 26     26   120 use Moose;
  26         51  
  26         164  
10 26     26   158259 use Carp;
  26         60  
  26         1896  
11 26     26   831 use List::MoreUtils qw[uniq];
  26         15102  
  26         257  
12              
13 26     26   16890 use Zonemaster::Engine::DNSName;
  26         60  
  26         916  
14 26     26   1700 use Zonemaster::Engine::Recursor;
  26         64  
  26         679  
15 26     26   8806 use Zonemaster::Engine::NSArray;
  26         87  
  26         35183  
16              
17             has 'name' => ( is => 'ro', isa => 'Zonemaster::Engine::DNSName', required => 1, coerce => 1 );
18             has 'parent' => ( is => 'ro', isa => 'Maybe[Zonemaster::Engine::Zone]', lazy_build => 1 );
19             has [ 'ns', 'glue' ] => ( is => 'ro', isa => 'ArrayRef', lazy_build => 1 );
20             has [ 'ns_names', 'glue_names' ] => ( is => 'ro', isa => 'ArrayRef[Zonemaster::Engine::DNSName]', lazy_build => 1 );
21             has 'glue_addresses' => ( is => 'ro', isa => 'ArrayRef[Zonemaster::LDNS::RR]', lazy_build => 1 );
22              
23             ###
24             ### Builders
25             ###
26              
27             sub _build_parent {
28 392     392   965 my ( $self ) = @_;
29              
30 392 100       9514 if ( $self->name eq '.' ) {
31 3         73 return $self;
32             }
33              
34 389         9557 my $pname = Zonemaster::Engine::Recursor->parent( q{} . $self->name );
35 389 50       1644 return if not $pname;
36             ## no critic (Modules::RequireExplicitInclusion)
37 389         11749 return __PACKAGE__->new( { name => $pname } );
38             }
39              
40             sub _build_glue_names {
41 386     386   975 my ( $self ) = @_;
42              
43 386 50       9619 if ( not $self->parent ) {
44 0         0 return [];
45             }
46              
47 386         10401 my $p = $self->parent->query_persistent( $self->name, 'NS' );
48              
49 386 100       1993 return [] if not defined $p;
50              
51 381         11539 return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
  1621         54560  
52             $p->get_records_for_name( 'ns', $self->name->string ) ];
53             }
54              
55             sub _build_glue {
56 385     385   984 my ( $self ) = @_;
57              
58 385         871 my $aref = [];
59 385         805 tie @$aref, 'Zonemaster::Engine::NSArray', @{ $self->glue_names };
  385         10348  
60              
61 385         10591 return $aref;
62             }
63              
64             sub _build_ns_names {
65 377     377   869 my ( $self ) = @_;
66              
67 377 50       9043 if ( $self->name eq '.' ) {
68 0         0 my %u;
69 0         0 $u{$_} = $_ for map { $_->name } @{ $self->ns };
  0         0  
  0         0  
70 0         0 return [ sort values %u ];
71             }
72              
73 377         851 my $p;
74 377         741 my $i = 0;
75 377         10325 while ( my $s = $self->glue->[$i] ) {
76 377         20812 $p = $s->query( $self->name, 'NS' );
77 377 100 100     2946 last if ( defined( $p ) and ( $p->type eq 'answer' ) and ( $p->rcode eq 'NOERROR' ) );
      100        
78 4         199 $i += 1;
79             }
80 377 100       9779 return [] if not defined $p;
81              
82 374         10418 return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
  1590         53948  
83             $p->get_records_for_name( 'ns', $self->name->string ) ];
84             } ## end sub _build_ns_names
85              
86             sub _build_ns {
87 508     508   1387 my ( $self ) = @_;
88              
89 508 100       12664 if ( $self->name eq '.' ) { # Root is a special case
90 131         740 return [ Zonemaster::Engine::Recursor->root_servers ];
91             }
92              
93 377         1097 my $aref = [];
94 377         922 tie @$aref, 'Zonemaster::Engine::NSArray', @{ $self->ns_names };
  377         10283  
95              
96 377         10334 return $aref;
97             }
98              
99             sub _build_glue_addresses {
100 1     1   3 my ( $self ) = @_;
101              
102 1 50       27 if ( not $self->parent ) {
103 0         0 return [];
104             }
105              
106 1         23 my $p = $self->parent->query_one( $self->name, 'NS' );
107 1 50       4 croak "Failed to get glue addresses" if not defined( $p );
108              
109 1         4 return [ $p->get_records( 'a' ), $p->get_records( 'aaaa' ) ];
110             }
111              
112             ###
113             ### Public Methods
114             ###
115              
116             sub query_one {
117 188     188 1 2815 my ( $self, $name, $type, $flags ) = @_;
118              
119             # Return response from the first server that gives one
120 188         374 my $i = 0;
121 188         4720 while ( my $ns = $self->ns->[$i] ) {
122 191 100 100     5726 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
123 4         28 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
124 4         12 next;
125             }
126              
127 187 100 100     584 if ( not Zonemaster::Engine->config->ipv6_ok and $ns->address->version == 6 ) {
128 3         20 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => "$ns" } );
129 3         8 next;
130             }
131              
132 184         658 my $p = $ns->query( $name, $type, $flags );
133 184 50       1241 return $p if defined( $p );
134             }
135             continue {
136 7         169 $i += 1;
137             }
138              
139 4         16 return;
140             } ## end sub query_one
141              
142             sub query_all {
143 46     46 1 2332 my ( $self, $name, $type, $flags ) = @_;
144              
145 46         93 my @servers = @{ $self->ns };
  46         1214  
146              
147 46 50       292 if ( not Zonemaster::Engine->config->ipv4_ok ) {
148 0         0 my @nope = grep { $_->address->version == 4 } @servers;
  0         0  
149 0         0 @servers = grep { $_->address->version != 4 } @servers;
  0         0  
150 0         0 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => ( join ';', map { "$_" } @nope ) } );
  0         0  
151             }
152              
153 46 50       690 if ( not Zonemaster::Engine->config->ipv6_ok ) {
154 0         0 my @nope = grep { $_->address->version == 6 } @servers;
  0         0  
155 0         0 @servers = grep { $_->address->version != 6 } @servers;
  0         0  
156 0         0 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => ( join ';', map { "$_" } @nope ) } );
  0         0  
157             }
158              
159 46         150 return [ map { $_->query( $name, $type, $flags ) } @servers ];
  171         591  
160             }
161              
162             sub query_auth {
163 77     77 1 1356 my ( $self, $name, $type, $flags ) = @_;
164              
165             # Return response from the first server that replies with AA set
166 77         131 my $i = 0;
167 77         1905 while ( my $ns = $self->ns->[$i] ) {
168 97 100 100     3666 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
169 4         29 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
170 4         9 next;
171             }
172              
173 93 100 100     300 if ( not Zonemaster::Engine->config->ipv6_ok and $ns->address->version == 6 ) {
174 3         21 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => "$ns" } );
175 3         7 next;
176             }
177              
178 90         334 my $p = $ns->query( $name, $type, $flags );
179 90 100 66     781 if ( $p and $p->aa ) {
180 75         1136 return $p;
181             }
182             }
183             continue {
184 22         855 $i += 1;
185             }
186              
187 2         9 return;
188             } ## end sub query_auth
189              
190             sub query_persistent {
191 429     429 1 1668 my ( $self, $name, $type, $flags ) = @_;
192              
193             # Return response from the first server that has a record like the one asked for
194 429         931 my $i = 0;
195 429         11488 while ( my $ns = $self->ns->[$i] ) {
196 515 100 100     18451 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
197 4         29 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
198 4         7 next;
199             }
200              
201 511 100 100     1791 if ( not Zonemaster::Engine->config->ipv6_ok and $ns->address->version == 6 ) {
202 3         20 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => "$ns" } );
203 3         7 next;
204             }
205              
206 508         2285 my $p = $ns->query( $name, $type, $flags );
207 508 100 66     3363 if ( $p and scalar( $p->get_records_for_name( $type, $name ) ) > 0 ) {
208 422         3891 return $p;
209             }
210             }
211             continue {
212 93         2582 $i += 1;
213             }
214              
215 7         28 return;
216             } ## end sub query_persistent
217              
218             sub is_in_zone {
219 56     56 1 664 my ( $self, $name ) = @_;
220              
221 56 50 33     161 if ( not ref( $name ) or ref( $name ) ne 'Zonemaster::Engine::DNSName' ) {
222 56         1375 $name = Zonemaster::Engine::DNSName->new( $name );
223             }
224              
225 56 100       84 if ( scalar( @{ $self->name->labels } ) != $self->name->common( $name ) ) {
  56         1509  
226 17         460 return 0; # Zone name cannot be a suffix of tested name
227             }
228              
229 39         120 my $p = $self->query_auth( "$name", 'SOA' );
230 39 100       102 if ( not $p ) {
231 1         27 return;
232             }
233              
234 38 50       119 if ( $p->is_redirect ) {
235 0         0 return 0; # Authoritative servers redirect us, so name must be out-of-zone
236             }
237              
238 38         102 my ( $soa ) = $p->get_records( 'SOA' );
239              
240 38 50       301 if ( not $soa ) {
241 0         0 return 0; # Auth server is broken, call it a "no".
242             }
243              
244 38 100       1130 if ( Zonemaster::Engine::DNSName->new( $soa->name ) eq $self->name ) {
245 9         253 return 1;
246             }
247             else {
248 29         833 return 0;
249             }
250             } ## end sub is_in_zone
251              
252 26     26   268 no Moose;
  26         66  
  26         176  
253             __PACKAGE__->meta->make_immutable;
254              
255             1;
256              
257             =head1 NAME
258              
259             Zonemaster::Engine::Zone - Object representing a DNS zone
260              
261             =head1 SYNOPSIS
262              
263             my $zone = Zonemaster::Engine::Zone->new({ name => 'nic.se' });
264             my $packet = $zone->parent->query_one($zone->name, 'NS');
265              
266              
267             =head1 DESCRIPTION
268              
269             Objects of this class represent zones in DNS. As far as possible, test
270             implementations should access information about zones via these
271             objects. Doing so will provide lazy-loading of the information,
272             well-defined methods in which the information is fetched, logging and
273             the ability to do things like testing zones that have not yet been
274             delegated.
275              
276             =head1 ATTRIBUTES
277              
278             =over
279              
280             =item name
281              
282             A L<Zonemaster::Engine::DNSName> object representing the name of the zone.
283              
284             =item parent
285              
286             A L<Zonemaster::Engine::Zone> object for this domain's parent domain. As a
287             special case, the root zone is considered to be its own parent (so
288             look for that if you recurse up the tree).
289              
290             =item ns_names
291              
292             A reference to an array of L<Zonemaster::Engine::DNSName> objects, holding the
293             names of the nameservers for the domain, as returned by the first
294             responding nameserver in the glue list.
295              
296             =item ns
297              
298             A reference to an array of L<Zonemaster::Engine::Nameserver> objects for the
299             domain, built by taking the list returned from L<ns_names()> and
300             looking up addresses for the names. One element will be added to this
301             list for each unique name/IP pair. Names for which no addresses could
302             be found will not be in this list. The list is lazy-loading, so take
303             care to only look at as many entries as you really need. There are
304             zones with more than 20 nameserver, and looking up the addresses of
305             them all can take som considerable time.
306              
307             =item glue_names
308              
309             A reference to a an array of L<Zonemaster::Engine::DNSName> objects, holding the names
310             of this zones nameservers as listed at the first responding nameserver of the
311             parent zone.
312              
313             =item glue
314              
315             A reference to an array of L<Zonemaster::Engine::Nameserver> objects for the
316             domain, built by taking the list returned from L<glue_names()> and
317             looking up addresses for the names. One element will be added to this
318             list for each unique name/IP pair. Names for which no addresses could
319             be found will not be in this list. The list is lazy-loading, so take
320             care to only look at as many entries as you really need.
321              
322             =item glue_addresses
323              
324             A list of L<Zonemaster::LDNS::RR::A> and L<Zonemaster::LDNS::RR::AAAA> records returned in
325             the Additional section of an NS query to the first listed nameserver for the
326             parent domain.
327              
328             =back
329              
330             =head1 METHODS
331              
332             =over
333              
334             =item query_one($name[, $type[, $flags]])
335              
336             Sends (or retrieves from cache) a query for the given name, type and flags sent to the first nameserver in the zone's ns list. If there is a
337             response, it will be returned in a L<Zonemaster::Engine::Packet> object. If the type arguments is not given, it defaults to 'A'. If the flags are not given, they default to C<class> IN and C<dnssec>, C<usevc> and C<recurse> according to configuration (which is by default off on all three).
338              
339             =item query_persistent($name[, $type[, $flags]])
340              
341             Identical to L<query_one>, except that instead of returning the packet from the
342             first server that returns one, it returns the first packet that actually
343             contains a resource record matching the requested name and type.
344              
345             =item query_auth($name[, $type[, $flags]])
346              
347             Identical to L<query_one>, except that instead of returning the packet from the
348             first server that returns one, it returns the first packet that has the AA flag set.
349              
350             =item query_all($name, $type, $flags)
351              
352             Sends (or retrieves from cache) queries to all the nameservers listed in the zone's ns list, and returns a reference to an array with the
353             responses. The responses can be either L<Zonemaster::Engine::Packet> objects or C<undef> values. The arguments are the same as for L<query_one>.
354              
355             =item is_in_zone($name)
356              
357             Returns true if the given name is in the zone, false if not. If it could not be
358             determined with a sufficient degree of certainty if the name is in the zone or
359             not, C<undef> is returned.
360              
361             =back
362              
363             =cut