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   175 use version; our $VERSION = version->declare("v1.1.3");
  26         61  
  26         196  
4              
5 26     26   2677 use 5.014002;
  26         90  
6 26     26   125 use strict;
  26         50  
  26         529  
7 26     26   117 use warnings;
  26         52  
  26         704  
8              
9 26     26   124 use Moose;
  26         50  
  26         172  
10 26     26   160433 use Carp;
  26         65  
  26         1863  
11 26     26   774 use List::MoreUtils qw[uniq];
  26         13399  
  26         250  
12              
13 26     26   16496 use Zonemaster::Engine::DNSName;
  26         60  
  26         645  
14 26     26   1856 use Zonemaster::Engine::Recursor;
  26         61  
  26         646  
15 26     26   8622 use Zonemaster::Engine::NSArray;
  26         101  
  26         37117  
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   976 my ( $self ) = @_;
29              
30 392 100       9849 if ( $self->name eq '.' ) {
31 3         71 return $self;
32             }
33              
34 389         9457 my $pname = Zonemaster::Engine::Recursor->parent( q{} . $self->name );
35 389 50       1682 return if not $pname;
36             ## no critic (Modules::RequireExplicitInclusion)
37 389         11726 return __PACKAGE__->new( { name => $pname } );
38             }
39              
40             sub _build_glue_names {
41 386     386   912 my ( $self ) = @_;
42              
43 386 50       9668 if ( not $self->parent ) {
44 0         0 return [];
45             }
46              
47 386         10049 my $p = $self->parent->query_persistent( $self->name, 'NS' );
48              
49 386 100       1993 return [] if not defined $p;
50              
51 381         11288 return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
  1621         53316  
52             $p->get_records_for_name( 'ns', $self->name->string ) ];
53             }
54              
55             sub _build_glue {
56 385     385   1003 my ( $self ) = @_;
57              
58 385         846 my $aref = [];
59 385         848 tie @$aref, 'Zonemaster::Engine::NSArray', @{ $self->glue_names };
  385         10236  
60              
61 385         10273 return $aref;
62             }
63              
64             sub _build_ns_names {
65 377     377   962 my ( $self ) = @_;
66              
67 377 50       8939 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         818 my $p;
74 377         728 my $i = 0;
75 377         9667 while ( my $s = $self->glue->[$i] ) {
76 377         20222 $p = $s->query( $self->name, 'NS' );
77 377 100 100     2828 last if ( defined( $p ) and ( $p->type eq 'answer' ) and ( $p->rcode eq 'NOERROR' ) );
      100        
78 4         193 $i += 1;
79             }
80 377 100       7124 return [] if not defined $p;
81              
82 374         10123 return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
  1590         52268  
83             $p->get_records_for_name( 'ns', $self->name->string ) ];
84             } ## end sub _build_ns_names
85              
86             sub _build_ns {
87 508     508   3157 my ( $self ) = @_;
88              
89 508 100       12443 if ( $self->name eq '.' ) { # Root is a special case
90 131         736 return [ Zonemaster::Engine::Recursor->root_servers ];
91             }
92              
93 377         1013 my $aref = [];
94 377         877 tie @$aref, 'Zonemaster::Engine::NSArray', @{ $self->ns_names };
  377         10147  
95              
96 377         11655 return $aref;
97             }
98              
99             sub _build_glue_addresses {
100 1     1   3 my ( $self ) = @_;
101              
102 1 50       31 if ( not $self->parent ) {
103 0         0 return [];
104             }
105              
106 1         24 my $p = $self->parent->query_one( $self->name, 'NS' );
107 1 50       5 croak "Failed to get glue addresses" if not defined( $p );
108              
109 1         8 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 3041 my ( $self, $name, $type, $flags ) = @_;
118              
119             # Return response from the first server that gives one
120 188         398 my $i = 0;
121 188         4998 while ( my $ns = $self->ns->[$i] ) {
122 191 100 100     6020 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
123 4         35 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
124 4         21 next;
125             }
126              
127 187 100 100     594 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         7 next;
130             }
131              
132 184         823 my $p = $ns->query( $name, $type, $flags );
133 184 50       1306 return $p if defined( $p );
134             }
135             continue {
136 7         167 $i += 1;
137             }
138              
139 4         17 return;
140             } ## end sub query_one
141              
142             sub query_all {
143 46     46 1 2097 my ( $self, $name, $type, $flags ) = @_;
144              
145 46         124 my @servers = @{ $self->ns };
  46         1266  
146              
147 46 50       287 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       705 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         148 return [ map { $_->query( $name, $type, $flags ) } @servers ];
  171         700  
160             }
161              
162             sub query_auth {
163 77     77 1 1353 my ( $self, $name, $type, $flags ) = @_;
164              
165             # Return response from the first server that replies with AA set
166 77         141 my $i = 0;
167 77         2047 while ( my $ns = $self->ns->[$i] ) {
168 97 100 100     3312 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
169 4         35 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
170 4         9 next;
171             }
172              
173 93 100 100     293 if ( not Zonemaster::Engine->config->ipv6_ok and $ns->address->version == 6 ) {
174 3         22 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => "$ns" } );
175 3         7 next;
176             }
177              
178 90         347 my $p = $ns->query( $name, $type, $flags );
179 90 100 66     790 if ( $p and $p->aa ) {
180 75         1237 return $p;
181             }
182             }
183             continue {
184 22         809 $i += 1;
185             }
186              
187 2         10 return;
188             } ## end sub query_auth
189              
190             sub query_persistent {
191 429     429 1 1531 my ( $self, $name, $type, $flags ) = @_;
192              
193             # Return response from the first server that has a record like the one asked for
194 429         939 my $i = 0;
195 429         11112 while ( my $ns = $self->ns->[$i] ) {
196 515 100 100     18134 if ( not Zonemaster::Engine->config->ipv4_ok and $ns->address->version == 4 ) {
197 4         34 Zonemaster::Engine->logger->add( SKIP_IPV4_DISABLED => { ns => "$ns" } );
198 4         10 next;
199             }
200              
201 511 100 100     3435 if ( not Zonemaster::Engine->config->ipv6_ok and $ns->address->version == 6 ) {
202 3         31 Zonemaster::Engine->logger->add( SKIP_IPV6_DISABLED => { ns => "$ns" } );
203 3         7 next;
204             }
205              
206 508         2424 my $p = $ns->query( $name, $type, $flags );
207 508 100 66     3434 if ( $p and scalar( $p->get_records_for_name( $type, $name ) ) > 0 ) {
208 422         3855 return $p;
209             }
210             }
211             continue {
212 93         2610 $i += 1;
213             }
214              
215 7         31 return;
216             } ## end sub query_persistent
217              
218             sub is_in_zone {
219 56     56 1 548 my ( $self, $name ) = @_;
220              
221 56 50 33     164 if ( not ref( $name ) or ref( $name ) ne 'Zonemaster::Engine::DNSName' ) {
222 56         1495 $name = Zonemaster::Engine::DNSName->new( $name );
223             }
224              
225 56 100       90 if ( scalar( @{ $self->name->labels } ) != $self->name->common( $name ) ) {
  56         1460  
226 17         431 return 0; # Zone name cannot be a suffix of tested name
227             }
228              
229 39         130 my $p = $self->query_auth( "$name", 'SOA' );
230 39 100       116 if ( not $p ) {
231 1         52 return;
232             }
233              
234 38 50       125 if ( $p->is_redirect ) {
235 0         0 return 0; # Authoritative servers redirect us, so name must be out-of-zone
236             }
237              
238 38         103 my ( $soa ) = $p->get_records( 'SOA' );
239              
240 38 50       321 if ( not $soa ) {
241 0         0 return 0; # Auth server is broken, call it a "no".
242             }
243              
244 38 100       1126 if ( Zonemaster::Engine::DNSName->new( $soa->name ) eq $self->name ) {
245 9         290 return 1;
246             }
247             else {
248 29         807 return 0;
249             }
250             } ## end sub is_in_zone
251              
252 26     26   257 no Moose;
  26         61  
  26         172  
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