File Coverage

blib/lib/Zonemaster/Engine/Recursor.pm
Criterion Covered Total %
statement 159 160 99.3
branch 60 70 85.7
condition 15 18 83.3
subroutine 18 18 100.0
pod 6 6 100.0
total 258 272 94.8


line stmt bran cond sub pod time code
1             package Zonemaster::Engine::Recursor;
2              
3 26     26   197 use version; our $VERSION = version->declare("v1.0.5");
  26         58  
  26         257  
4              
5 26     26   2925 use 5.014002;
  26         90  
6 26     26   122 use warnings;
  26         54  
  26         751  
7              
8 26     26   148 use Moose;
  26         49  
  26         157  
9 26     26   166508 use JSON::PP;
  26         203797  
  26         1981  
10 26     26   1813 use Zonemaster::Engine::Util;
  26         59  
  26         1540  
11 26     26   8323 use Zonemaster::Engine::Net::IP;
  26         86  
  26         1304  
12 26     26   192 use Zonemaster::Engine;
  26         60  
  26         39262  
13              
14             my $seed_data;
15              
16             our %recurse_cache;
17              
18             {
19             local $/;
20             my $json = <DATA>;
21             $seed_data = decode_json $json;
22             }
23              
24             sub recurse {
25 77     77 1 313 my ( $self, $name, $type, $class ) = @_;
26 77         318 $name = name( $name );
27 77   100     284 $type //= 'A';
28 77   100     455 $class //= 'IN';
29              
30 77         361 Zonemaster::Engine->logger->add( RECURSE => { name => $name, type => $type, class => $class } );
31              
32 77 100       320 if ( exists $recurse_cache{$name}{$type}{$class} ) {
33 27         84 return $recurse_cache{$name}{$type}{$class};
34             }
35              
36 50         271 my ( $p, $state ) =
37             $self->_recurse( $name, $type, $class,
38             { ns => [ root_servers() ], count => 0, common => 0, seen => {}, glue => {} } );
39 50         608 $recurse_cache{$name}{$type}{$class} = $p;
40              
41 50         2215 return $p;
42             }
43              
44             sub parent {
45 406     406 1 2091 my ( $self, $name ) = @_;
46 406         1537 $name = name( $name );
47              
48 406         2043 my ( $p, $state ) =
49             $self->_recurse( $name, 'SOA', 'IN',
50             { ns => [ root_servers() ], count => 0, common => 0, seen => {}, glue => {} } );
51              
52 406         3678 my $pname;
53 406 100       2269 if ( name( $state->{trace}[0][0] ) eq name( $name ) ) {
54 392         1861 $pname = name( $state->{trace}[1][0] );
55             }
56             else {
57 14         81 $pname = name( $state->{trace}[0][0] );
58             }
59              
60             # Extra check that parent really is parent.
61 406 100       11690 if ( $name->next_higher ne $pname ) {
62 8         35 my $source_ns = $state->{trace}[0][1];
63 8         28 my $source_ip = $state->{trace}[0][2];
64              
65             # No $source_ns means we're looking at root taken from priming
66 8 100       41 if ( $source_ns ) {
67 4         334 my $pp;
68 4 100       47 if ( $source_ns->can( 'query' ) ) {
69 3         16 $pp = $source_ns->query( $name->next_higher->string, 'SOA' );
70             }
71             else {
72 1         13 my $n = ns( $source_ns, $source_ip );
73 1         9 $pp = $n->query( $name->next_higher->string, 'SOA' );
74             }
75 4 50       233 if ( $pp ) {
76 4         33 my ( $rr ) = $pp->get_records( 'SOA', 'answer' );
77 4 100       70 if ( $rr ) {
78 1         36 $pname = name( $rr->owner );
79             }
80             }
81             }
82             } ## end if ( $name->next_higher...)
83              
84 406 100       10900 if ( wantarray() ) {
85 5         167 return ( $pname, $p );
86             }
87             else {
88 401         13855 return $pname;
89             }
90             } ## end sub parent
91              
92             sub _recurse {
93 6422     6422   27764 my ( $self, $name, $type, $class, $state ) = @_;
94 6422         32501 $name = q{} . name( $name );
95              
96 6422 50       169596 if ( $state->{in_progress}{$name}{$type} ) {
97 0         0 return;
98             }
99 6422         22658 $state->{in_progress}{$name}{$type} = 1;
100              
101 6422         13697 while ( my $ns = pop @{ $state->{ns} } ) {
  19390         122398  
102 19386 100       1627303 my $nsname = $ns->can( 'name' ) ? q{} . $ns->name : q{};
103 19386 100       514134 my $nsaddress = $ns->can( 'address' ) ? $ns->address->ip : q{};
104 19386         180757 Zonemaster::Engine->logger->add(
105             RECURSE_QUERY => {
106             source => "$ns",
107             ns => $nsname,
108             address => $nsaddress,
109             name => $name,
110             type => $type,
111             class => $class
112             }
113             );
114 19386         132083 my $p = $self->_do_query( $ns, $name, $type, { class => $class }, $state );
115              
116 19386 100       125019 next if not $p; # Ask next server if no response
117              
118 19337 100 100     127983 if ( $p->rcode eq 'REFUSED' or $p->rcode eq 'SERVFAIL' ) {
119             # Respond with these if we can't get a better response
120 55         683 $state->{candidate} = $p;
121 55         540 next;
122             }
123              
124 19282 100       277348 if ( $p->no_such_record ) { # Node exists, but not record
125 601         8938 return ( $p, $state );
126             }
127              
128 18681 100       76112 if ( $p->no_such_name ) { # Node does not exist
129 22         219 return ( $p, $state );
130             }
131              
132 18659 100       92896 if ( $self->_is_answer( $p ) ) { # Return answer
133 5795         115386 return ( $p, $state );
134             }
135              
136             # So it's not an error, not an empty response and not an answer
137              
138 12864 50       287894 if ( $p->is_redirect ) {
139 12864         63820 my $zname = name( lc( ( $p->get_records( 'ns' ) )[0]->name ) );
140              
141 12864 100       131589 next if $zname eq '.'; # Redirect to root is never right.
142              
143 12863 50       51898 next if $state->{seen}{$zname}; # We followed this redirect before
144              
145 12863         38194 $state->{seen}{$zname} = 1;
146 12863         45025 my $common = name( $zname )->common( name( $state->{qname} ) );
147              
148             next
149 12863 50       337570 if $common < $state->{common}; # Redirect going up the hierarchy is not OK
150              
151 12863         31347 $state->{common} = $common;
152 12863         54472 $state->{ns} = $self->get_ns_from( $p, $state ); # Follow redirect
153 12863         42649 $state->{count} += 1;
154 12863 50       54259 return ( undef, $state ) if $state->{count} > 20; # Loop protection
155 12863         29865 unshift @{ $state->{trace} }, [ $zname, $ns, $p->answerfrom ];
  12863         105003  
156              
157 12863         100126 next;
158             } ## end if ( $p->is_redirect )
159             } ## end while ( my $ns = pop @{ $state...})
160 4 100       20 return ( $state->{candidate}, $state ) if $state->{candidate};
161              
162 3         12 return ( undef, $state );
163             } ## end sub _recurse
164              
165             sub _do_query {
166 19387     19387   77840 my ( $self, $ns, $name, $type, $opts, $state ) = @_;
167              
168 19387 100 66     180954 if ( ref( $ns ) and $ns->can( 'query' ) ) {
    100          
169 17419         81947 my $p = $ns->query( $name, $type, $opts );
170              
171 17419 100       55493 if ( $p ) {
172 17379 100       108632 for my $rr ( grep { $_->type eq 'A' or $_->type eq 'AAAA' } $p->answer, $p->additional ) {
  159471         1138050  
173 159186         510180 $state->{glue}{ lc( name( $rr->name ) ) }{ $rr->address } = 1;
174             }
175             }
176 17419         173966 return $p;
177             }
178             elsif ( my $href = $state->{glue}{ lc( name( $ns ) ) } ) {
179 1         4 foreach my $addr ( keys %$href ) {
180 1         5 my $realns = ns( $ns, $addr );
181 1         5 my $p = $self->_do_query( $realns, $name, $type, $opts, $state );
182 1 50       4 if ( $p ) {
183 1         4 return $p;
184             }
185             }
186             }
187             else {
188 1967         9428 $state->{glue}{ lc( name( $ns ) ) } = {};
189 1967         52179 my @addr = $self->get_addresses_for( $ns, $state );
190 1967 100       9778 if ( @addr > 0 ) {
191 1962         6191 foreach my $addr ( @addr ) {
192 1962         11181 $state->{glue}{ lc( name( $ns ) ) }{ $addr->short } = 1;
193 1962         242356 my $new = ns( $ns, $addr->short );
194 1962         11353 my $p = $new->query( $name, $type, $opts );
195 1962 100       25126 return $p if $p;
196             }
197             }
198             else {
199 5         21 return;
200             }
201             }
202             } ## end sub _do_query
203              
204             sub get_ns_from {
205 12863     12863 1 39750 my ( $self, $p, $state ) = @_;
206 12863         26526 my ( @new, @extra );
207              
208 12863         46002 my @names = sort map { name( lc( $_->nsdname ) ) } $p->get_records( 'ns' );
  117589         979176  
209              
210             $state->{glue}{ lc( name( $_->name ) ) }{ $_->address } = 1
211 12863         178082 for ( $p->get_records( 'a' ), $p->get_records( 'aaaa' ) );
212              
213 12863         125466 foreach my $name ( @names ) {
214 117589 100       493476 if ( exists $state->{glue}{ lc( name( $name ) ) } ) {
215 113572         220176 for my $addr ( keys %{ $state->{glue}{ lc( name( $name ) ) } } ) {
  113572         380007  
216 158144         519734 push @new, ns( $name, $addr );
217             }
218             }
219             else {
220 4017         108980 push @extra, $name;
221             }
222             }
223              
224 12863 50       99584 @new = sort { $a->name cmp $b->name or $a->address->ip cmp $b->address->ip } @new;
  325241         8176392  
225 12863         52449 @extra = sort { $a cmp $b } @extra;
  2771         7707  
226              
227 12863         371836 return [ @new, @extra ];
228             } ## end sub get_ns_from
229              
230             sub get_addresses_for {
231 2988     2988 1 9973 my ( $self, $name, $state ) = @_;
232 2988         5933 my @res;
233 2988   100     12495 $state //=
234             { ns => [ root_servers() ], count => 0, common => 0, seen => {} };
235              
236             my ( $pa ) = $self->_recurse(
237             "$name", 'A', 'IN',
238             {
239             ns => [ root_servers() ],
240             count => $state->{count},
241             common => 0,
242             in_progress => $state->{in_progress},
243             glue => $state->{glue}
244             }
245 2988         12168 );
246              
247             # Name does not exist, just stop
248 2988 100 66     96548 if ( $pa and $pa->no_such_name ) {
249 10         64 return;
250             }
251              
252             my ( $paaaa ) = $self->_recurse(
253             "$name", 'AAAA', 'IN',
254             {
255             ns => [ root_servers() ],
256             count => $state->{count},
257             common => 0,
258             in_progress => $state->{in_progress},
259             glue => $state->{glue}
260             }
261 2978         14823 );
262              
263 2978         99654 my @rrs;
264             my %cname;
265 2978 50       12593 if ( $pa ) {
266 2978         14784 push @rrs, $pa->get_records( 'a' );
267 2978         13115 $cname{ $_->cname } = 1 for $pa->get_records_for_name( 'CNAME', $name );
268             }
269 2978 50       12218 if ( $paaaa ) {
270 2978         9922 push @rrs, $paaaa->get_records( 'aaaa' );
271 2978         10702 $cname{ $_->cname } = 1 for $paaaa->get_records_for_name( 'CNAME', $name );
272             }
273              
274 2978         15436 foreach my $rr ( sort { $a->address cmp $b->address } @rrs ) {
  7498         56174  
275 7426 100 66     26064 if ( name( $rr->name ) eq $name or $cname{ $rr->name } ) {
276 5365         63333 push @res, Zonemaster::Engine::Net::IP->new( $rr->address );
277             }
278             }
279              
280 2978         31504 return @res;
281             } ## end sub get_addresses_for
282              
283             sub _is_answer {
284 18659     18659   53905 my ( $self, $packet ) = @_;
285              
286 18659         62083 return ( $packet->type eq 'answer' );
287             }
288              
289             sub clear_cache {
290 53     53 1 175 %recurse_cache = ();
291             }
292              
293             sub root_servers {
294 166628         645263 return map { Zonemaster::Engine::Util::ns( $_->{name}, $_->{address} ) }
295 7574     7574 1 17480 sort { $a->{name} cmp $b->{name} } @{ $seed_data->{'.'} };
  507458         695382  
  7574         52071  
296             }
297              
298 26     26   216 no Moose;
  26         58  
  26         239  
