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   187 use version; our $VERSION = version->declare("v1.0.5");
  26         52  
  26         252  
4              
5 26     26   2525 use 5.014002;
  26         85  
6 26     26   131 use warnings;
  26         47  
  26         746  
7              
8 26     26   142 use Moose;
  26         49  
  26         182  
9 26     26   165766 use JSON::PP;
  26         206102  
  26         1970  
10 26     26   1873 use Zonemaster::Engine::Util;
  26         59  
  26         1457  
11 26     26   8311 use Zonemaster::Engine::Net::IP;
  26         82  
  26         1275  
12 26     26   182 use Zonemaster::Engine;
  26         58  
  26         40498  
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 285 my ( $self, $name, $type, $class ) = @_;
26 77         298 $name = name( $name );
27 77   100     254 $type //= 'A';
28 77   100     356 $class //= 'IN';
29              
30 77         325 Zonemaster::Engine->logger->add( RECURSE => { name => $name, type => $type, class => $class } );
31              
32 77 100       299 if ( exists $recurse_cache{$name}{$type}{$class} ) {
33 27         74 return $recurse_cache{$name}{$type}{$class};
34             }
35              
36 50         267 my ( $p, $state ) =
37             $self->_recurse( $name, $type, $class,
38             { ns => [ root_servers() ], count => 0, common => 0, seen => {}, glue => {} } );
39 50         631 $recurse_cache{$name}{$type}{$class} = $p;
40              
41 50         1815 return $p;
42             }
43              
44             sub parent {
45 406     406 1 1991 my ( $self, $name ) = @_;
46 406         1518 $name = name( $name );
47              
48 406         2031 my ( $p, $state ) =
49             $self->_recurse( $name, 'SOA', 'IN',
50             { ns => [ root_servers() ], count => 0, common => 0, seen => {}, glue => {} } );
51              
52 406         3863 my $pname;
53 406 100       2298 if ( name( $state->{trace}[0][0] ) eq name( $name ) ) {
54 392         1901 $pname = name( $state->{trace}[1][0] );
55             }
56             else {
57 14         65 $pname = name( $state->{trace}[0][0] );
58             }
59              
60             # Extra check that parent really is parent.
61 406 100       11675 if ( $name->next_higher ne $pname ) {
62 8         32 my $source_ns = $state->{trace}[0][1];
63 8         22 my $source_ip = $state->{trace}[0][2];
64              
65             # No $source_ns means we're looking at root taken from priming
66 8 100       37 if ( $source_ns ) {
67 4         267 my $pp;
68 4 100       28 if ( $source_ns->can( 'query' ) ) {
69 3         16 $pp = $source_ns->query( $name->next_higher->string, 'SOA' );
70             }
71             else {
72 1         6 my $n = ns( $source_ns, $source_ip );
73 1         5 $pp = $n->query( $name->next_higher->string, 'SOA' );
74             }
75 4 50       137 if ( $pp ) {
76 4         24 my ( $rr ) = $pp->get_records( 'SOA', 'answer' );
77 4 100       81 if ( $rr ) {
78 1         67 $pname = name( $rr->owner );
79             }
80             }
81             }
82             } ## end if ( $name->next_higher...)
83              
84 406 100       10498 if ( wantarray() ) {
85 5         172 return ( $pname, $p );
86             }
87             else {
88 401         12997 return $pname;
89             }
90             } ## end sub parent
91              
92             sub _recurse {
93 6422     6422   28155 my ( $self, $name, $type, $class, $state ) = @_;
94 6422         32529 $name = q{} . name( $name );
95              
96 6422 50       171940 if ( $state->{in_progress}{$name}{$type} ) {
97 0         0 return;
98             }
99 6422         23897 $state->{in_progress}{$name}{$type} = 1;
100              
101 6422         14706 while ( my $ns = pop @{ $state->{ns} } ) {
  19390         122331  
102 19386 100       1663133 my $nsname = $ns->can( 'name' ) ? q{} . $ns->name : q{};
103 19386 100       520942 my $nsaddress = $ns->can( 'address' ) ? $ns->address->ip : q{};
104 19386         175456 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         132011 my $p = $self->_do_query( $ns, $name, $type, { class => $class }, $state );
115              
116 19386 100       130104 next if not $p; # Ask next server if no response
117              
118 19337 100 100     119661 if ( $p->rcode eq 'REFUSED' or $p->rcode eq 'SERVFAIL' ) {
119             # Respond with these if we can't get a better response
120 55         741 $state->{candidate} = $p;
121 55         575 next;
122             }
123              
124 19282 100       280529 if ( $p->no_such_record ) { # Node exists, but not record
125 601         9139 return ( $p, $state );
126             }
127              
128 18681 100       77096 if ( $p->no_such_name ) { # Node does not exist
129 22         209 return ( $p, $state );
130             }
131              
132 18659 100       87957 if ( $self->_is_answer( $p ) ) { # Return answer
133 5795         116494 return ( $p, $state );
134             }
135              
136             # So it's not an error, not an empty response and not an answer
137              
138 12864 50       286564 if ( $p->is_redirect ) {
139 12864         69886 my $zname = name( lc( ( $p->get_records( 'ns' ) )[0]->name ) );
140              
141 12864 100       128807 next if $zname eq '.'; # Redirect to root is never right.
142              
143 12863 50       51204 next if $state->{seen}{$zname}; # We followed this redirect before
144              
145 12863         39084 $state->{seen}{$zname} = 1;
146 12863         43626 my $common = name( $zname )->common( name( $state->{qname} ) );
147              
148             next
149 12863 50       345641 if $common < $state->{common}; # Redirect going up the hierarchy is not OK
150              
151 12863         29379 $state->{common} = $common;
152 12863         54147 $state->{ns} = $self->get_ns_from( $p, $state ); # Follow redirect
153 12863         41291 $state->{count} += 1;
154 12863 50       50908 return ( undef, $state ) if $state->{count} > 20; # Loop protection
155 12863         28058 unshift @{ $state->{trace} }, [ $zname, $ns, $p->answerfrom ];
  12863         98531  
156              
157 12863         105687 next;
158             } ## end if ( $p->is_redirect )
159             } ## end while ( my $ns = pop @{ $state...})
160 4 100       22 return ( $state->{candidate}, $state ) if $state->{candidate};
161              
162 3         13 return ( undef, $state );
163             } ## end sub _recurse
164              
165             sub _do_query {
166 19387     19387   78495 my ( $self, $ns, $name, $type, $opts, $state ) = @_;
167              
168 19387 100 66     178377 if ( ref( $ns ) and $ns->can( 'query' ) ) {
    100          
169 17419         85103 my $p = $ns->query( $name, $type, $opts );
170              
171 17419 100       58750 if ( $p ) {
172 17379 100       110193 for my $rr ( grep { $_->type eq 'A' or $_->type eq 'AAAA' } $p->answer, $p->additional ) {
  159471         1159755  
173 159186         497935 $state->{glue}{ lc( name( $rr->name ) ) }{ $rr->address } = 1;
174             }
175             }
176 17419         174416 return $p;
177             }
178             elsif ( my $href = $state->{glue}{ lc( name( $ns ) ) } ) {
179 1         5 foreach my $addr ( keys %$href ) {
180 1         5 my $realns = ns( $ns, $addr );
181 1         6 my $p = $self->_do_query( $realns, $name, $type, $opts, $state );
182 1 50       5 if ( $p ) {
183 1         3 return $p;
184             }
185             }
186             }
187             else {
188 1967         9146 $state->{glue}{ lc( name( $ns ) ) } = {};
189 1967         53667 my @addr = $self->get_addresses_for( $ns, $state );
190 1967 100       9702 if ( @addr > 0 ) {
191 1962         6418 foreach my $addr ( @addr ) {
192 1962         10630 $state->{glue}{ lc( name( $ns ) ) }{ $addr->short } = 1;
193 1962         242898 my $new = ns( $ns, $addr->short );
194 1962         10647 my $p = $new->query( $name, $type, $opts );
195 1962 100       24998 return $p if $p;
196             }
197             }
198             else {
199 5         20 return;
200             }
201             }
202             } ## end sub _do_query
203              
204             sub get_ns_from {
205 12863     12863 1 42646 my ( $self, $p, $state ) = @_;
206 12863         27312 my ( @new, @extra );
207              
208 12863         45612 my @names = sort map { name( lc( $_->nsdname ) ) } $p->get_records( 'ns' );
  117589         974234  
209              
210             $state->{glue}{ lc( name( $_->name ) ) }{ $_->address } = 1
211 12863         178241 for ( $p->get_records( 'a' ), $p->get_records( 'aaaa' ) );
212              
213 12863         122710 foreach my $name ( @names ) {
214 117589 100       503016 if ( exists $state->{glue}{ lc( name( $name ) ) } ) {
215 113572         197227 for my $addr ( keys %{ $state->{glue}{ lc( name( $name ) ) } } ) {
  113572         361875  
216 158144         537116 push @new, ns( $name, $addr );
217             }
218             }
219             else {
220 4017         112208 push @extra, $name;
221             }
222             }
223              
224 12863 50       95864 @new = sort { $a->name cmp $b->name or $a->address->ip cmp $b->address->ip } @new;
  325285         8366859  
225 12863         52446 @extra = sort { $a cmp $b } @extra;
  2771         8021  
226              
227 12863         377493 return [ @new, @extra ];
228             } ## end sub get_ns_from
229              
230             sub get_addresses_for {
231 2988     2988 1 9719 my ( $self, $name, $state ) = @_;
232 2988         5914 my @res;
233 2988   100     11804 $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         11994 );
246              
247             # Name does not exist, just stop
248 2988 100 66     94744 if ( $pa and $pa->no_such_name ) {
249 10         84 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         14838 );
262              
263 2978         95873 my @rrs;
264             my %cname;
265 2978 50       12560 if ( $pa ) {
266 2978         14821 push @rrs, $pa->get_records( 'a' );
267 2978         12668 $cname{ $_->cname } = 1 for $pa->get_records_for_name( 'CNAME', $name );
268             }
269 2978 50       11567 if ( $paaaa ) {
270 2978         10393 push @rrs, $paaaa->get_records( 'aaaa' );
271 2978         10603 $cname{ $_->cname } = 1 for $paaaa->get_records_for_name( 'CNAME', $name );
272             }
273              
274 2978         15714 foreach my $rr ( sort { $a->address cmp $b->address } @rrs ) {
  7498         54967  
275 7426 100 66     24776 if ( name( $rr->name ) eq $name or $cname{ $rr->name } ) {
276 5365         61041 push @res, Zonemaster::Engine::Net::IP->new( $rr->address );
277             }
278             }
279              
280 2978         32103 return @res;
281             } ## end sub get_addresses_for
282              
283             sub _is_answer {
284 18659     18659   56547 my ( $self, $packet ) = @_;
285              
286 18659         62246 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         615461 return map { Zonemaster::Engine::Util::ns( $_->{name}, $_->{address} ) }
295 7574     7574 1 17914 sort { $a->{name} cmp $b->{name} } @{ $seed_data->{'.'} };
  507458         700729  
  7574         51438  
296             }
297              
298 26     26   212 no Moose;
  26         57  
  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             }