use
version ;
our
$VERSION
= version->declare(
"v1.1.0"
);
our
%recurse_cache
;
our
%_fake_addresses_cache
;
sub
init_recursor {
my
$hints_path
= dist_file(
'Zonemaster-Engine'
,
'named.root'
);
my
$hints_text
= read_file(
$hints_path
);
my
$hints_data
= parse_hints(
$hints_text
);
Zonemaster::Engine::Recursor->add_fake_addresses(
'.'
,
$hints_data
);
}
sub
add_fake_addresses {
my
(
$class
,
$domain
,
$href
) =
@_
;
$domain
=
lc
$domain
;
foreach
my
$name
(
keys
%{
$href
} ) {
my
@ips
= uniq @{
$href
->{
$name
} };
$name
=
lc
$name
;
push
@{
$_fake_addresses_cache
{
$domain
}{
$name
} }, ();
foreach
my
$ip
(
@ips
) {
push
@{
$_fake_addresses_cache
{
$domain
}{
$name
} },
$ip
;
}
}
return
;
}
sub
has_fake_addresses {
my
(
$class
,
$domain
) =
@_
;
$domain
=
lc
$domain
;
return
!!
$_fake_addresses_cache
{
$domain
};
}
sub
get_fake_addresses {
my
(
$class
,
$domain
,
$nsname
) =
@_
;
(
defined
$domain
) or croak
'Argument must be defined: $domain'
;
$domain
=
lc
$domain
;
$nsname
= (
defined
$nsname
) ?
lc
$nsname
:
q{}
;
if
(
exists
$_fake_addresses_cache
{
$domain
}{
$nsname
} ) {
return
@{
$_fake_addresses_cache
{
$domain
}{
$nsname
} };
}
else
{
return
();
}
}
sub
get_fake_names {
my
(
$class
,
$domain
) =
@_
;
$domain
=
lc
$domain
;
if
(
exists
$_fake_addresses_cache
{
$domain
} ) {
return
keys
%{
$_fake_addresses_cache
{
$domain
}};
}
else
{
return
();
}
}
sub
remove_fake_addresses {
my
(
$class
,
$domain
) =
@_
;
$domain
=
lc
$domain
;
delete
$_fake_addresses_cache
{
$domain
};
return
;
}
sub
recurse {
my
(
$class
,
$name
,
$type
,
$dns_class
,
$ns
) =
@_
;
$name
= name(
$name
);
$type
//=
'A'
;
$dns_class
//=
'IN'
;
Zonemaster::Engine->logger->add(
RECURSE
=> {
name
=>
$name
,
type
=>
$type
,
class
=>
$dns_class
} );
if
(
exists
$recurse_cache
{
$name
}{
$type
}{
$dns_class
} ) {
return
$recurse_cache
{
$name
}{
$type
}{
$dns_class
};
}
my
%state
= (
ns
=> [ root_servers() ],
count
=> 0,
common
=> 0,
seen
=> {},
glue
=> {} );
if
(
defined
$ns
) {
ref
(
$ns
) eq
'ARRAY'
or croak
'Argument $ns must be an arrayref'
;
$state
{ns} =
$ns
;
}
my
(
$p
,
$state
) =
$class
->_recurse(
$name
,
$type
,
$dns_class
, \
%state
);
$recurse_cache
{
$name
}{
$type
}{
$dns_class
} =
$p
;
return
$p
;
}
sub
parent {
my
(
$class
,
$name
) =
@_
;
$name
= name(
$name
);
my
(
$p
,
$state
) =
$class
->_recurse(
$name
,
'SOA'
,
'IN'
,
{
ns
=> [ root_servers() ],
count
=> 0,
common
=> 0,
seen
=> {},
glue
=> {} } );
my
$pname
;
if
( name(
$state
->{trace}[0][0] ) eq name(
$name
) ) {
$pname
= name(
$state
->{trace}[1][0] );
}
else
{
$pname
= name(
$state
->{trace}[0][0] );
}
if
(
$name
->next_higher ne
$pname
) {
my
$source_ns
=
$state
->{trace}[0][1];
my
$source_ip
=
$state
->{trace}[0][2];
if
(
$source_ns
) {
my
$pp
;
if
(
$source_ns
->can(
'query'
) ) {
$pp
=
$source_ns
->query(
$name
->next_higher->string,
'SOA'
);
}
else
{
my
$n
= ns(
$source_ns
,
$source_ip
);
$pp
=
$n
->query(
$name
->next_higher->string,
'SOA'
);
}
if
(
$pp
) {
my
(
$rr
) =
$pp
->get_records(
'SOA'
,
'answer'
);
if
(
$rr
) {
$pname
= name(
$rr
->owner );
}
}
}
}
if
(
wantarray
() ) {
return
(
$pname
,
$p
);
}
else
{
return
$pname
;
}
}
sub
_resolve_cname {
my
(
$class
,
$name
,
$type
,
$dns_class
,
$p
,
$state
) =
@_
;
$name
= name(
$name
);
Zonemaster::Engine->logger->add(
CNAME_START
=> {
name
=>
$name
,
type
=>
$type
,
dns_class
=>
$dns_class
} );
my
@cname_rrs
=
$p
->get_records(
'CNAME'
,
'answer'
);
my
(
%duplicate_cname_rrs
,
@original_rrs
);
for
my
$rr
(
@cname_rrs
) {
my
$rr_hash
=
$rr
->class .
'/CNAME/'
.
lc
(
$rr
->owner) .
'/'
.
lc
(
$rr
->cname);
if
(
exists
$duplicate_cname_rrs
{
$rr_hash
} ) {
$duplicate_cname_rrs
{
$rr_hash
}++;
}
else
{
$duplicate_cname_rrs
{
$rr_hash
} = 0;
push
@original_rrs
,
$rr
;
}
}
unless
(
scalar
@original_rrs
==
scalar
@cname_rrs
) {
Zonemaster::Engine->logger->add(
CNAME_RECORDS_DUPLICATES
=> {
records
=>
join
(
';'
,
map
{
"$_ => $duplicate_cname_rrs{$_}"
if
$duplicate_cname_rrs
{
$_
} > 0 }
keys
%duplicate_cname_rrs
)
}
);
@cname_rrs
=
@original_rrs
;
}
if
(
scalar
@cname_rrs
>
$CNAME_MAX_RECORDS
) {
Zonemaster::Engine->logger->add(
CNAME_RECORDS_TOO_MANY
=> {
name
=>
$name
,
count
=>
scalar
@cname_rrs
,
max
=>
$CNAME_MAX_RECORDS
} );
return
(
undef
,
$state
);
}
my
(
%cnames
,
%seen_targets
,
%forbidden_targets
);
for
my
$rr
(
@cname_rrs
) {
my
$rr_owner
= name(
$rr
->owner );
my
$rr_target
= name(
$rr
->cname );
if
(
exists
$forbidden_targets
{
lc
(
$rr_owner
)} ) {
Zonemaster::Engine->logger->add(
CNAME_RECORDS_MULTIPLE_FOR_NAME
=> {
name
=>
$rr_owner
} );
return
(
undef
,
$state
);
}
if
(
lc
(
$rr_owner
) eq
lc
(
$rr_target
) or
exists
$seen_targets
{
lc
(
$rr_target
)} or
grep
{
$_
eq
lc
(
$rr_target
) } (
keys
%forbidden_targets
) ) {
Zonemaster::Engine->logger->add(
CNAME_LOOP_INNER
=> {
name
=>
join
(
';'
,
map
{
$_
->owner }
@cname_rrs
),
target
=>
join
(
';'
,
map
{
$_
->cname }
@cname_rrs
) } );
return
(
undef
,
$state
);
}
$seen_targets
{
lc
(
$rr_target
)} = 1;
$forbidden_targets
{
lc
(
$rr_owner
)} = 1;
$cnames
{
$rr_owner
} =
$rr_target
;
}
my
$target
=
$name
;
my
$cname_counter
= 0;
while
(
$cnames
{
$target
} ) {
return
(
undef
,
$state
)
if
$cname_counter
>
$CNAME_MAX_RECORDS
;
$target
=
$cnames
{
$target
};
$cname_counter
++;
}
if
(
$cname_counter
!=
scalar
@cname_rrs
) {
Zonemaster::Engine->logger->add(
CNAME_RECORDS_CHAIN_BROKEN
=> {
name
=>
$name
,
cname_rrs
=>
scalar
@cname_rrs
,
cname_counter
=>
$cname_counter
} );
return
(
undef
,
$state
);
}
if
(
scalar
$p
->get_records(
$type
,
'answer'
) ) {
if
(
$p
->has_rrs_of_type_for_name(
$type
,
$target
) ) {
Zonemaster::Engine->logger->add(
CNAME_FOLLOWED_IN_ZONE
=> {
name
=>
$name
,
type
=>
$type
,
target
=>
$target
} );
return
(
$p
,
$state
);
}
Zonemaster::Engine->logger->add(
CNAME_NO_MATCH
=> {
name
=>
$name
,
type
=>
$type
,
target
=>
$target
,
owner_names
=>
join
(
';'
,
map
{
$_
->owner }
$p
->get_records(
$type
) ) } );
return
(
undef
,
$state
);
}
if
(
exists
$state
->{in_progress}{
lc
(
$target
)}{
$type
} ) {
Zonemaster::Engine->logger->add(
CNAME_LOOP_OUTER
=> {
name
=>
$name
,
target
=>
$target
,
targets_seen
=>
join
(
';'
,
keys
%{
$state
->{tseen} } ) } );
return
(
undef
,
$state
);
}
$state
->{tseen}{
lc
(
$target
)} = 1;
$state
->{tcount} += 1;
if
(
$state
->{tcount} >
$CNAME_MAX_CHAIN_LENGTH
) {
Zonemaster::Engine->logger->add(
CNAME_CHAIN_TOO_LONG
=> {
count
=>
$state
->{tcount},
max
=>
$CNAME_MAX_CHAIN_LENGTH
} );
return
(
undef
,
$state
);
}
unless
(
$name
->is_in_bailiwick(
$target
) ) {
Zonemaster::Engine->logger->add(
CNAME_FOLLOWED_OUT_OF_ZONE
=> {
name
=>
$name
,
target
=>
$target
} );
(
$p
,
$state
) =
$class
->_recurse(
$target
,
$type
,
$dns_class
,
{
ns
=> [ root_servers() ],
count
=> 0,
common
=> 0,
seen
=> {},
tseen
=>
$state
->{tseen},
tcount
=>
$state
->{tcount},
glue
=> {},
in_progress
=>
$state
->{in_progress} });
}
else
{
}
return
(
$p
,
$state
);
}
sub
_recurse {
my
(
$class
,
$name
,
$type
,
$dns_class
,
$state
) =
@_
;
$name
=
q{}
. name(
$name
);
if
(
$state
->{in_progress}{
$name
}{
$type
} ) {
return
;
}
$state
->{in_progress}{
$name
}{
$type
} = 1;
while
(
my
$ns
=
pop
@{
$state
->{ns} } ) {
my
$nsname
=
$ns
->can(
'name'
) ?
q{}
.
$ns
->name :
q{}
;
my
$nsaddress
=
$ns
->can(
'address'
) ?
$ns
->address->ip :
q{}
;
Zonemaster::Engine->logger->add(
RECURSE_QUERY
=> {
source
=>
"$ns"
,
ns
=>
$nsname
,
address
=>
$nsaddress
,
name
=>
$name
,
type
=>
$type
,
class
=>
$dns_class
,
}
);
my
$p
=
$class
->_do_query(
$ns
,
$name
,
$type
, {
class
=>
$dns_class
},
$state
);
next
if
not
$p
;
if
(
$p
->rcode eq
'REFUSED'
or
$p
->rcode eq
'SERVFAIL'
) {
$state
->{candidate} =
$p
;
next
;
}
if
(
$p
->no_such_record ) {
return
(
$p
,
$state
);
}
if
(
$p
->no_such_name ) {
return
(
$p
,
$state
);
}
if
(
$class
->_is_answer(
$p
) ) {
if
( not
$p
->has_rrs_of_type_for_name(
$type
,
$name
) and
scalar
$p
->get_records_for_name(
'CNAME'
,
$name
,
'answer'
) ) {
(
$p
,
$state
) =
$class
->_resolve_cname(
$name
,
$type
,
$dns_class
,
$p
,
$state
);
}
return
(
$p
,
$state
);
}
if
(
$p
->is_redirect ) {
my
$zname
= name(
lc
( (
$p
->get_records(
'ns'
) )[0]->name ) );
next
if
$zname
eq
'.'
;
next
if
$state
->{seen}{
$zname
};
$state
->{seen}{
$zname
} = 1;
my
$common
= name(
$zname
)->common( name(
$state
->{qname} ) );
next
if
$common
<
$state
->{common};
$state
->{common} =
$common
;
$state
->{ns} =
$class
->get_ns_from(
$p
,
$state
);
$state
->{count} += 1;
if
(
$state
->{count} > 20 ) {
Zonemaster::Engine->logger->add(
LOOP_PROTECTION
=> {
caller
=>
'Zonemaster::Engine::Recursor->_recurse'
,
child_zone_name
=>
$name
,
name
=>
$zname
}
);
return
(
undef
,
$state
);
}
unshift
@{
$state
->{trace} }, [
$zname
,
$ns
,
$p
->answerfrom ];
next
;
}
}
return
(
$state
->{candidate},
$state
)
if
$state
->{candidate};
return
(
undef
,
$state
);
}
sub
_do_query {
my
(
$class
,
$ns
,
$name
,
$type
,
$opts
,
$state
) =
@_
;
if
(
ref
(
$ns
) and
$ns
->can(
'query'
) ) {
my
$p
=
$ns
->query(
$name
,
$type
,
$opts
);
if
(
$p
) {
for
my
$rr
(
grep
{
$_
->type eq
'A'
or
$_
->type eq
'AAAA'
}
$p
->answer,
$p
->additional ) {
$state
->{glue}{
lc
( Zonemaster::Engine::DNSName->from_string(
$rr
->name ) ) }{
$rr
->address } = 1;
}
}
return
$p
;
}
elsif
(
my
$href
=
$state
->{glue}{
lc
( name(
$ns
) ) } ) {
foreach
my
$addr
(
keys
%$href
) {
my
$realns
= ns(
$ns
,
$addr
);
my
$p
=
$class
->_do_query(
$realns
,
$name
,
$type
,
$opts
,
$state
);
if
(
$p
) {
return
$p
;
}
}
return
;
}
else
{
$state
->{glue}{
lc
( name(
$ns
) ) } = {};
my
@addr
=
$class
->get_addresses_for(
$ns
,
$state
);
if
(
@addr
> 0 ) {
foreach
my
$addr
(
@addr
) {
$state
->{glue}{
lc
( name(
$ns
) ) }{
$addr
->short } = 1;
my
$new
= ns(
$ns
,
$addr
->short );
my
$p
=
$new
->query(
$name
,
$type
,
$opts
);
return
$p
if
$p
;
}
return
;
}
else
{
return
;
}
}
}
sub
get_ns_from {
my
(
$class
,
$p
,
$state
) =
@_
;
my
(
@new
,
@extra
);
my
@names
=
sort
map
{ Zonemaster::Engine::DNSName->from_string(
lc
(
$_
->nsdname ) ) }
$p
->get_records(
'ns'
);
$state
->{glue}{
lc
( Zonemaster::Engine::DNSName->from_string(
$_
->name ) ) }{
$_
->address } = 1
for
(
$p
->get_records(
'a'
),
$p
->get_records(
'aaaa'
) );
foreach
my
$name
(
@names
) {
if
(
exists
$state
->{glue}{
lc
(
$name
) } ) {
for
my
$addr
(
keys
%{
$state
->{glue}{
lc
(
$name
) } } ) {
push
@new
, ns(
$name
,
$addr
);
}
}
else
{
push
@extra
,
$name
;
}
}
@new
=
sort
{
$a
->name cmp
$b
->name or
$a
->address->ip cmp
$b
->address->ip }
@new
;
@extra
=
sort
{
$a
cmp
$b
}
@extra
;
return
[
@new
,
@extra
];
}
sub
get_addresses_for {
my
(
$class
,
$name
,
$state
) =
@_
;
my
@res
;
$state
//=
{
ns
=> [ root_servers() ],
count
=> 0,
common
=> 0,
seen
=> {} };
my
(
$pa
) =
$class
->_recurse(
"$name"
,
'A'
,
'IN'
,
{
ns
=> [ root_servers() ],
count
=>
$state
->{count},
common
=> 0,
in_progress
=>
$state
->{in_progress},
glue
=>
$state
->{glue}
}
);
if
(
$pa
and
$pa
->no_such_name ) {
return
;
}
my
(
$paaaa
) =
$class
->_recurse(
"$name"
,
'AAAA'
,
'IN'
,
{
ns
=> [ root_servers() ],
count
=>
$state
->{count},
common
=> 0,
in_progress
=>
$state
->{in_progress},
glue
=>
$state
->{glue}
}
);
my
@rrs
;
my
%cname
;
if
(
$pa
) {
push
@rrs
,
$pa
->get_records(
'a'
);
$cname
{
$_
->cname } = 1
for
$pa
->get_records_for_name(
'CNAME'
,
$name
);
}
if
(
$paaaa
) {
push
@rrs
,
$paaaa
->get_records(
'aaaa'
);
$cname
{
$_
->cname } = 1
for
$paaaa
->get_records_for_name(
'CNAME'
,
$name
);
}
foreach
my
$rr
(
sort
{
$a
->address cmp
$b
->address }
@rrs
) {
if
( name(
$rr
->name ) eq
$name
or
$cname
{
$rr
->name } ) {
push
@res
, Net::IP::XS->new(
$rr
->address );
}
}
return
@res
;
}
sub
_is_answer {
my
(
$class
,
$packet
) =
@_
;
return
(
$packet
->type eq
'answer'
);
}
sub
clear_cache {
%recurse_cache
= ();
return
;
}
sub
root_servers {
my
$root_addresses
=
$_fake_addresses_cache
{
'.'
};
my
@servers
;
for
my
$name
(
sort
keys
%{
$root_addresses
} ) {
for
my
$address
( @{
$root_addresses
->{
$name
} } ) {
push
@servers
, ns(
$name
,
$address
);
}
}
return
@servers
;
}
1;
Hide Show 161 lines of Pod