299             __PACKAGE__->meta->make_immutable;
300              
301             1;
302              
303             =head1 NAME
304              
305             Zonemaster::Engine::Recursor - recursive resolver for Zonemaster
306              
307             =head1 SYNOPSIS
308              
309             my $packet = Zonemaster::Engine::Recursor->recurse($name, $type, $class);
310             my $pname = Zonemaster::Engine::Recursor->parent('example.org');
311              
312             =head1 METHODS
313              
314             =over
315              
316             =item recurse($name, $type, $class)
317              
318             Does a recursive resolution from the root servers down for the given triplet.
319              
320             =item parent($name)
321              
322             Does a recursive resolution from the root down for the given name (using type C<SOA> and class C<IN>). If the resolution is successful, it returns
323             the domain name of the second-to-last step. If the resolution is unsuccessful, it returns the domain name of the last step.
324              
325             =item get_ns_from($packet, $state)
326              
327             Internal method. Takes a packet and a recursion state and returns a list of ns objects. Used to follow redirections.
328              
329             =item get_addresses_for($name[, $state])
330              
331             Takes a name and returns a (possibly empty) list of IP addresses for
332             that name (in the form of L<Zonemaster::Engine::Net::IP> objects). When used
333             internally by the recursor it's passed a recursion state as its second
334             argument.
335              
336             =item clear_cache()
337              
338             Class method to empty the cache of responses to recursive queries.
339              
340             =item root_servers()
341              
342             Returns a list of ns objects representing the root servers. The list of root servers is hardcoded into this module.
343              
344             =back
345              
346             =cut
347              
348             __DATA__
349             {
350             "." : [
351             {
352             "name" : "m.root-servers.net",
353             "address" : "202.12.27.33"
354             },
355             {
356             "name" : "m.root-servers.net",
357             "address" : "2001:dc3:0:0:0:0:0:35"
358             },
359             {
360             "name" : "e.root-servers.net",
361             "address" : "192.203.230.10"
362             },
363             {
364             "address" : "199.7.83.42",
365             "name" : "l.root-servers.net"
366             },
367             {
368             "address" : "2001:500:3:0:0:0:0:42",
369             "name" : "l.root-servers.net"
370             },
371             {
372             "address" : "198.41.0.4",
373             "name" : "a.root-servers.net"
374             },
375             {
376             "address" : "2001:503:ba3e:0:0:0:2:30",
377             "name" : "a.root-servers.net"
378             },
379             {
380             "address" : "192.5.5.241",
381             "name" : "f.root-servers.net"
382             },
383             {
384             "address" : "2001:500:2f:0:0:0:0:f",
385             "name" : "f.root-servers.net"
386             },
387             {
388             "address" : "199.7.91.13",
389             "name" : "d.root-servers.net"
390             },
391             {
392             "address" : "2001:500:2d:0:0:0:0:d",
393             "name" : "d.root-servers.net"
394             },
395             {
396             "address" : "192.58.128.30",
397             "name" : "j.root-servers.net"
398             },
399             {
400             "address" : "2001:503:c27:0:0:0:2:30",
401             "name" : "j.root-servers.net"
402             },
403             {
404             "address" : "128.63.2.53",
405             "name" : "h.root-servers.net"
406             },
407             {
408             "name" : "h.root-servers.net",
409             "address" : "2001:500:1:0:0:0:803f:235"
410             },
411             {
412             "name" : "g.root-servers.net",
413             "address" : "192.112.36.4"
414             },
415             {
416             "name" : "k.root-servers.net",
417             "address" : "193.0.14.129"
418             },
419             {
420             "address" : "2001:7fd:0:0:0:0:0:1",
421             "name" : "k.root-servers.net"
422             },
423             {
424             "name" : "b.root-servers.net",
425             "address" : "192.228.79.201"
426             },
427             {
428             "address" : "192.33.4.12",
429             "name" : "c.root-servers.net"
430             },
431             {
432             "name" : "i.root-servers.net",
433             "address" : "192.36.148.17"
434             },
435             {
436             "name" : "i.root-servers.net",
437             "address" : "2001:7fe:0:0:0:0:0:53"
438             }
439             ]
440             }