diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6000f412b..71703bae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: - develop #- latest perl: - - '5.40' + - '5.42' - '5.36' - '5.26' runner: @@ -35,6 +35,9 @@ jobs: with: perl-version: ${{ matrix.perl }} + - name: update + run: sudo apt update -y + - name: Binary dependencies run: | # * These were taken from the installation instruction. @@ -97,3 +100,41 @@ jobs: - name: Test run: | prove -lv t + + build-artifact: + needs: run-tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: update + run: sudo apt update -y + + - name: apt install + run: sudo apt-get install -y build-essential git libmodule-install-perl gettext + + - name: build + run: perl Makefile.PL && make all dist + + - name: Get short SHA + id: short_sha + run: | + if [ "${{ github.event_name }}" == "pull_request" ]; then + echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_ENV + else + echo "SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_ENV + fi + + - name: Get Zonemaster-Engine version + id: version + run: | + result=`grep "use version; our $VERSION" lib/Zonemaster/Engine.pm` + result+='printf $VERSION;' + VERSION=`perl -e "$result"` + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: upload artifact + uses: actions/upload-artifact@v4 + with: + name: Zonemaster-Engine-${{ steps.version.outputs.version }}-${{ env.SHORT_SHA }} + path: Zonemaster-Engine-${{ steps.version.outputs.version }}.tar.gz diff --git a/Changes b/Changes index 0eae12233..190c41eba 100644 --- a/Changes +++ b/Changes @@ -1,5 +1,26 @@ Release history for Zonemaster component Zonemaster-Engine +v9.0.0 2026-06-29 (part of Zonemaster v2026.1 release) + + [Breaking changes] + - Removes deprecated profile properties, + resolver.defaults.recurse, resolver.defaults.igntc + and resolver.defaults.usevc (#1532) + - Changes saved packets’ serialization format (#1517, #1531) + + [Features] + - Creates independent caching of custom name server recursive + lookups (#1521) + + [Fixes] + - Updates Swedish translation (#1533) + - Updates French translation (#1536) + - Replaces Perl INIT block with explicit initialization (#1530) + - Hardens test case Nameserver15 against creative version + strings (#1528) + - Updates test case DNSSEC10 implementation (#1523, #1539) + + v8.1.1 2026-03-04 (part of Zonemaster v2025.2.1 release) [Fixes] diff --git a/Dockerfile b/Dockerfile index 6b3ddb79c..7169078e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,6 +34,7 @@ RUN apk add --no-cache \ perl-yaml \ perl-yaml-libyaml \ && cpanm --no-wget --from=https://cpan.metacpan.org/ \ + CBOR::XS \ Email::Valid \ List::Compare \ Locale::PO \ diff --git a/Makefile.PL b/Makefile.PL index 90c15bd53..4c3b550c5 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -17,6 +17,7 @@ all_from 'lib/Zonemaster/Engine.pm'; # "2.1.0" could be declared as "2.001" but not as "2.1" # (see Zonemaster::LDNS below) +requires 'CBOR::XS' => 0; requires 'Class::Accessor' => 0; requires 'Clone' => 0; requires 'Email::Valid' => 0; @@ -34,7 +35,7 @@ requires 'Net::IP::XS' => 0.21; requires 'Readonly' => 0; requires 'Text::CSV' => 0; requires 'YAML::XS' => 0; -requires 'Zonemaster::LDNS' => 5.000002; # For v5.0.2 +requires 'Zonemaster::LDNS' => 5.001000; # For v5.1.0 test_requires 'Locale::PO' => 0; test_requires 'Pod::Coverage' => 0; diff --git a/README.md b/README.md index c4b43bc4d..adf0be5df 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,15 @@ read on the [CPAN site]. Documentation on Zonemaster-Engine is also found under the [docs] directory. +## CI artifact + +A tarball (`Zonemaster-Engine-.tar.gz`) is built and uploaded as a GitHub Actions artifact on every push and pull request. This artifact can be useful for release testing and PR review. +To download it: +1. Go to the [Actions tab](https://github.com/zonemaster/zonemaster-engine/actions) of the repository. +2. Select a workflow run (e.g. for a specific PR or branch). +3. Scroll to the bottom of the run summary to the **Artifacts** section. +4. Download the artifact named `Zonemaster-Engine--`. +The artifact name includes the module version and the first 7 characters of the commit SHA. ## Participation, Contact and Bug reporting diff --git a/lib/Zonemaster/Engine.pm b/lib/Zonemaster/Engine.pm index deb5b9a24..669f6a65b 100644 --- a/lib/Zonemaster/Engine.pm +++ b/lib/Zonemaster/Engine.pm @@ -3,7 +3,7 @@ package Zonemaster::Engine; use v5.16.0; use warnings; -use version; our $VERSION = version->declare("v8.1.1"); +use version; our $VERSION = version->declare("v9.0.0"); BEGIN { # Locale::TextDomain (<= 1.20) doesn't know about File::ShareDir so give a helping hand. @@ -26,20 +26,17 @@ use Zonemaster::Engine::Test; use Zonemaster::Engine::Recursor; use Zonemaster::Engine::ASNLookup; -INIT { - init_engine(); -} - our $logger; our $recursor = Zonemaster::Engine::Recursor->new; -my $init_done = 0; - +my $init_done; sub init_engine { return if $init_done++; Zonemaster::Engine::Recursor::init_recursor(); } +init_engine(); + sub logger { return $logger //= Zonemaster::Engine::Logger->new; } @@ -253,7 +250,7 @@ This manual describes the main L module. If what you're afte =item init_engine() -Run the initialization tasks if they have not been run already. This method is called automatically in INIT block. +Run the initialization tasks if they have not been run already. This method is called automatically at runtime. =item test_zone($name) diff --git a/lib/Zonemaster/Engine/Nameserver.pm b/lib/Zonemaster/Engine/Nameserver.pm index f270e8da7..ad6b500b4 100644 --- a/lib/Zonemaster/Engine/Nameserver.pm +++ b/lib/Zonemaster/Engine/Nameserver.pm @@ -8,25 +8,25 @@ use version; our $VERSION = version->declare("v1.1.16"); use Class::Accessor qw[ antlers ]; use Zonemaster::Engine::DNSName; -use Zonemaster::Engine; use Zonemaster::Engine::Packet; use Zonemaster::Engine::Nameserver::Cache; use Zonemaster::Engine::Recursor; use Zonemaster::Engine::Constants qw( :ip :misc ); use Zonemaster::LDNS; -use Net::IP::XS; -use Time::HiRes qw[time]; -use JSON::PP; -use MIME::Base64; -use Module::Find qw[useall]; use Carp qw( confess croak ); -use List::Util qw[max min sum]; +use CBOR::XS; use Digest::MD5; +use Fcntl qw( SEEK_SET ); +use List::Util qw( max min sum ); +use MIME::Base64; +use Module::Find qw( useall ); +use Net::IP::XS; use POSIX (); -use Scalar::Util qw[ blessed ]; +use Scalar::Util qw( blessed ); +use Time::HiRes qw( gettimeofday time tv_interval ); -our @ISA = qw (Class::Accessor); +our @ISA = qw( Class::Accessor ); use overload '""' => \&string, @@ -286,29 +286,10 @@ sub query { } ## end if ( $name =~ m/([.]|\A)\Q$fname\E\z/xi) } ## end foreach my $fname ( sort keys...) - my $md5 = Digest::MD5->new; - - $md5->add( q{NAME} , $name, - q{TYPE} , "\U$type", - q{CLASS} , "\U$class", - q{DNSSEC} , $dnssec, - q{USEVC} , $usevc, - q{RECURSE} , $recurse ); - - if ( exists $href->{edns_details} ) { - $md5->add( q{EDNS_VERSION} , $href->{edns_details}{version} // 0, - q{EDNS_Z} , $href->{edns_details}{z} // 0, - q{EDNS_EXTENDED_RCODE} , $href->{edns_details}{rcode} // 0, - q{EDNS_DATA} , $href->{edns_details}{data} // q{} ); - $edns_size = $href->{edns_details}{size} // ( $href->{edns_size} // ( $dnssec ? $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT : $EDNS_UDP_PAYLOAD_DEFAULT ) ); - } + my $idx = $self->_key_for_query_cache( $name, $type, $href ); croak "edns_size (or edns_details->size) parameter must be a value between 0 and 65535" if $edns_size > 65535 or $edns_size < 0; - $md5->add( q{EDNS_UDP_SIZE} , $edns_size ); - - my $idx = $md5->b64digest(); - my ( $in_cache, $p ) = $self->cache->get_key( $idx ); if ( not $in_cache ) { $p = $self->_query( $name, $type, $href ); @@ -374,6 +355,114 @@ sub add_fake_ds { return; } ## end sub add_fake_ds + +# Builds the Zonemaster::LDNS::Packet object that would be sent for a query, +# taking options into account. + +sub _make_query_packet { + my ( $self, $qname, $qtype, $opts ) = @_; + + $qtype //= 'A'; + my $qclass = $opts->{class} //= 'IN'; + + my $dnssec = do { + if ( exists $opts->{edns_details} and exists $opts->{edns_details}{do} ) { + $opts->{edns_details}{do}; + } + elsif ( exists $opts->{dnssec} ) { + $opts->{dnssec}; + } + else { + 0; + } + }; + + my $edns_size = do { + if ( exists $opts->{edns_details} and exists $opts->{edns_details}{size} ) { + $opts->{edns_details}{size}; + } + elsif ( exists $opts->{edns_size} ) { + $opts->{edns_size}; + } + elsif ( $dnssec ) { + $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT; + } + elsif ( exists $opts->{edns_details} ) { + $EDNS_UDP_PAYLOAD_DEFAULT; + } + else { + 0; + } + }; + + die "Invalid value $edns_size for EDNS payload size (should be in 0..65535 range)" + unless $edns_size >= 0 and $edns_size <= 65535; + + my $packet = Zonemaster::LDNS::Packet->new( "$qname", $qtype, $qclass ); + $packet->rd($opts->{recurse} // 0); + + if ( exists $opts->{edns_details} ) { + $packet->set_edns_present(); + + if ( exists $opts->{edns_details}{version} ) { + $packet->edns_version($opts->{edns_details}{version}); + } + if ( exists $opts->{edns_details}{z} ) { + $packet->edns_z($opts->{edns_details}{z}); + } + if ( exists $opts->{edns_details}{rcode} ) { + $packet->edns_rcode($opts->{edns_details}{rcode}); + } + if ( exists $opts->{edns_details}{data} ) { + $packet->edns_data($opts->{edns_details}{data}); + } + } + + $packet->do($dnssec); + $packet->edns_size($edns_size); + + return $packet; +} + + +# Computes the key to use to search the cache for a packet corresponding to a +# query we have previously sent. + +sub _key_for_query_cache { + my ( $self, $name, $type, $href ) = @_; + + my $usevc = $href->{usevc} // 0; + + my $pkt = $self->_make_query_packet( $name, $type, $href ); + + # Repurpose the ID field. We want this field to be zeroed out + # in order to match queries with the exact same contents (except + # transaction ID), but we may want cache keys to differ on other + # attributes, for example transport. + # + # Currently the layout is as follows: + # + # 15 8 + # +-----+-----+-----+-----+-----+-----+-----+-----+ + # | TCP | (set to 0) | + # +-----+-----+-----+-----+-----+-----+-----+-----+ + # + # 7 0 + # +-----+-----+-----+-----+-----+-----+-----+-----+ + # | (set to 0) | + # +-----+-----+-----+-----+-----+-----+-----+-----+ + # + # where: + # * bit 15 is set to 1 if the query is sent over TCP, 0 otherwise + + my $fake_id = 0; + $fake_id |= (1 << 15) if $usevc; + + $pkt->id($fake_id); + + return $pkt->wireformat(); +} + sub _query { my ( $self, $name, $type, $href ) = @_; my %flags; @@ -440,31 +529,8 @@ sub _query { ); } else { - if ( exists $href->{edns_details} ) { - my $pkt = Zonemaster::LDNS::Packet->new("$name", $type, $href->{class} ); - $pkt->set_edns_present(); - - $pkt->do($flags{q{dnssec}}); - $pkt->edns_size($flags{q{edns_size}}); - - if ( exists $href->{edns_details}{version} ) { - $pkt->edns_version($href->{edns_details}{version}); - } - if ( exists $href->{edns_details}{z} ) { - $pkt->edns_z($href->{edns_details}{z}); - } - if ( exists $href->{edns_details}{rcode} ) { - $pkt->edns_rcode($href->{edns_details}{rcode}); - } - if ( exists $href->{edns_details}{data} ) { - $pkt->edns_data($href->{edns_details}{data}); - } - - $res = eval { $self->dns->query_with_pkt( $pkt ) }; - } - else { - $res = eval { $self->dns->query( "$name", $type, $href->{class} ) }; - } + my $pkt = $self->_make_query_packet( $name, $type, $href ); + $res = eval { $self->dns->query_with_pkt( $pkt ) }; if ( $@ ) { my $msg = "$@"; @@ -514,71 +580,133 @@ sub compare { return $self->string cmp $other->string; } + +# Converts a Zonemaster::Engine::Packet object to a predictable representation +# as an array, which can then be turned into CBOR. + +sub _serialize_packet { + my ( $packet ) = @_; + + return undef if not defined $packet; + + return [ + $packet->packet->wireformat(), + $packet->packet->answerfrom(), + $packet->packet->timestamp(), + $packet->packet->querytime(), + ]; +} + sub save { my ( $class, $filename ) = @_; - my $old = POSIX::setlocale( POSIX::LC_ALL, 'C' ); - my $json = JSON::PP->new->allow_blessed->convert_blessed; - $json = $json->canonical( 1 ); - open my $fh, '>', $filename or die "Cache save failed: $!"; - foreach my $name ( sort keys %object_cache ) { - foreach my $addr ( sort keys %{ $object_cache{$name} } ) { - say $fh "$name $addr " . $json->encode( $object_cache{$name}{$addr}->cache->data ); + + my $dumped_contents = { + format_version => 1, + engine_version => Zonemaster::Engine->VERSION(), + packets => do { + # Gives an array of (nameserver, cached packets) pairs. + # A nameserver is itself a (name, IP) pair and each cached packet + # is represented as a (bytes, source IP, timestamp, query time) tuple, + # or undef if there was no response. + my @result; + foreach my $name ( sort keys %object_cache ) { + foreach my $addr ( sort keys %{ $object_cache{$name} } ) { + my @ns_packets; + my $cached_data = $object_cache{$name}{$addr}->cache->data; + + next if scalar %$cached_data == 0; + + foreach my $entry_key ( sort keys %$cached_data ) { + push @ns_packets, [ $entry_key, _serialize_packet($cached_data->{$entry_key}) ]; + } + push @result, [ [ $name, $addr ], \@ns_packets ]; + } + }; + \@result; } - } + }; + + my $cbor = CBOR::XS->new(); + + # We begin the file with the CBOR magic value deliberately, so that we can + # reject files that do not start with that signature on loading. + print $fh $CBOR::XS::MAGIC, $cbor->encode($dumped_contents); close $fh or die $!; Zonemaster::Engine->logger->add( SAVED_NS_CACHE => { file => $filename } ); +} - POSIX::setlocale( POSIX::LC_ALL, $old ); - return; + +# Performs the inverse operation of _serialize_packet(): from a deserialized +# CBOR array, reconstructs a Zonemaster::Engine::Packet object. + +sub _deserialize_packet { + my ( $cbor_packet ) = @_; + + return undef if not defined $cbor_packet; + + my ( $bytes, $answerfrom, $timestamp, $querytime ) = @$cbor_packet; + my $packet = Zonemaster::Engine::Packet->new( + { packet => Zonemaster::LDNS::Packet->new_from_wireformat( $bytes ) } + ); + $packet->answerfrom( $answerfrom ) if defined $answerfrom; + $packet->timestamp( $timestamp ) if defined $timestamp; + $packet->querytime( $querytime ) if defined $querytime; + + return $packet; } sub restore { my ( $class, $filename ) = @_; - useall 'Zonemaster::LDNS::RR'; - my $decode = JSON::PP->new->filter_json_single_key_object( - 'Zonemaster::LDNS::Packet' => sub { - my ( $ref ) = @_; - ## no critic (Modules::RequireExplicitInclusion) - my $obj = Zonemaster::LDNS::Packet->new_from_wireformat( decode_base64( $ref->{data} ) ); - $obj->answerfrom( $ref->{answerfrom} ); - $obj->timestamp( $ref->{timestamp} ); + open my $fh, '<', $filename or die "Failed to open restore data file: $!\n"; - return $obj; - } - )->filter_json_single_key_object( - 'Zonemaster::Engine::Packet' => sub { - my ( $ref ) = @_; - return Zonemaster::Engine::Packet->new( { packet => $ref } ); - } - ); + # Expect CBOR magic string at beginning of file. + my $found_magic = do { + my $buf; + my $len = read($fh, $buf, length($CBOR::XS::MAGIC)); + seek($fh, 0, SEEK_SET) or die "seek: $!"; + ($len == 3 and $buf eq $CBOR::XS::MAGIC); + }; + croak "The restore data file seems to be corrupted" if not $found_magic; + + my $cbor = CBOR::XS->new(); + my $saved_contents = $cbor->decode(do { local $/; <$fh> }); + close $fh; + + my $format_version = $saved_contents->{format_version}; + croak "Unsupported format version $format_version" if $format_version != 1; my $cache_type = Zonemaster::Engine::Nameserver::Cache->get_cache_type( Zonemaster::Engine::Profile->effective ); my $cache_class = Zonemaster::Engine::Nameserver::Cache->get_cache_class( $cache_type ); - open my $fh, '<', $filename or die "Failed to open restore data file: $!\n"; - while ( my $line = <$fh> ) { - my ( $name, $addr, $data ) = split( / /, $line, 3 ); - my $ref = $decode->decode( $data ); - my $ns = Zonemaster::Engine::Nameserver->new( + foreach my $entry ( @{ $saved_contents->{packets} } ) { + my ( $ns_pair, $ns_packets ) = @$entry; + + my $data = {}; + foreach my $ns_cache_entry ( @$ns_packets ) { + my ( $key, $value ) = @$ns_cache_entry; + $data->{$key} = _deserialize_packet($value); + } + + my $addr = Net::IP::XS->new( $ns_pair->[1] ); + my $ns = Zonemaster::Engine::Nameserver->new( { - name => $name, - address => Net::IP::XS->new($addr), - cache => $cache_class->new( { data => $ref, address => Net::IP::XS->new( $addr ) } ) + name => $ns_pair->[0], + address => $addr, + cache => $cache_class->new( { data => $data, address => $addr } ) } ); } - close $fh; Zonemaster::Engine->logger->add( RESTORED_NS_CACHE => { file => $filename } ); return; -} ## end sub restore +} sub max_time { my ( $self ) = @_; @@ -647,10 +775,38 @@ sub axfr { my ( $self, $domain, $callback, $class ) = @_; $class //= 'IN'; + my $idx = $self->_key_for_query_cache( $domain, 'AXFR', { class => $class, usevc => 1 } ); + my ( $in_cache, $p ) = $self->cache->get_key( $idx ); + + if ( $in_cache ) { + if ( $p->rcode() ne 'NOERROR' ) { + # Croak with the same error message the real AXFR croaked with. + my ( undef, $ede_text ) = $p->packet->first_ede(); + croak $ede_text // "AXFR transfer error: REFUSED"; + } + + my $last_ret = 1; + foreach my $rr ( $p->answer() ) { + $last_ret = $callback->($rr); + last if $last_ret == 0; + } + return $last_ret; + } + else { + my ( $ret, $error, $p ) = $self->_axfr( $domain, $callback, $class ); + $self->cache->set_key( $idx, $p ); + croak $error if defined $error; + return $ret; + } +} + +sub _axfr { + my ( $self, $domain, $callback, $class ) = @_; + if ( Zonemaster::Engine::Profile->effective->get( q{no_network} ) ) { croak sprintf - "External AXFR query for %s attempted to %s while running with no_network", - $domain, $self->string; + "External AXFR query for %s attempted to %s while running with no_network", + $domain, $self->string; } if ( $self->address->version == 4 and not Zonemaster::Engine::Profile->effective->get( q{net.ipv4} ) ) { @@ -663,8 +819,43 @@ sub axfr { return; } - return $self->dns->axfr( $domain, $callback, $class ); -} ## end sub axfr + my @rrs; + my $wrapped_callback = sub { + push @rrs, $_[0]; + return $callback->( @_ ); + }; + + my $t0 = [gettimeofday]; + my $ret = eval { $self->dns->axfr( $domain, $wrapped_callback, $class ) }; + my $error = $@ if not defined $ret; + my $querytime = tv_interval($t0); + + # Build a synthetic packet containing all the resource records we + # collected, so that this AXFR can be cached (and therefore replayed) + # adequately. + my $p = Zonemaster::Engine::Packet->new({ + packet => Zonemaster::LDNS::Packet->new( $domain, 'AXFR', $class ) + }); + + $p->timestamp(time()); + $p->querytime($querytime * 1000); + $p->answerfrom($self->address->short); + $p->id(0); + $p->packet->qr(1); + if ( defined $error ) { + $p->rcode('REFUSED'); + # Use an Extended DNS Error 13 (Cached Error) in the synthetic packet + # to store the original error message. + my $file = __FILE__; + chomp $error; + $error =~ s/ at $file line \d+\.$//; + $p->packet->first_ede( 13, $error ); + } + $p->packet->aa(1); + $p->unique_push( 'answer', $_ ) foreach @rrs; + + return $ret, $error, $p; +} sub source_address { my ( $self ) = @_; diff --git a/lib/Zonemaster/Engine/Profile.pm b/lib/Zonemaster/Engine/Profile.pm index f1293c53c..4e93344d0 100644 --- a/lib/Zonemaster/Engine/Profile.pm +++ b/lib/Zonemaster/Engine/Profile.pm @@ -55,15 +55,9 @@ my %profile_properties_details = ( q{resolver.defaults.debug} => { type => q{Bool} }, - q{resolver.defaults.igntc} => { - type => q{Bool} - }, q{resolver.defaults.fallback} => { type => q{Bool} }, - q{resolver.defaults.recurse} => { - type => q{Bool} - }, q{resolver.defaults.retrans} => { type => q{Num}, min => 1, @@ -74,9 +68,6 @@ my %profile_properties_details = ( min => 1, max => 255 }, - q{resolver.defaults.usevc} => { - type => q{Bool} - }, q{resolver.defaults.timeout} => { type => q{Num} }, @@ -630,7 +621,7 @@ Internal method used to get a value in a nested hashes-of-hashes. Where $hash_ref is the hash to explore and @path are the labels of the property to get. - @path = split /\./, q{resolver.defaults.usevc}; + @path = split /\./, q{resolver.defaults.timeout}; =head2 _set_value_to_nested_hash @@ -640,7 +631,7 @@ Internal method used to set a value in a nested hashes-of-hashes. Where $hash_ref is the hash to explore and @path are the labels of the property to set. - @path = split /\./, q{resolver.defaults.usevc}; + @path = split /\./, q{resolver.defaults.timeout}; =head1 PROFILE PROPERTIES @@ -688,18 +679,6 @@ If set to "" (empty string), the OS default IPv6 address is used. Default: "" (empty string). -=head2 resolver.defaults.igntc - -A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file. - -=head2 resolver.defaults.recurse - -A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file. - -=head2 resolver.defaults.usevc - -A boolean. Default false. Ignored. Deprecated and planned for removal in v2026.1. Remove it from your profile file. - =head2 net.ipv4 A boolean. If true, resolver objects are allowed to send queries over diff --git a/lib/Zonemaster/Engine/Recursor.pm b/lib/Zonemaster/Engine/Recursor.pm index 31e2abc66..c6fdf16a8 100644 --- a/lib/Zonemaster/Engine/Recursor.pm +++ b/lib/Zonemaster/Engine/Recursor.pm @@ -12,13 +12,13 @@ use File::Slurp qw( read_file ); use JSON::PP; use Net::IP::XS; use List::MoreUtils qw[uniq]; +use Memoize; -use Zonemaster::Engine; +use Zonemaster::Engine::Nameserver; use Zonemaster::Engine::DNSName; use Zonemaster::Engine::Util qw( name ns parse_hints ); use Zonemaster::Engine::Constants ":cname"; -our %recurse_cache; our %_fake_addresses_cache; sub init_recursor { @@ -95,22 +95,40 @@ sub recurse { $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; + my %state = ( ns => defined $ns ? [ @$ns ] : [ root_servers() ], count => 0, common => 0, seen => {}, glue => {} ); + + my ( $p, $state_final ) = $class->_recurse( $name, $type, $dns_class, \%state ); return $p; } +# Cache the results of recurse() to speed up repeated queries for the same name, type, +# class and set of name servers. Parameters are normalized to ensure that the cache is +# hit for semantically identical queries. +memoize('recurse', NORMALIZER => sub { + my ( $class, $name, $type, $dns_class, $ns ) = @_; + + $name = name( $name ); + $type //= 'A'; + $dns_class //= 'IN'; + $ns //= [ root_servers() ]; + + my $nss = join( ',', sort @$ns ); + + return join( '|', + ref($class) || $class, + $name, + $type, + $dns_class, + $nss + ); +}); + sub parent { my ( $class, $name ) = @_; $name = name( $name ); @@ -336,7 +354,7 @@ sub _recurse { next if $common < $state->{common}; # Redirect going up the hierarchy is not OK $state->{common} = $common; - $state->{ns} = $class->get_ns_from( $p, $state ); # Follow redirect + $state->{ns} = $class->_get_ns_from( $p, $state ); # Follow redirect $state->{count} += 1; if ( $state->{count} > 20 ) { # Loop protection Zonemaster::Engine->logger->add( LOOP_PROTECTION => { @@ -399,7 +417,7 @@ sub _do_query { } } ## end sub _do_query -sub get_ns_from { +sub _get_ns_from { my ( $class, $p, $state ) = @_; my ( @new, @extra ); @@ -423,7 +441,7 @@ sub get_ns_from { @extra = sort { $a cmp $b } @extra; return [ @new, @extra ]; -} ## end sub get_ns_from +} ## end sub _get_ns_from sub get_addresses_for { my ( $class, $name, $state ) = @_; @@ -484,7 +502,7 @@ sub _is_answer { } sub clear_cache { - %recurse_cache = (); + Memoize::flush_cache(\&recurse); return; } @@ -514,10 +532,6 @@ Zonemaster::Engine::Recursor - recursive resolver for Zonemaster =head1 CLASS VARIABLES -=head2 %recurse_cache - -Will cache result of previous queries. - =head2 %_fake_addresses_cache A hash of hashrefs of arrayrefs. @@ -550,10 +564,6 @@ Returns a L object (which can be C). Does a recursive resolution from the root down for the given name (using type C and class C). If the resolution is successful, it returns the domain name of the second-to-last step. If the resolution is unsuccessful, it returns the domain name of the last step. -=head2 get_ns_from($packet, $state) - -Internal method. Takes a packet and a recursion state and returns a list of ns objects. Used to follow redirections. - =head2 get_addresses_for($name[, $state]) Takes a name and returns a (possibly empty) list of IP addresses for @@ -591,7 +601,7 @@ N.B. This method does not affect fake delegation data. =head2 root_servers() -Returns a list of ns objects representing the root servers. +Returns a list of L objects representing the root servers. my @name_servers = Zonemaster::Engine::Recursor->root_servers(); @@ -621,6 +631,16 @@ The mandatory keys for that hash are 'ns' (arrayref), 'count' (integer), 'common Returns a L (or C) and a hash. +=head2 _get_ns_from() + + my @ns = _get_ns_from( $packet, $state ); + +Used to follow redirections by the L helper method in this module. + +Takes a L object and a reference to a hash. + +Returns a list of L objects. + =head2 _resolve_cname() my ( $p, $state_hash ) = _resolve_cname( $name, $type_string, $dns_class_string, $p, $state_hash ); diff --git a/lib/Zonemaster/Engine/Test/DNSSEC.pm b/lib/Zonemaster/Engine/Test/DNSSEC.pm index df882195a..cd42817f6 100644 --- a/lib/Zonemaster/Engine/Test/DNSSEC.pm +++ b/lib/Zonemaster/Engine/Test/DNSSEC.pm @@ -528,7 +528,6 @@ sub metadata { DS10_ALGO_NOT_SUPPORTED_BY_ZM DS10_ERR_MULT_NSEC DS10_ERR_MULT_NSEC3 - DS10_ERR_MULT_NSEC3PARAM DS10_EXPECTED_NSEC_NSEC3_MISSING DS10_HAS_NSEC DS10_HAS_NSEC3 @@ -561,6 +560,7 @@ sub metadata { DS10_NSEC_RRSIG_NOT_YET_VALID DS10_NSEC_RRSIG_NO_DNSKEY DS10_NSEC_RRSIG_VERIFY_ERROR + DS10_NONSTANDARD_NSEC_RESPONSE DS10_SERVER_NO_DNSSEC DS10_ZONE_NO_DNSSEC ) @@ -1204,11 +1204,6 @@ Readonly my %TAG_DESCRIPTIONS => ( 'Multiple NSEC3 records when one is expected. Fetched from name servers "{ns_list}".', @_; }, - DS10_ERR_MULT_NSEC3PARAM => sub { - __x # DNSSEC:DS10_ERR_MULT_NSEC3PARAM - 'Multiple NSEC3PARAM records when one is expected. Fetched from name servers "{ns_list}".', - @_; - }, DS10_EXPECTED_NSEC_NSEC3_MISSING => sub { __x # DNSSEC:DS10_EXPECTED_NSEC_NSEC3_MISSING 'The server responded with DNSKEY but not with expected NSEC or NSEC3. ' @@ -1397,6 +1392,13 @@ Readonly my %TAG_DESCRIPTIONS => ( . 'Fetched from name servers "{ns_list}".', @_; }, + DS10_NONSTANDARD_NSEC_RESPONSE => sub { + __x # DNSSEC:DS10_NONSTANDARD_NSEC_RESPONSE + 'The following name servers give a non-standard response to the NSEC query ' + . '(NSEC RR in authority section instead of answer section). ' + . 'Fetched from name servers "{ns_list}".', + @_; + }, DS10_SERVER_NO_DNSSEC => sub { __x # DNSSEC:DS10_SERVER_NO_DNSSEC 'The following name servers do not support DNSSEC or have not been properly ' @@ -3561,8 +3563,8 @@ sub dnssec10 { my @query_types = ( $type_dnskey, $type_nsec, $type_nsec3param ); my %algo_not_supported_by_zm; - my ( @erroneous_multiple_nsec, @erroneous_multiple_nsec3, @erroneous_multiple_nsec3param ); - my ( @nsec_in_answer, @nsec3param_in_answer ); + my ( @erroneous_multiple_nsec, @erroneous_multiple_nsec3 ); + my ( @nsec_in_response, @nsec_nonstandard_nodata, @nsec3param_in_answer ); my ( @nsec_incorrect_type_list, @nsec3_incorrect_type_list ); my ( @nsec_mismatches_apex, @nsec3_mismatches_apex, @nsec3param_mismatches_apex ); my ( @nsec_missing_signature, @nsec3_missing_signature ); @@ -3621,7 +3623,7 @@ sub dnssec10 { } elsif ( $nsec_p->answer ) { if ( scalar $nsec_p->get_records( $type_nsec, q{answer} ) ) { - push @nsec_in_answer, @all_ns_for_ip; + push @nsec_in_response, @all_ns_for_ip; if ( scalar $nsec_p->get_records( $type_nsec, q{answer} ) > 1 ) { push @erroneous_multiple_nsec, @all_ns_for_ip; @@ -3634,9 +3636,92 @@ sub dnssec10 { push @nsec_erroneous_answer, @all_ns_for_ip; } } - elsif ( not $nsec_p->answer and scalar $nsec_p->get_records( $type_nsec3, q{authority} ) ) { - my @nsec3_rrs = $nsec_p->get_records( $type_nsec3, q{authority} ); + elsif ( not $nsec_p->answer and scalar $nsec_p->get_records( $type_nsec, q{authority} ) ) { + push @nsec_in_response, @all_ns_for_ip; + push @nsec_nonstandard_nodata, @all_ns_for_ip; + unless ( scalar $nsec_p->get_records( $type_soa, q{authority} ) ) { + push @nsec_nodata_missing_soa, @all_ns_for_ip; + } + elsif ( ($nsec_p->get_records( $type_soa, q{authority} ))[0]->owner ne $zone->name ) { + push @{ $nsec_nodata_wrong_soa{$zone->name} }, @all_ns_for_ip; + } + + my @nsec_rrs = $nsec_p->get_records( $type_nsec, q{authority} ); + + if ( scalar @nsec_rrs > 1 ) { + push @erroneous_multiple_nsec, @all_ns_for_ip; + } + else { + if ( $nsec_rrs[0]->owner ne $zone->name ) { + push @nsec_mismatches_apex, @all_ns_for_ip; + } + else { + my @mandatory_typelist = qw( SOA NS DNSKEY NSEC RRSIG ); + my @forbidden_typelist = qw( NSEC3PARAM NSEC3 ); + my %typelist = %{ $nsec_rrs[0]->typehref }; + + foreach my $type ( @mandatory_typelist ) { + if ( not exists $typelist{$type} ) { + push @nsec_incorrect_type_list, @all_ns_for_ip; + last; + } + } + + foreach my $type ( @forbidden_typelist ) { + if ( exists $typelist{$type} ) { + push @nsec_incorrect_type_list, @all_ns_for_ip; + last; + } + } + } + + my @nsec_rrsig_rrs = grep { $_->typecovered eq q{NSEC} } $nsec_p->get_records_for_name( q{RRSIG}, $nsec_rrs[0]->name ); + + unless ( scalar @nsec_rrsig_rrs ) { + push @nsec_missing_signature, @all_ns_for_ip; + } + else { + foreach my $rr ( @nsec_rrsig_rrs ) { + my @matching_dnskeys = grep { $rr->keytag == $_->keytag } @dnskey_records; + + unless ( scalar @matching_dnskeys ) { + push @{ $nsec_rrsig_no_dnskey{$rr->keytag} }, @all_ns_for_ip; + } + elsif ( $rr->expiration < $testing_time ) { + push @{ $nsec_rrsig_expired{$rr->keytag} }, @all_ns_for_ip; + } + elsif ( $rr->inception > $testing_time ) { + push @{ $nsec_rrsig_not_yet_valid{$rr->keytag} }, @all_ns_for_ip; + } + else { + my $i = 1; + foreach my $dnskey ( @matching_dnskeys ) { + my $msg = q{}; + my $validated = $rr->verify_time( [grep { name( $_->name ) eq name( $rr->name ) } @nsec_rrs], [ $dnskey ], $testing_time, $msg ); + + if ( $validated ) { + push @nsec_rrsig_verified, @all_ns_for_ip; + last; + } + + if ( $i >= scalar @matching_dnskeys ) { + if ( $msg =~ /Unknown cryptographic algorithm/ ) { + push @{ $algo_not_supported_by_zm{$dnskey->keytag}{$dnskey->algorithm} }, @all_ns_for_ip; + } + else { + push @{ $nsec_rrsig_verify_error{$dnskey->keytag} }, @all_ns_for_ip; + } + } + + $i++; + } + } + } + } + } + } + elsif ( not $nsec_p->answer and scalar $nsec_p->get_records( $type_nsec3, q{authority} ) ) { push @nsec_nsec3_nodata, @all_ns_for_ip; unless ( scalar $nsec_p->get_records( $type_soa, q{authority} ) ) { @@ -3646,6 +3731,8 @@ sub dnssec10 { push @{ $nsec3_nodata_wrong_soa{$zone->name} }, @all_ns_for_ip; } + my @nsec3_rrs = $nsec_p->get_records( $type_nsec3, q{authority} ); + if ( scalar @nsec3_rrs > 1 ) { push @erroneous_multiple_nsec3, @all_ns_for_ip; } @@ -3728,10 +3815,7 @@ sub dnssec10 { if ( scalar $nsec3param_p->get_records( $type_nsec3param, q{answer} ) ) { push @nsec3param_in_answer, @all_ns_for_ip; - if ( scalar $nsec3param_p->get_records( $type_nsec3param, q{answer} ) > 1 ) { - push @erroneous_multiple_nsec3param, @all_ns_for_ip; - } - elsif ( ($nsec3param_p->get_records( $type_nsec3param, q{answer} ))[0]->owner ne $zone->name ) { + if ( ($nsec3param_p->get_records( $type_nsec3param, q{answer} ))[0]->owner ne $zone->name ) { push @nsec3param_mismatches_apex, @all_ns_for_ip; } } @@ -3843,16 +3927,16 @@ sub dnssec10 { ); } - if ( scalar @erroneous_multiple_nsec3param ) { + if ( scalar @nsec_nonstandard_nodata ) { push @results, _emit_log( - DS10_ERR_MULT_NSEC3PARAM => { - ns_list => join( q{;}, uniq sort @erroneous_multiple_nsec3param ) + DS10_NONSTANDARD_NSEC_RESPONSE => { + ns_list => join( q{;}, uniq sort @nsec_nonstandard_nodata ) } ); } - my $lc = List::Compare->new( \@nsec_in_answer, \@nsec3param_nsec_nodata ); + my $lc = List::Compare->new( \@nsec_in_response, \@nsec3param_nsec_nodata ); my @diff = $lc->get_symmetric_difference; my @union = uniq map { $_->string } ( @nsec3param_in_answer, @nsec_nsec3_nodata ); my $lc2 = List::Compare->new( \@diff, \@union ); @@ -3869,7 +3953,7 @@ sub dnssec10 { $lc = List::Compare->new( \@nsec3param_in_answer, \@nsec_nsec3_nodata ); @diff = $lc->get_symmetric_difference; - @union = uniq map { $_->string } ( @nsec_in_answer, @nsec3param_nsec_nodata ); + @union = uniq map { $_->string } ( @nsec_in_response, @nsec3param_nsec_nodata ); $lc2 = List::Compare->new( \@diff, \@union ); @final_diff = $lc2->get_symmetric_difference; @@ -3882,7 +3966,7 @@ sub dnssec10 { ); } - $lc = List::Compare->new( [ @nsec3param_in_answer, @nsec_nsec3_nodata ], [ @nsec_in_answer, @nsec3param_nsec_nodata ] ); + $lc = List::Compare->new( [ @nsec3param_in_answer, @nsec_nsec3_nodata ], [ @nsec_in_response, @nsec3param_nsec_nodata ] ); my @intersection = $lc->get_intersection; if ( @intersection ) { @@ -3894,16 +3978,16 @@ sub dnssec10 { ); } - if ( ( scalar @nsec_in_answer or @nsec3param_nsec_nodata ) and not scalar @nsec3param_in_answer and not scalar @nsec_nsec3_nodata ) { + if ( ( scalar @nsec_in_response or @nsec3param_nsec_nodata ) and not scalar @nsec3param_in_answer and not scalar @nsec_nsec3_nodata ) { push @results, _emit_log( DS10_HAS_NSEC => { - ns_list => join( q{;}, uniq sort ( @nsec_in_answer, @nsec3param_nsec_nodata ) ) + ns_list => join( q{;}, uniq sort ( @nsec_in_response, @nsec3param_nsec_nodata ) ) } ); } - if ( ( scalar @nsec3param_in_answer or @nsec_nsec3_nodata ) and not scalar @nsec_in_answer and not scalar @nsec3param_nsec_nodata ) { + if ( ( scalar @nsec3param_in_answer or @nsec_nsec3_nodata ) and not scalar @nsec_in_response and not scalar @nsec3param_nsec_nodata ) { push @results, _emit_log( DS10_HAS_NSEC3 => { @@ -3913,7 +3997,7 @@ sub dnssec10 { } @union = ( @nsec3param_in_answer, @nsec_nsec3_nodata ); - my @second_union = ( @nsec_in_answer, @nsec3param_nsec_nodata ); + my @second_union = ( @nsec_in_response, @nsec3param_nsec_nodata ); $lc = List::Compare->new( \@union, \@second_union ); my @first = $lc->get_unique; my @second = $lc->get_complement; @@ -4223,7 +4307,7 @@ sub dnssec10 { ); } - $lc = List::Compare->new( [ @all_ns ], [ @ignored_nss, @without_dnskey, @nsec_in_answer, @nsec3param_nsec_nodata, @nsec3param_in_answer, @nsec_nsec3_nodata ] ); + $lc = List::Compare->new( [ @all_ns ], [ @ignored_nss, @without_dnskey, @nsec_in_response, @nsec3param_nsec_nodata, @nsec3param_in_answer, @nsec_nsec3_nodata ] ); @first = $lc->get_unique; if ( @first ) { diff --git a/lib/Zonemaster/Engine/Test/Nameserver.pm b/lib/Zonemaster/Engine/Test/Nameserver.pm index ee7e4b701..4677a3953 100644 --- a/lib/Zonemaster/Engine/Test/Nameserver.pm +++ b/lib/Zonemaster/Engine/Test/Nameserver.pm @@ -5,17 +5,17 @@ use warnings; use version; our $VERSION = version->declare( "v1.1.0" ); +use JSON::PP; use List::MoreUtils qw[uniq none]; use Locale::TextDomain qw[Zonemaster-Engine]; -use Readonly; -use JSON::PP; use Net::IP::XS; +use Readonly; -use Zonemaster::Engine::Profile; use Zonemaster::Engine::Constants qw[:ip]; +use Zonemaster::Engine::Profile; use Zonemaster::Engine::Test::Address; -use Zonemaster::Engine::Util; use Zonemaster::Engine::TestMethods; +use Zonemaster::Engine::Util qw( escape_unprintable scramble_case ); =head1 NAME @@ -1747,10 +1747,10 @@ sub nameserver15 { push @wrong_record_class, $ns; } - my $string = $rr->txtdata; + my $string = escape_unprintable($rr->txtdata); $string =~ s/^\s+|\s+$//g; # Remove leading and trailing spaces - if ( $string and $string ne "") { + if ( $string ne "" ) { push @{ $txt_data{$string}{$query_name} }, $ns; delete $sending_version_query{$ns}; } diff --git a/lib/Zonemaster/Engine/Util.pm b/lib/Zonemaster/Engine/Util.pm index 77e4808eb..657edffc8 100644 --- a/lib/Zonemaster/Engine/Util.pm +++ b/lib/Zonemaster/Engine/Util.pm @@ -8,6 +8,7 @@ use version; our $VERSION = version->declare("v1.1.13"); use Exporter 'import'; BEGIN { our @EXPORT_OK = qw[ + escape_unprintable info ipversion_ok name @@ -27,10 +28,10 @@ BEGIN { use Net::DNS::ZoneFile; use Pod::Simple::SimpleTree; -use Zonemaster::Engine; use Zonemaster::Engine::Constants qw[:ip :soa]; use Zonemaster::Engine::DNSName; use Zonemaster::Engine::Profile; +use Zonemaster::Engine::Nameserver; sub ns { my ( $name, $address ) = @_; @@ -185,6 +186,39 @@ sub serial_gt { ); } +{ + # HACK: It would be cleaner to use a state variable, but Perl versions + # older than 5.28.0 disallow initializing list state variables, e.g. + # writing “state @a = qw(a b c)”. + # + # A workaround could be to use a state arrayref variable for the + # substitution table, but this incurs a performance penalty. + # + # Once we support Perl ⩾ 5.28, we can rewrite this function using proper + # state variables. See also: + # . + my @substitution_table = map { + if ( $_ == 92 ) { + '\\\\' + } + elsif ( $_ >= 32 and $_ <= 126 ) { + chr $_ + } + else { + sprintf '\%03d', $_ + } + } 0..255; + + sub escape_unprintable { + my ( $input ) = @_; + + my $output = ''; + $output .= $substitution_table[$_] foreach (unpack "C*", $input); + + return $output; + } +} + 1; =head1 NAME @@ -285,6 +319,31 @@ Check if a test is blacklisted and should run or not. Check if IP version operations are permitted. Tests are done against Zonemaster::Engine::Profile->effective content. +=item escape_unprintable + +Replaces all non-ASCII characters and control characters in a byte string with +decimal escape codes. The resulting string only contains printable ASCII +characters, which makes it safer for display on a terminal or for storage in a +database. + +For example, C<"hello \x1B[34mworld!\x1B[0m\\\xFF"> is turned into +C<'hello \027[34mworld!\027[0m\\\255'>. + +Do not use this function on character strings, or it might produce invalid +results. Only use it on byte strings, e.g. UTF-8 encoded text returned by +calling Encode::encode() on a character string, or raw data received from +Zonemaster-LDNS. + +Space (ASCII 0x20) characters are left as they are, but all other ASCII +whitespace characters, i.e. HORIZONTAL TAB (ASCII 0x09), LINE FEED (ASCII +0x0A), VERTICAL TAB (ASCII 0x0B), FORM FEED (ASCII 0x0C) and CARRIAGE RETURN +(ASCII 0x0D) will be replaced by their equivalent decimal escapes. + +Beware that the resulting string might still contain sequences of characters +that can be dangerous in other contexts, requiring further escaping. For +example, if the string is meant to be displayed on an HTML GUI, the result +must have angle brackets and ampersand characters escaped in a second pass. + =item test_levels WIP, here to please L. diff --git a/lib/Zonemaster/Engine/Zone.pm b/lib/Zonemaster/Engine/Zone.pm index 3d73e19f8..03c9c811a 100644 --- a/lib/Zonemaster/Engine/Zone.pm +++ b/lib/Zonemaster/Engine/Zone.pm @@ -457,7 +457,8 @@ parent domain. =item query_one($name[, $type[, $flags]]) 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 -response, it will be returned in a L object. If the type arguments is not given, it defaults to 'A'. If the flags are not given, they default to C IN and C, C and C according to configuration (which is by default off on all three). +response, it will be returned in a L object. Regarding defaults for optional arguments "type" and "flags", +see L. =item query_persistent($name[, $type[, $flags]]) diff --git a/share/fr.po b/share/fr.po index b28d9c4c0..2121cc2ec 100644 --- a/share/fr.po +++ b/share/fr.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-12-09 16:55+0100\n" +"POT-Creation-Date: 2026-06-16 11:19+0200\n" "PO-Revision-Date: 2025-12-09 17:08+0100\n" "Last-Translator: thomas.green@afnic.fr\n" "Language-Team: Zonemaster Team\n" @@ -1812,15 +1812,6 @@ msgstr "" "Plusieurs enregistrements NSEC alors qu'un seul est attendu. Information " "retournée par les serveurs de noms \"{ns_list}\"." -#. DNSSEC:DS10_ERR_MULT_NSEC3PARAM -#, perl-brace-format -msgid "" -"Multiple NSEC3PARAM records when one is expected. Fetched from name servers " -"\"{ns_list}\"." -msgstr "" -"Plusieurs enregistrements NSEC3PARAM alors qu'un seul est attendu. " -"Information retournée par les serveurs de noms \"{ns_list}\"." - #. DNSSEC:DS10_EXPECTED_NSEC_NSEC3_MISSING #, perl-brace-format msgid "" @@ -2121,6 +2112,18 @@ msgstr "" "l'enregistrement NSEC ne peut pas être vérifié. Information retournée par " "les serveurs de noms \"{ns_list}\"." +#. DNSSEC:DS10_NONSTANDARD_NSEC_RESPONSE +#, perl-brace-format +msgid "" +"The following name servers give a non-standard response to the NSEC query " +"(NSEC RR in authority section instead of answer section). Fetched from name " +"servers \"{ns_list}\"." +msgstr "" +"Les serveurs de noms suivants renvoient une réponse non standard à la " +"requête NSEC (enregistrement NSEC se trouvant dans la section autorité au " +"lieu de la section réponse). Information retournée par les serveurs de noms " +"\"{ns_list}\"." + #. DNSSEC:DS10_SERVER_NO_DNSSEC #, perl-brace-format msgid "" diff --git a/share/profile.json b/share/profile.json index 05396e1f6..ebf832271 100644 --- a/share/profile.json +++ b/share/profile.json @@ -14,12 +14,9 @@ "resolver" : { "defaults" : { "debug" : false, - "igntc" : false, "fallback" : true, - "recurse" : false, "retrans" : 3, "retry" : 2, - "usevc" : false, "timeout": 5 } }, @@ -283,7 +280,6 @@ "DS10_ALGO_NOT_SUPPORTED_BY_ZM" : "NOTICE", "DS10_ERR_MULT_NSEC" : "ERROR", "DS10_ERR_MULT_NSEC3" : "ERROR", - "DS10_ERR_MULT_NSEC3PARAM" : "ERROR", "DS10_EXPECTED_NSEC_NSEC3_MISSING" : "ERROR", "DS10_HAS_NSEC" : "INFO", "DS10_HAS_NSEC3" : "INFO", @@ -291,10 +287,11 @@ "DS10_INCONSISTENT_NSEC3" : "ERROR", "DS10_INCONSISTENT_NSEC_NSEC3" : "ERROR", "DS10_MIXED_NSEC_NSEC3" : "ERROR", + "DS10_NONSTANDARD_NSEC_RESPONSE" : "NOTICE", "DS10_NSEC3PARAM_GIVES_ERR_ANSWER" : "ERROR", "DS10_NSEC3PARAM_MISMATCHES_APEX" : "ERROR", "DS10_NSEC3PARAM_QUERY_RESPONSE_ERR" : "ERROR", - "DS10_NSEC3_ERR_TYPE_LIST" : "ERROR", + "DS10_NSEC3_ERR_TYPE_LIST" : "NOTICE", "DS10_NSEC3_MISMATCHES_APEX" : "ERROR", "DS10_NSEC3_MISSING_SIGNATURE" : "ERROR", "DS10_NSEC3_NODATA_MISSING_SOA" : "ERROR", @@ -304,7 +301,7 @@ "DS10_NSEC3_RRSIG_NOT_YET_VALID" : "ERROR", "DS10_NSEC3_RRSIG_NO_DNSKEY" : "WARNING", "DS10_NSEC3_RRSIG_VERIFY_ERROR" : "ERROR", - "DS10_NSEC_ERR_TYPE_LIST" : "ERROR", + "DS10_NSEC_ERR_TYPE_LIST" : "NOTICE", "DS10_NSEC_GIVES_ERR_ANSWER" : "ERROR", "DS10_NSEC_MISMATCHES_APEX" : "ERROR", "DS10_NSEC_MISSING_SIGNATURE" : "ERROR", diff --git a/share/profile.yaml b/share/profile.yaml index 29749b7f7..8ed2e59ee 100644 --- a/share/profile.yaml +++ b/share/profile.yaml @@ -14,12 +14,9 @@ resolver: defaults: debug: false fallback: true - igntc: false - recurse: false retrans: 3 retry: 2 timeout: 5 - usevc: false test_cases: - address01 - address02 @@ -348,7 +345,6 @@ test_levels: DS10_ALGO_NOT_SUPPORTED_BY_ZM: NOTICE DS10_ERR_MULT_NSEC: ERROR DS10_ERR_MULT_NSEC3: ERROR - DS10_ERR_MULT_NSEC3PARAM: ERROR DS10_EXPECTED_NSEC_NSEC3_MISSING: ERROR DS10_HAS_NSEC: INFO DS10_HAS_NSEC3: INFO @@ -356,10 +352,11 @@ test_levels: DS10_INCONSISTENT_NSEC3: ERROR DS10_INCONSISTENT_NSEC_NSEC3: ERROR DS10_MIXED_NSEC_NSEC3: ERROR + DS10_NONSTANDARD_NSEC_RESPONSE: NOTICE DS10_NSEC3PARAM_GIVES_ERR_ANSWER: ERROR DS10_NSEC3PARAM_MISMATCHES_APEX: ERROR DS10_NSEC3PARAM_QUERY_RESPONSE_ERR: ERROR - DS10_NSEC3_ERR_TYPE_LIST: ERROR + DS10_NSEC3_ERR_TYPE_LIST: NOTICE DS10_NSEC3_MISMATCHES_APEX: ERROR DS10_NSEC3_MISSING_SIGNATURE: ERROR DS10_NSEC3_NODATA_MISSING_SOA: ERROR @@ -369,7 +366,7 @@ test_levels: DS10_NSEC3_RRSIG_NOT_YET_VALID: ERROR DS10_NSEC3_RRSIG_NO_DNSKEY: WARNING DS10_NSEC3_RRSIG_VERIFY_ERROR: ERROR - DS10_NSEC_ERR_TYPE_LIST: ERROR + DS10_NSEC_ERR_TYPE_LIST: NOTICE DS10_NSEC_GIVES_ERR_ANSWER: ERROR DS10_NSEC_MISMATCHES_APEX: ERROR DS10_NSEC_MISSING_SIGNATURE: ERROR diff --git a/share/sv.po b/share/sv.po index 22680712e..8230de4b6 100644 --- a/share/sv.po +++ b/share/sv.po @@ -2,9 +2,8 @@ msgid "" msgstr "" "Project-Id-Version: 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-04 15:43+0000\n" -"PO-Revision-Date: 2026-03-02 16:56+0000\n" -"Last-Translator: mattias.paivarinta@iis.se\n" +"POT-Creation-Date: 2026-06-12 20:11+0000\n" +"PO-Revision-Date: 2026-06-12 20:10+0000\n" "Language-Team: Zonemaster Team\n" "Language: sv\n" "MIME-Version: 1.0\n" @@ -1697,15 +1696,6 @@ msgstr "" "Flera NSEC-poster hittades där endast en förväntades. Hämtad från " "namnservrarna \"{ns_list}\"." -#. DNSSEC:DS10_ERR_MULT_NSEC3PARAM -#, perl-brace-format -msgid "" -"Multiple NSEC3PARAM records when one is expected. Fetched from name servers " -"\"{ns_list}\"." -msgstr "" -"Flera NSEC3PARAM-poster hittades där endast en förväntades. Hämtad från " -"namnservrarna \"{ns_list}\"." - #. DNSSEC:DS10_EXPECTED_NSEC_NSEC3_MISSING #, perl-brace-format msgid "" @@ -1989,6 +1979,17 @@ msgstr "" "RRSIG-signaturen med \"key tag\" {keytag} för NSEC-posten kan inte " "verifieras. Hämtad från namnservrarna med IP-adresserna \"{ns_list}\"." +#. DNSSEC:DS10_NONSTANDARD_NSEC_RESPONSE +#, perl-brace-format +msgid "" +"The following name servers give a non-standard response to the NSEC query " +"(NSEC RR in authority section instead of answer section). Fetched from name " +"servers \"{ns_list}\"." +msgstr "" +"Följande namnservrar ger ett svar på NSEC-förfrågan som inte följer " +"standarden (NSEC-post i \"authority\" istället för i \"answer\"). Hämtad " +"från namnservrarna \"{ns_list}\"." + #. DNSSEC:DS10_SERVER_NO_DNSSEC #, perl-brace-format msgid "" diff --git a/t/Test-address.data b/t/Test-address.data index 15a1289f5..8d00ec94a 100644 Binary files a/t/Test-address.data and b/t/Test-address.data differ diff --git a/t/Test-address01.data b/t/Test-address01.data index a8cf4b234..387993ebf 100644 Binary files a/t/Test-address01.data and b/t/Test-address01.data differ diff --git a/t/Test-address03.data b/t/Test-address03.data index 702dbaf1c..ceb1aed66 100644 Binary files a/t/Test-address03.data and b/t/Test-address03.data differ diff --git a/t/Test-basic.data b/t/Test-basic.data index 3b26758b7..f4017a2ea 100644 Binary files a/t/Test-basic.data and b/t/Test-basic.data differ diff --git a/t/Test-basic01.data b/t/Test-basic01.data index 9d9baa73c..59069398d 100644 Binary files a/t/Test-basic01.data and b/t/Test-basic01.data differ diff --git a/t/Test-basic02.data b/t/Test-basic02.data index a8d043360..0dc1557ac 100644 Binary files a/t/Test-basic02.data and b/t/Test-basic02.data differ diff --git a/t/Test-connectivity.data b/t/Test-connectivity.data index 07d69f921..244207d71 100644 Binary files a/t/Test-connectivity.data and b/t/Test-connectivity.data differ diff --git a/t/Test-connectivity03.data b/t/Test-connectivity03.data index b4dde0bc7..2ca7fcd58 100644 Binary files a/t/Test-connectivity03.data and b/t/Test-connectivity03.data differ diff --git a/t/Test-connectivity03.t b/t/Test-connectivity03.t index 74661ab5f..76802b845 100644 --- a/t/Test-connectivity03.t +++ b/t/Test-connectivity03.t @@ -48,7 +48,7 @@ $profile_test = Zonemaster::Engine::Profile->from_json( $json ); Zonemaster::Engine::Profile->effective->merge( $profile_test ); ### -my $zone = Zonemaster::Engine->zone( q{001.tf} ); +my $zone = Zonemaster::Engine->zone( q{001.re} ); zone_gives( $testcase, $zone, [qw{IPV4_ONE_ASN IPV6_ONE_ASN}] ); zone_gives_not( $testcase, $zone, [qw{EMPTY_ASN_SET ERROR_ASN_DATABASE IPV4_DIFFERENT_ASN IPV4_SAME_ASN IPV6_DIFFERENT_ASN IPV6_SAME_ASN}] ); diff --git a/t/Test-connectivity04.data b/t/Test-connectivity04.data index f321a965a..e0b2bddec 100644 Binary files a/t/Test-connectivity04.data and b/t/Test-connectivity04.data differ diff --git a/t/Test-consistency.data b/t/Test-consistency.data index 746efd4da..f31379597 100644 Binary files a/t/Test-consistency.data and b/t/Test-consistency.data differ diff --git a/t/Test-consistency05.data b/t/Test-consistency05.data index f056e9265..cbce8f0e4 100644 Binary files a/t/Test-consistency05.data and b/t/Test-consistency05.data differ diff --git a/t/Test-consistency06.data b/t/Test-consistency06.data index 508afdb18..21d72d32a 100644 Binary files a/t/Test-consistency06.data and b/t/Test-consistency06.data differ diff --git a/t/Test-delegation.data b/t/Test-delegation.data index a48ec53d7..137e86a9f 100644 Binary files a/t/Test-delegation.data and b/t/Test-delegation.data differ diff --git a/t/Test-delegation.t b/t/Test-delegation.t index 93a876b5a..165302ced 100644 --- a/t/Test-delegation.t +++ b/t/Test-delegation.t @@ -28,7 +28,7 @@ my $iis = Zonemaster::Engine->zone( q{iis.se} ); %res = map { $_->tag => $_ } Zonemaster::Engine::Test::Delegation->all( $iis ); ok( $res{NAMES_MATCH}, q{NAMES_MATCH} ); -%res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{crystone.se} ); +%res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{krtgroupe.fr} ); ok( $res{EXTRA_NAME_PARENT}, q{EXTRA_NAME_PARENT} ); ok( $res{EXTRA_NAME_CHILD}, q{EXTRA_NAME_CHILD} ); ok( $res{TOTAL_NAME_MISMATCH}, q{TOTAL_NAME_MISMATCH} ); @@ -36,18 +36,13 @@ ok( $res{NO_NS_CNAME}, q{NO_NS_CNAME} ); ok( $res{SOA_EXISTS}, q{SOA_EXISTS} ); ok( $res{ARE_AUTHORITATIVE}, q{ARE_AUTHORITATIVE} ); -%res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{woli.se} ); +%res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{critere.fr} ); ok( $res{SOA_NOT_EXISTS}, q{SOA_NOT_EXISTS} ); -TODO: { - local $TODO = "Need to find domain name with that error"; +%res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{supaero.fr} ); +ok( $res{IS_NOT_AUTHORITATIVE}, q{IS_NOT_AUTHORITATIVE} ); - %res = map { $_->tag => 1 } Zonemaster::Engine->test_module( q{delegation}, q{elsine.se} ); - ok( $res{IS_NOT_AUTHORITATIVE}, q{IS_NOT_AUTHORITATIVE} ); - - ok( $res{NS_IS_CNAME}, q{NS_IS_CNAME} ); - -} +ok( $res{NS_IS_CNAME}, q{NS_IS_CNAME} ); if ( $ENV{ZONEMASTER_RECORD} ) { Zonemaster::Engine::Nameserver->save( $datafile ); diff --git a/t/Test-delegation01.data b/t/Test-delegation01.data index f3487c83e..c7447f83f 100644 Binary files a/t/Test-delegation01.data and b/t/Test-delegation01.data differ diff --git a/t/Test-delegation02.data b/t/Test-delegation02.data index 829eee156..866331791 100644 Binary files a/t/Test-delegation02.data and b/t/Test-delegation02.data differ diff --git a/t/Test-delegation03.data b/t/Test-delegation03.data index e80e06ae4..9cc7aeb01 100644 Binary files a/t/Test-delegation03.data and b/t/Test-delegation03.data differ diff --git a/t/Test-dnssec-more.data b/t/Test-dnssec-more.data index 4c4ccd94b..1a585537a 100644 Binary files a/t/Test-dnssec-more.data and b/t/Test-dnssec-more.data differ diff --git a/t/Test-dnssec.data b/t/Test-dnssec.data index c5dd531be..64e543ec2 100644 Binary files a/t/Test-dnssec.data and b/t/Test-dnssec.data differ diff --git a/t/Test-dnssec01.data b/t/Test-dnssec01.data index 23bdedb8c..971cd9bf7 100644 Binary files a/t/Test-dnssec01.data and b/t/Test-dnssec01.data differ diff --git a/t/Test-dnssec03.data b/t/Test-dnssec03.data index c81a7efe3..7e1dbe710 100644 Binary files a/t/Test-dnssec03.data and b/t/Test-dnssec03.data differ diff --git a/t/Test-dnssec05.data b/t/Test-dnssec05.data index 599447df1..b40fc0ad9 100644 Binary files a/t/Test-dnssec05.data and b/t/Test-dnssec05.data differ diff --git a/t/Test-dnssec07.data b/t/Test-dnssec07.data index 8a598288b..39997d5e5 100644 Binary files a/t/Test-dnssec07.data and b/t/Test-dnssec07.data differ diff --git a/t/Test-dnssec10.data b/t/Test-dnssec10.data index aef45c1de..0938aa005 100644 Binary files a/t/Test-dnssec10.data and b/t/Test-dnssec10.data differ diff --git a/t/Test-dnssec10.t b/t/Test-dnssec10.t index 434e9edfe..fe6f5a026 100644 --- a/t/Test-dnssec10.t +++ b/t/Test-dnssec10.t @@ -20,7 +20,6 @@ my @all_tags = qw( DS10_ALGO_NOT_SUPPORTED_BY_ZM DS10_ERR_MULT_NSEC DS10_ERR_MULT_NSEC3 - DS10_ERR_MULT_NSEC3PARAM DS10_EXPECTED_NSEC_NSEC3_MISSING DS10_HAS_NSEC DS10_HAS_NSEC3 @@ -28,6 +27,7 @@ my @all_tags = qw( DS10_INCONSISTENT_NSEC3 DS10_INCONSISTENT_NSEC_NSEC3 DS10_MIXED_NSEC_NSEC3 + DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC3PARAM_GIVES_ERR_ANSWER DS10_NSEC3PARAM_MISMATCHES_APEX DS10_NSEC3PARAM_QUERY_RESPONSE_ERR @@ -176,10 +176,10 @@ my %subtests = ( [], [], ], - 'ERR-MULT-NSEC3PARAM-1' => [ + 'MULT-NSEC3PARAM-1' => [ 1, - q(err-mult-nsec3param-1.dnssec10.xa), - [ qw( DS10_ERR_MULT_NSEC3PARAM DS10_HAS_NSEC3 ) ], + q(mult-nsec3param-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC3 ) ], undef, [], [], @@ -496,6 +496,78 @@ my %subtests = ( [], [], ], + 'NS-NSEC-RESP-1' => [ + 1, + q(ns-nsec-resp-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-MISS-SOA-1' => [ + 1, + q(ns-nsec-resp-miss-soa-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_NODATA_MISSING_SOA ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-WRONG-SOA-1' => [ + 1, + q(ns-nsec-resp-wrong-soa-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_NODATA_WRONG_SOA ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-MULT-NSEC-1' => [ + 1, + q(ns-nsec-resp-mult-nsec-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_ERR_MULT_NSEC ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-NSEC-MM-APEX-1' => [ + 1, + q(ns-nsec-resp-nsec-mm-apex-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_MISMATCHES_APEX ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-ERR-TP-1' => [ + 1, + q(ns-nsec-resp-err-tp-1.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_ERR_TYPE_LIST ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-ERR-TP-2' => [ + 1, + q(ns-nsec-resp-err-tp-2.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_ERR_TYPE_LIST ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-ERR-TP-3' => [ + 1, + q(ns-nsec-resp-err-tp-3.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_ERR_TYPE_LIST ) ], + undef, + [], + [], + ], + 'NS-NSEC-RESP-ERR-TP-4' => [ + 1, + q(ns-nsec-resp-err-tp-4.dnssec10.xa), + [ qw( DS10_HAS_NSEC DS10_NONSTANDARD_NSEC_RESPONSE DS10_NSEC_ERR_TYPE_LIST ) ], + undef, + [], + [], + ], 'SERVER-NO-DNSSEC-1' => [ 1, q(server-no-dnssec-1.dnssec10.xa), diff --git a/t/Test-dnssec16.data b/t/Test-dnssec16.data index be9878a09..808c9642a 100644 Binary files a/t/Test-dnssec16.data and b/t/Test-dnssec16.data differ diff --git a/t/Test-nameserver.data b/t/Test-nameserver.data index 843537e53..89ea61100 100644 Binary files a/t/Test-nameserver.data and b/t/Test-nameserver.data differ diff --git a/t/Test-nameserver01-A.data b/t/Test-nameserver01-A.data index f514ef794..de7f5cbe0 100644 Binary files a/t/Test-nameserver01-A.data and b/t/Test-nameserver01-A.data differ diff --git a/t/Test-nameserver01-B.data b/t/Test-nameserver01-B.data index 9306ae18a..2f53902d6 100644 Binary files a/t/Test-nameserver01-B.data and b/t/Test-nameserver01-B.data differ diff --git a/t/Test-nameserver01-C.data b/t/Test-nameserver01-C.data index afb7c7082..c0d737e12 100644 Binary files a/t/Test-nameserver01-C.data and b/t/Test-nameserver01-C.data differ diff --git a/t/Test-nameserver01-D.data b/t/Test-nameserver01-D.data index ccd41da36..53a987b9c 100644 Binary files a/t/Test-nameserver01-D.data and b/t/Test-nameserver01-D.data differ diff --git a/t/Test-nameserver15.data b/t/Test-nameserver15.data index 8b8b9d8ff..6accf7053 100644 Binary files a/t/Test-nameserver15.data and b/t/Test-nameserver15.data differ diff --git a/t/Test-syntax.data b/t/Test-syntax.data index 613d435aa..6ac2bb260 100644 Binary files a/t/Test-syntax.data and b/t/Test-syntax.data differ diff --git a/t/Test-syntax06-A.data b/t/Test-syntax06-A.data index ccf3f5253..81b7e39af 100644 Binary files a/t/Test-syntax06-A.data and b/t/Test-syntax06-A.data differ diff --git a/t/Test-syntax06-B.data b/t/Test-syntax06-B.data index 216e4c1d2..f6904478f 100644 Binary files a/t/Test-syntax06-B.data and b/t/Test-syntax06-B.data differ diff --git a/t/Test-syntax06-C.data b/t/Test-syntax06-C.data index c75f2d817..c43d1bc6d 100644 Binary files a/t/Test-syntax06-C.data and b/t/Test-syntax06-C.data differ diff --git a/t/Test-syntax06-D.data b/t/Test-syntax06-D.data index d06e5d0da..a6454d1d7 100644 Binary files a/t/Test-syntax06-D.data and b/t/Test-syntax06-D.data differ diff --git a/t/Test-syntax06-E.data b/t/Test-syntax06-E.data index d8dfe6c81..b5a3a7cd5 100644 Binary files a/t/Test-syntax06-E.data and b/t/Test-syntax06-E.data differ diff --git a/t/Test-syntax06-F.data b/t/Test-syntax06-F.data index c5ba83bbf..590ae3a67 100644 Binary files a/t/Test-syntax06-F.data and b/t/Test-syntax06-F.data differ diff --git a/t/Test-syntax06-G.data b/t/Test-syntax06-G.data index 7e4b33aaf..8c7e26685 100644 Binary files a/t/Test-syntax06-G.data and b/t/Test-syntax06-G.data differ diff --git a/t/Test-syntax06-I.data b/t/Test-syntax06-I.data index 6fabfea76..a9a90033f 100644 Binary files a/t/Test-syntax06-I.data and b/t/Test-syntax06-I.data differ diff --git a/t/Test-syntax06-J.data b/t/Test-syntax06-J.data index b1f06ecad..46c281c3b 100644 Binary files a/t/Test-syntax06-J.data and b/t/Test-syntax06-J.data differ diff --git a/t/Test-syntax06-K.data b/t/Test-syntax06-K.data index be34d7b8b..d1df53cfe 100644 Binary files a/t/Test-syntax06-K.data and b/t/Test-syntax06-K.data differ diff --git a/t/Test-syntax06-L.data b/t/Test-syntax06-L.data index 155bb1db6..b6defcc78 100644 Binary files a/t/Test-syntax06-L.data and b/t/Test-syntax06-L.data differ diff --git a/t/Test-zone.data b/t/Test-zone.data index a65c9009c..3f5730730 100644 Binary files a/t/Test-zone.data and b/t/Test-zone.data differ diff --git a/t/Test-zone01-A.data b/t/Test-zone01-A.data index ea573183c..9bf35c77b 100644 Binary files a/t/Test-zone01-A.data and b/t/Test-zone01-A.data differ diff --git a/t/Test-zone01-A.t b/t/Test-zone01-A.t index 888e9c8cd..7686da14f 100644 --- a/t/Test-zone01-A.t +++ b/t/Test-zone01-A.t @@ -15,9 +15,10 @@ if ( not $ENV{ZONEMASTER_RECORD} ) { } Zonemaster::Engine->add_fake_delegation( - 'xa' => { - 'ibdns01.labs.prive.nic.fr' => ['10.1.72.23'], - 'ibdns01-24.labs.prive.nic.fr' => ['10.1.72.24'], + 'mname-not-master.zone01.xa' => { + 'ns1.mname-not-master.zone01.xa' => ['10.1.72.23'], + 'ns2.mname-not-master.zone01.xa' => ['10.1.72.24'], + 'ns3.mname-not-master.zone01.xa' => ['10.1.72.25'], }, fill_in_empty_oob_glue => 0, ); @@ -42,4 +43,4 @@ if ( $ENV{ZONEMASTER_RECORD} ) { Zonemaster::Engine::Nameserver->save( $datafile ); } -done_testing; \ No newline at end of file +done_testing; diff --git a/t/Test-zone01-B.data b/t/Test-zone01-B.data index 9ca68e36f..67652392b 100644 Binary files a/t/Test-zone01-B.data and b/t/Test-zone01-B.data differ diff --git a/t/Test-zone01-B.t b/t/Test-zone01-B.t index 5c7dfdc60..55067d0ac 100644 --- a/t/Test-zone01-B.t +++ b/t/Test-zone01-B.t @@ -15,9 +15,10 @@ if ( not $ENV{ZONEMASTER_RECORD} ) { } Zonemaster::Engine->add_fake_delegation( - 'xa' => { - 'ibdns01.labs.prive.nic.fr' => ['10.1.72.23'], - 'ibdns01-24.labs.prive.nic.fr' => ['10.1.72.24'], + 'multi-mname-not-master.zone01.xa' => { + 'ns1.multi-mname-not-master.zone01.xa' => ['10.1.72.23'], + 'ns2.multi-mname-not-master.zone01.xa' => ['10.1.72.24'], + 'ns3.multi-mname-not-master.zone01.xa' => ['10.1.72.25'], }, fill_in_empty_oob_glue => 0, ); @@ -42,4 +43,4 @@ if ( $ENV{ZONEMASTER_RECORD} ) { Zonemaster::Engine::Nameserver->save( $datafile ); } -done_testing; \ No newline at end of file +done_testing; diff --git a/t/Test-zone09-1.data b/t/Test-zone09-1.data index 404464a42..aa5d968ad 100644 Binary files a/t/Test-zone09-1.data and b/t/Test-zone09-1.data differ diff --git a/t/Test-zone09.data b/t/Test-zone09.data index ead5f2c8f..95eb7dc9f 100644 Binary files a/t/Test-zone09.data and b/t/Test-zone09.data differ diff --git a/t/Test-zone11-1.data b/t/Test-zone11-1.data index 5711db270..aa6708dc2 100644 Binary files a/t/Test-zone11-1.data and b/t/Test-zone11-1.data differ diff --git a/t/Test-zone11-2.data b/t/Test-zone11-2.data index cfd9078b7..aefc876d0 100644 Binary files a/t/Test-zone11-2.data and b/t/Test-zone11-2.data differ diff --git a/t/Test-zone11-3.data b/t/Test-zone11-3.data index 6b6b49f47..4c66ab1c3 100644 Binary files a/t/Test-zone11-3.data and b/t/Test-zone11-3.data differ diff --git a/t/Test-zone11.data b/t/Test-zone11.data index 56fa50ac1..c7830435d 100644 Binary files a/t/Test-zone11.data and b/t/Test-zone11.data differ diff --git a/t/Test.data b/t/Test.data index 1efc095b2..0bd8b92b0 100644 Binary files a/t/Test.data and b/t/Test.data differ diff --git a/t/asn.data b/t/asn.data index c72c3ba55..7149e06bb 100644 Binary files a/t/asn.data and b/t/asn.data differ diff --git a/t/asn.t b/t/asn.t index d3b68abba..c47a43916 100644 --- a/t/asn.t +++ b/t/asn.t @@ -21,9 +21,9 @@ my ( $asn2, $prefix2 ) = Zonemaster::Engine::ASNLookup->get_with_prefix( '91.226 is $asn2->[0], 197564, '91.226.36.46 is in AS197564'; is $prefix2->prefix, '91.226.36.0/23', '91.226.36.46 is in 91.226.36.0/24'; -my @asn3 = Zonemaster::Engine::ASNLookup->get( '2001:503:ba3e::2:30' ); +my @asn3 = Zonemaster::Engine::ASNLookup->get( '2001:7fd::1' ); is( scalar( @asn3 ), 1, 'Only one result' ); -ok $asn3[0] >= 390000, '2001:503:ba3e::2:30 is in AS' . $asn3[0]; +is $asn3[0], 25152, '2001:7fd::1 is in AS 25152'; my ( $asn4, $prefix4 ) = Zonemaster::Engine::ASNLookup->get_with_prefix( '192.168.0.1' ); ok( scalar @{$asn4} == 0, '192.168.0.1 (RFC1918 address) is in no AS' ); diff --git a/t/logger.t b/t/logger.t index f780bf892..61f420ca1 100644 --- a/t/logger.t +++ b/t/logger.t @@ -1,13 +1,15 @@ -use Test::More; -use Test::Fatal; use File::Slurp; +use JSON::PP; +use Test::Fatal; +use Test::More; BEGIN { + use_ok( 'Zonemaster::Engine' ); + use_ok( 'Zonemaster::Engine::Util' ); use_ok( 'Zonemaster::Engine::Logger' ); use_ok( 'Zonemaster::Engine::Logger::Entry' ); use_ok( 'Zonemaster::Engine::Exception' ); } -use Zonemaster::Engine::Util; my $log = Zonemaster::Engine->logger; @@ -35,6 +37,35 @@ ok( scalar( @{ Zonemaster::Engine->logger->entries } ) >= 2, 'expected number of like( "$entry", qr/System:Unspecified:TEST an=argument/, 'stringification overload' ); +$entry = info( 'TEST2', { an => 'argument', domain => Zonemaster::Engine::Util::name( 'logger.test.example' ) } ); + +# FIXME: Ideally, we really should instead have domain=logger.test.example +# without the double quotes around the domain name. +# +# Providing the domain name as a regular string gives us no double quotes +# around the domain name. Providing it directly as a +# Zonemaster::Engine::DNSName does give us double quotes, however. That’s +# because blessed objects get turned into their JSON representations, while +# regular strings are interpolated as they are. The TO_JSON method for DNSName +# objects returns a simple string, which is then converted into JSON in the +# usual double-quoted notation. +# +# It’s an annoying inconsistency, but any attempt at a fix might cause +# repercussions when translating a log entry to a human language, a string, or +# JSON. For now, we expect the broken behavior, but that should be fixed +# someday. +is( + "$entry", + 'System:Unspecified:TEST2 an=argument; domain="logger.test.example"', + 'stringification overload works with DNS names' ); +is_deeply( + (decode_json $log->json)->[-1]->{args}, + { + an => 'argument', + domain => 'logger.test.example' + }, + 'entry arguments serialize to JSON' ); + is( $entry->level, 'DEBUG', 'right level' ); my $example = Zonemaster::Engine::Logger::Entry->new({ module => 'Basic', tag => 'B02_NS_BROKEN', testcase => 'Basic02' } ); is( $example->level, 'ERROR', 'expected level' ); diff --git a/t/methodsv2.data b/t/methodsv2.data index b3dcac6ce..69846ebb3 100644 Binary files a/t/methodsv2.data and b/t/methodsv2.data differ diff --git a/t/nameserver-axfr.data b/t/nameserver-axfr.data index a9087855e..61986052c 100644 Binary files a/t/nameserver-axfr.data and b/t/nameserver-axfr.data differ diff --git a/t/nameserver-axfr.t b/t/nameserver-axfr.t index 1ef8602d0..737d995ba 100644 --- a/t/nameserver-axfr.t +++ b/t/nameserver-axfr.t @@ -4,17 +4,16 @@ use warnings; use Test::More; use Test::Fatal; +use Zonemaster::Engine; use Zonemaster::Engine::Util; -use Zonemaster::Engine::Nameserver; use Zonemaster::LDNS; -use Sub::Override; my $datafile = 't/nameserver-axfr.data'; -my %saved_axfr; -my $override = Sub::Override->new(); - - -setup( $datafile ); +if ( not $ENV{ZONEMASTER_RECORD} ) { + die "Stored data file missing" if not -r $datafile; + Zonemaster::Engine::Nameserver->restore( $datafile ); + Zonemaster::Engine::Profile->effective->set( q{no_network}, 1 ); +} # This should be a successful AXFR my $ns = Zonemaster::Engine::Nameserver->new( { name => 'kennedy.faerywicca.se', address => '46.21.106.227' } ); @@ -40,95 +39,7 @@ like( ); is( $counter, 0, 'No records seen' ); -finish( $datafile ); - -done_testing; - -### -### Functions to record and replay AXFRs. -### - -sub setup { - my ( $datafile ) = @_; - if ( not $ENV{ZONEMASTER_RECORD} ) { - - # Replay - die "Stored data file missing" if not -r $datafile; - open my $fh, '<', $datafile or die "Failed to open $datafile for reading: $!\n"; - while ( my $line = $fh->getline ) { - my ( $domain, $type, $str ) = split( /\t/, $line, 3 ); - if ( $type eq 'RR' ) { - my $rr = eval { Zonemaster::LDNS::RR->new( $str ) }; - if ( $rr ) { - push @{ $saved_axfr{$domain} }, $rr; - } - else { - warn "Failed to parse: $str\n"; - } - } - elsif ( $type eq 'EXCEPTION' ) { - $saved_axfr{$domain} = $str; - } - } - Zonemaster::Engine::Profile->effective->set( q{no_network}, 1 ); - - $override->override( - 'Zonemaster::Engine::Nameserver::axfr', - sub { - my ( $self, $domain, $callback, $class ) = @_; - if ( exists $saved_axfr{$domain} ) { - if ( ref( $saved_axfr{$domain} ) ) { - while ( my $rr = pop @{ $saved_axfr{$domain} } ) { - $callback->( $rr ); - } - } - else { - die $saved_axfr{$domain}; - } - } - else { - die "AXFR Request for domain that has not been saved."; - } - } - ); - } ## end if ( not $ENV{ZONEMASTER_RECORD...}) - else { - # Record - $override->wrap( - 'Zonemaster::Engine::Nameserver::axfr', - sub { - my ( $old_axfr, $self, $domain, $callback, $class ) = @_; - my @rrs; - my $new_cb = sub { - push @rrs, $_[0]; - $callback->( $_[0] ); - }; - my $result = eval { $old_axfr->( $self, $domain, $new_cb, $class ) }; - if ( $@ ) { - $saved_axfr{$domain} = "$@"; - die $@; - } - else { - $saved_axfr{$domain} = \@rrs; - } - } - ); - } ## end else [ if ( not $ENV{ZONEMASTER_RECORD...})] -} ## end sub setup - -sub finish { - my ( $datafile ) = @_; - if ( $ENV{ZONEMASTER_RECORD} ) { - open my $fh, '>', $datafile or die "Failed to open $datafile for writing: $!\n"; - while ( my ( $domain, $aref ) = each %saved_axfr ) { - if ( ref $aref ) { - say $fh $domain . "\tRR\t" . $_->string for @$aref; - } - else { - chomp( $aref ); - say $fh $domain . "\tEXCEPTION\t$aref"; - } - } - close $fh; - } +if ( $ENV{ZONEMASTER_RECORD} ) { + Zonemaster::Engine::Nameserver->save( $datafile ); } +done_testing; diff --git a/t/nameserver.data b/t/nameserver.data index 5078068da..39c36ab36 100644 Binary files a/t/nameserver.data and b/t/nameserver.data differ diff --git a/t/nameserver.t b/t/nameserver.t index 59b03e533..2c88f6014 100644 --- a/t/nameserver.t +++ b/t/nameserver.t @@ -125,176 +125,61 @@ is( $dsrr->hexdigest, 'deadbeef', 'Expected digest data' ); subtest 'dnssec, edns_size and edns_details{do, size} flags behavior for queries' => sub { my $ns = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'd.nic.fr', address => '194.0.9.1' } ] ); - my $p = $ns->query( 'fr', 'SOA' ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 0, 'edns_size flag is unset' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( (!$p->has_edns and !$p->do), 'non-EDNS response received on query with all default parameters' ); - - $p = $ns->query( 'fr', 'SOA', { "dnssec" => 0 } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 0, 'edns_size flag is unset' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( (!$p->has_edns and !$p->do), 'non-EDNS response received on query with dnssec unset' ); - - # Note that the following tests also implicitly test that flags are correctly re-evaluated between - # each consecutive queries. - - $p = $ns->query( 'a.fr', 'SOA', { "dnssec" => 1 } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( $ns->dns->dnssec, 'dnssec flag is set' ); - is( $ns->dns->edns_size, $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT, 'edns_size uses default DNSSEC query value' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and $p->do), 'DNSSEC response received on query with dnssec set' ); - - $p = $ns->query( 'b.fr', 'SOA', { "edns_size" => 1000 } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 1000, 'edns_size uses given value' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with edns_size set' ); - - $p = $ns->query( 'c.fr', 'SOA', { "dnssec" => 0, "edns_size" => 1001 } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 1001, 'edns_size uses given value instead of default for non-DNSSEC EDNS queries' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with dnssec unset and edns_size set' ); - - $p = $ns->query( 'd.fr', 'SOA', { "dnssec" => 1, "edns_size" => 1002 } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( $ns->dns->dnssec, 'dnssec flag is set' ); - is( $ns->dns->edns_size, 1002, 'edns_size uses given value instead of default for DNSSEC queries' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and $p->do), 'DNSSEC response received on query with dnssec set and edns_size set' ); - - $p = $ns->query( 'e.fr', 'SOA', { "edns_details" => {} } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, $EDNS_UDP_PAYLOAD_DEFAULT, 'edns_size uses default EDNS query value for non-DNSSEC EDNS queries' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with edns_details set' ); - - $p = $ns->query( 'f.fr', 'SOA', { "edns_details" => { "do" => 1 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( $ns->dns->dnssec, 'dnssec flag is also set via edns_details{do}' ); - is( $ns->dns->edns_size, $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT, 'edns_size also uses default DNSSEC query value when set with edns_details{do}' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and $p->do), 'DNSSEC response received on query with edns_details{do} set' ); - - $p = $ns->query( 'g.fr', 'SOA', { "edns_details" => { "size" => 900 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 900, 'edns_size also uses given value when set with edns_details{size}' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with edns_details{size} set' ); - - $p = $ns->query( 'h.fr', 'SOA', { "edns_details" => { "size" => 0 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 0, 'edns_size also uses given value when set with edns_details{size}' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query even with edns_details{size} set to 0' ); - - $p = $ns->query( 'i.fr', 'SOA', { "dnssec" => 1, "edns_details" => { "do" => 0 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'edns_details{do} takes precedence over dnssec for (un)setting the dnssec flag' ); - is( $ns->dns->edns_size, $EDNS_UDP_PAYLOAD_DEFAULT, 'edns_size uses default EDNS query value when dnssec flag is unset by edns_details{do}' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with dnssec unset by edns_details{do}' ); - - $p = $ns->query( 'j.fr', 'SOA', { "edns_size" => 1003, "edns_details" => { "size" => 901 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( !$ns->dns->dnssec, 'dnssec flag is unset' ); - is( $ns->dns->edns_size, 901, 'edns_details{size} takes precedence over edns_size for setting the edns_size flag' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and !$p->do), 'non-DNSSEC EDNS response received on query with edns_size and edns_details{size} set' ); - - $p = $ns->query( 'k.fr', 'SOA', { "dnssec" => 1, "edns_size" => 1004, "edns_details" => { "size" => 0 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( $ns->dns->dnssec, 'dnssec flag is set' ); - is( $ns->dns->edns_size, 0, 'edns_size is unset' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and $p->do), 'DNSSEC response received on query with dnssec set and even with edns_details{size} set to 0' ); - - $p = $ns->query( 'l.fr', 'SOA', { "dnssec" => 0, "edns_size" => 1005, "edns_details" => { "do" => 1, "size" => 0 } } ); - if ( $ENV{ZONEMASTER_RECORD} ) { - ok( $ns->dns->dnssec, 'dnssec flag is set' ); - is( $ns->dns->edns_size, 0, 'edns_size is unset' ); - } - else { - SKIP: { - skip "no live recording - dnssec and edns_size flags in query can't be checked", 2; - }; - } - ok( ($p->has_edns and $p->do), 'DNSSEC response received on query with dnssec set by edns_details{do} and even with edns_details{size} set to 0' ); + my $p = $ns->_make_query_packet( 'fr', 'SOA' ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 0, 'EDNS size flag is unset' ); + + $p = $ns->_make_query_packet( 'fr', 'SOA', { "dnssec" => 0 } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 0, 'EDNS size flag is unset' ); + + $p = $ns->_make_query_packet( 'a.fr', 'SOA', { "dnssec" => 1 } ); + ok( $p->do(), 'DNSSEC flag is set' ); + is( $p->edns_size(), $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT, 'EDNS size uses default DNSSEC query value' ); + + $p = $ns->_make_query_packet( 'b.fr', 'SOA', { "edns_size" => 1000 } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 1000, 'EDNS size uses given value' ); + + $p = $ns->_make_query_packet( 'c.fr', 'SOA', { "dnssec" => 0, "edns_size" => 1001 } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 1001, 'EDNS size uses given value instead of default for non-DNSSEC EDNS queries' ); + + $p = $ns->_make_query_packet( 'd.fr', 'SOA', { "dnssec" => 1, "edns_size" => 1002 } ); + ok( $p->do(), 'DNSSEC flag is set' ); + is( $p->edns_size(), 1002, 'EDNS size uses given value instead of default for DNSSEC queries' ); + + $p = $ns->_make_query_packet( 'e.fr', 'SOA', { "edns_details" => {} } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), $EDNS_UDP_PAYLOAD_DEFAULT, 'EDNS size uses default value for non-DNSSEC EDNS queries' ); + + $p = $ns->_make_query_packet( 'f.fr', 'SOA', { "edns_details" => { "do" => 1 } } ); + ok( $p->do(), 'DNSSEC flag is also set via edns_details{do}' ); + is( $p->edns_size(), $EDNS_UDP_PAYLOAD_DNSSEC_DEFAULT, 'EDNS size uses default DNSSEC query value when set with edns_details{do}' ); + + $p = $ns->_make_query_packet( 'g.fr', 'SOA', { "edns_details" => { "size" => 900 } } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 900, 'EDNS size also uses given value when set with edns_details{size}' ); + + $p = $ns->_make_query_packet( 'h.fr', 'SOA', { "edns_details" => { "size" => 0 } } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 0, 'EDNS size also uses given value when set with edns_details{size}' ); + + $p = $ns->_make_query_packet( 'i.fr', 'SOA', { "dnssec" => 1, "edns_details" => { "do" => 0 } } ); + ok( !$p->do(), 'edns_details{do} takes precedence over dnssec for (un)setting the DNSSEC flag' ); + is( $p->edns_size(), $EDNS_UDP_PAYLOAD_DEFAULT, 'EDNS size uses default EDNS query value when DNSSEC flag is unset by edns_details{do}' ); + + $p = $ns->_make_query_packet( 'j.fr', 'SOA', { "edns_size" => 1003, "edns_details" => { "size" => 901 } } ); + ok( !$p->do(), 'DNSSEC flag is unset' ); + is( $p->edns_size(), 901, 'edns_details{size} takes precedence over edns_size for setting the edns_size flag' ); + + $p = $ns->_make_query_packet( 'k.fr', 'SOA', { "dnssec" => 1, "edns_size" => 1004, "edns_details" => { "size" => 0 } } ); + ok( $p->do(), 'DNSSEC flag is set' ); + is( $p->edns_size(), 0, 'EDNS size is unset' ); + + $p = $ns->_make_query_packet( 'l.fr', 'SOA', { "dnssec" => 0, "edns_size" => 1005, "edns_details" => { "do" => 1, "size" => 0 } } ); + ok( $p->do(), 'DNSSEC flag is set' ); + is( $p->edns_size(), 0, 'EDNS size is unset' ); dies_ok { $p = $ns->query( 'fr', 'SOA', { "edns_size" => 65536 } ); } "dies when edns_size exceeds 65535"; dies_ok { $p = $ns->query( 'fr', 'SOA', { "edns_details" => { "size" => 65536 } } ); } "dies when edns_size (set with edns_details->size) exceeds 65535"; @@ -313,22 +198,61 @@ is($ns_test->dns->source, '::1', 'Source IPv6 address set.'); # We have to make a query to test the following message tags, so no_network must be false. Zonemaster::Engine::Profile->effective->set( q{no_network}, 0 ); +Zonemaster::Engine->logger->clear_history(); + # 192.0.2.17 is part of TEST-NET-1 IP address range (see RFC6890) and reserved # for documentation. my $fail_ns = Zonemaster::Engine::Nameserver->new( { name => 'fail', address => '192.0.2.17' } ); my $fail_p = $fail_ns->query( 'example.org', 'SOA', {} ); is( $fail_p, undef, 'No return from broken server' ); -my ( $e ) = grep { $_->tag eq 'LOOKUP_ERROR' } @{ Zonemaster::Engine->logger->entries }; -isa_ok( $e, 'Zonemaster::Engine::Logger::Entry' ); -( $e ) = grep { $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }; -is( %{$e->args}{proto}, 'UDP', 'Name server is blacklisted for UDP on non-EDNS SOA UDP query' ); +if ( $ENV{ZONEMASTER_RECORD} ) { + # The tests in this block will not work if we are running offline (i.e. + # without ZONEMASTER_RECORD=1). + # + # That is because the nameserver.data file already cached the + # failure to query $fail_ns, therefore $fail_ns->query() immediately + # returns undef without attempting to use the network. This path only + # generates a CACHED_RETURN logger entry, whereas the LOOKUP_ERROR and + # BLACKLISTING entries are only generated when we are actually attempting + # the query over the network. + # + # If we are running offline and the previous $fail_ns->query() did give us + # those two messages, it arguably means that there is a bug in the cache. + my ( $e ) = grep { $_->tag eq 'LOOKUP_ERROR' } @{ Zonemaster::Engine->logger->entries }; + isa_ok( $e, 'Zonemaster::Engine::Logger::Entry' ); + + ( $e ) = grep { $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }; + is( %{$e->args}{proto}, 'UDP', 'Name server is blacklisted for UDP on non-EDNS SOA UDP query' ); +} +else { + ok( ! grep({ $_->tag eq 'EXTERNAL_QUERY' } @{ Zonemaster::Engine->logger->entries }), + 'No network access was attempted' ) or diag(join("\n", @{ Zonemaster::Engine->logger->entries })); + ok( ! grep({ $_->tag eq 'LOOKUP_ERROR' } @{ Zonemaster::Engine->logger->entries }), + 'The lookup error came from cache, not network' ); + ok( ! grep({ $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }), + 'No blacklisting is done when running offline' ); +} Zonemaster::Engine->logger->clear_history(); my $fail_p_tcp = $fail_ns->query( 'example.org', 'SOA', { usevc => 1 } ); -( $e ) = grep { $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }; -is( %{$e->args}{proto}, 'TCP', 'Name server is blacklisted for TCP on non-EDNS SOA TCP query' ); +is( $fail_p_tcp, undef, 'No return from broken server on TCP either' ); + +if ( $ENV{ZONEMASTER_RECORD} ) { + # For the same reason as above, the test in this block will not work if + # running offline (i.e. without ZONEMASTER_RECORD=1). + my ( $e ) = grep { $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }; + is( %{$e->args}{proto}, 'TCP', 'Name server is blacklisted for TCP on non-EDNS SOA TCP query' ); +} +else { + ok( ! grep({ $_->tag eq 'EXTERNAL_QUERY' } @{ Zonemaster::Engine->logger->entries }), + 'No network access was attempted' ) or diag(join("\n", @{ Zonemaster::Engine->logger->entries })); + ok( ! grep({ $_->tag eq 'LOOKUP_ERROR' } @{ Zonemaster::Engine->logger->entries }), + 'The lookup error came from cache, not network' ); + ok( ! grep({ $_->tag eq 'BLACKLISTING' } @{ Zonemaster::Engine->logger->entries }), + 'No blacklisting is done when running offline' ); +} if ( $ENV{ZONEMASTER_RECORD} ) { Zonemaster::Engine::Nameserver->save( $datafile ); diff --git a/t/profiles.t b/t/profiles.t index 3d0f054e4..11ca1d225 100644 --- a/t/profiles.t +++ b/t/profiles.t @@ -18,11 +18,8 @@ Readonly my $EXAMPLE_PROFILE_1_YAML => q( resolver: defaults: fallback: true - igntc: false - recurse: true retrans: 234 retry: 123 - usevc: true source4: 192.0.2.53 source6: 2001:db8::42 net: @@ -57,9 +54,6 @@ Readonly my $EXAMPLE_PROFILE_1 => q( { "resolver": { "defaults": { - "usevc": true, - "recurse": true, - "igntc": false, "fallback": true, "retry": 123, "retrans": 234 @@ -113,9 +107,6 @@ Readonly my $EXAMPLE_PROFILE_2 => q( { "resolver": { "defaults": { - "usevc": false, - "recurse": false, - "igntc": true, "fallback": false, "retry": 99, "retrans": 88 @@ -227,9 +218,6 @@ subtest 'from_json("{}") returns a profile with all properties unset' => sub { subtest 'from_json() parses values from a string' => sub { my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 ); - is $profile->get( 'resolver.defaults.usevc' ), 1, 'resolver.defaults.usevc was parsed from JSON'; - is $profile->get( 'resolver.defaults.recurse' ), 1, 'resolver.defaults.recurse was parsed from JSON'; - is $profile->get( 'resolver.defaults.igntc' ), 0, 'resolver.defaults.igntc was parsed from JSON'; is $profile->get( 'resolver.defaults.fallback' ), 1, 'resolver.defaults.fallback was parsed from JSON'; is $profile->get( 'net.ipv4' ), 1, 'net.ipv4 was parsed from JSON'; is $profile->get( 'net.ipv6' ), 0, 'net.ipv6 was parsed from JSON'; @@ -263,12 +251,12 @@ subtest 'from_json() dies on illegal paths' => sub { throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"foobar":1}}}' ) } qr/^.*Unknown property .*/, 'resolver.defaults.foobar'; throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"dnssec":1}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.dnssec'; throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"edns_size":1}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.edns_size'; + throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"usevc":0}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.usevc'; + throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"recurse":0}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.recurse'; + throws_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"igntc":1}}}' ); } qr/^.*Unknown property .*/, 'resolver.defaults.igntc'; }; subtest 'from_json() dies on illegal values' => sub { - dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"usevc":0}}}' ); } "checks type of resolver.defaults.usevc"; - dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"recurse":0}}}' ); } "checks type of resolver.defaults.recurse"; - dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"igntc":1}}}' ); } "checks type of resolver.defaults.igntc"; dies_ok { Zonemaster::Engine::Profile->from_json( '{"resolver":{"defaults":{"fallback":0}}}' ); } "checks type of resolver.defaults.fallback"; dies_ok { Zonemaster::Engine::Profile->from_json( '{"net":{"ipv4":1}}' ); } "checks type of net.ipv4"; dies_ok { Zonemaster::Engine::Profile->from_json( '{"net":{"ipv6":0}}' ); } "checks type of net.ipv6"; @@ -302,9 +290,6 @@ subtest 'get() returns 1 for true' => sub { '{ "resolver": { "defaults": { - "usevc": true, - "recurse": true, - "igntc": true, "fallback": true } }, @@ -316,9 +301,6 @@ subtest 'get() returns 1 for true' => sub { }' ); - is $profile->get( 'resolver.defaults.usevc' ), 1, "returns 1 for true resolver.defaults.usevc"; - is $profile->get( 'resolver.defaults.recurse' ), 1, "returns 1 for true resolver.defaults.recurse"; - is $profile->get( 'resolver.defaults.igntc' ), 1, "returns 1 for true resolver.defaults.igntc"; is $profile->get( 'resolver.defaults.fallback' ), 1, "returns 1 for true resolver.defaults.fallback"; is $profile->get( 'net.ipv4' ), 1, "returns 1 for true net.ipv4"; is $profile->get( 'net.ipv6' ), 1, "returns 1 for true net.ipv6"; @@ -330,9 +312,6 @@ subtest 'get() returns 0 for false' => sub { '{ "resolver": { "defaults": { - "usevc": false, - "recurse": false, - "igntc": false, "fallback": false } }, @@ -344,9 +323,6 @@ subtest 'get() returns 0 for false' => sub { }' ); - is $profile->get( 'resolver.defaults.usevc' ), 0, "returns 0 for false resolver.defaults.usevc"; - is $profile->get( 'resolver.defaults.recurse' ), 0, "returns 0 for false resolver.defaults.recurse"; - is $profile->get( 'resolver.defaults.igntc' ), 0, "returns 0 for false resolver.defaults.igntc"; is $profile->get( 'resolver.defaults.fallback' ), 0, "returns 0 for false resolver.defaults.fallback"; is $profile->get( 'net.ipv4' ), 0, "returns 0 for false net.ipv4"; is $profile->get( 'net.ipv6' ), 0, "returns 0 for false net.ipv6"; @@ -399,9 +375,6 @@ subtest 'get() dies if the given property name is invalid' => sub { subtest 'set() inserts values for unset properties' => sub { my $profile = Zonemaster::Engine::Profile->new; - $profile->set( 'resolver.defaults.usevc', 1 ); - $profile->set( 'resolver.defaults.recurse', 1 ); - $profile->set( 'resolver.defaults.igntc', 0 ); $profile->set( 'resolver.defaults.fallback', 1 ); $profile->set( 'net.ipv4', 0 ); $profile->set( 'net.ipv6', 1 ); @@ -417,9 +390,6 @@ subtest 'set() inserts values for unset properties' => sub { $profile->set( 'test_cases', ['Zone01'] ); $profile->set( 'cache', { redis => { server => '127.0.0.1:6379', expire => 3600 } } ); - is $profile->get( 'resolver.defaults.usevc' ), 1, 'resolver.defaults.usevc can be given a value when unset'; - is $profile->get( 'resolver.defaults.recurse' ), 1, 'resolver.defaults.recurse can be given a value when unset'; - is $profile->get( 'resolver.defaults.igntc' ), 0, 'resolver.defaults.igntc can be given a value when unset'; is $profile->get( 'resolver.defaults.fallback' ), 1, 'resolver.defaults.fallback can be given a value when unset'; is $profile->get( 'net.ipv4' ), 0, 'net.ipv4 can be given a value when unset'; is $profile->get( 'net.ipv6' ), 1, 'net.ipv6 can be given a value when unset'; @@ -442,9 +412,6 @@ subtest 'set() inserts values for unset properties' => sub { subtest 'set() updates values for set properties' => sub { my $profile = Zonemaster::Engine::Profile->from_json( $EXAMPLE_PROFILE_1 ); - $profile->set( 'resolver.defaults.usevc', 0 ); - $profile->set( 'resolver.defaults.recurse', 0 ); - $profile->set( 'resolver.defaults.igntc', 1 ); $profile->set( 'resolver.defaults.fallback', 0 ); $profile->set( 'net.ipv4', 0 ); $profile->set( 'net.ipv6', 1 ); @@ -460,9 +427,6 @@ subtest 'set() updates values for set properties' => sub { $profile->set( 'test_cases', ['Zone02'] ); $profile->set( 'cache', { redis => { server => '127.0.0.2:6379', expire => 7200 } } ); - is $profile->get( 'resolver.defaults.usevc' ), 0, 'resolver.defaults.usevc was updated'; - is $profile->get( 'resolver.defaults.recurse' ), 0, 'resolver.defaults.recurse was updated'; - is $profile->get( 'resolver.defaults.igntc' ), 1, 'resolver.defaults.igntc was updated'; is $profile->get( 'net.ipv4' ), 0, 'net.ipv4 was updated'; is $profile->get( 'net.ipv6' ), 1, 'net.ipv6 was updated'; is $profile->get( 'no_network' ), 0, 'no_network was updated'; @@ -583,9 +547,6 @@ subtest 'merge() with a profile with all properties unset' => sub { $profile1->merge( $profile2 ); - is $profile1->get( 'resolver.defaults.usevc' ), 1, 'keeps value of resolver.defaults.usevc'; - is $profile1->get( 'resolver.defaults.recurse' ), 1, 'keeps value of resolver.defaults.recurse'; - is $profile1->get( 'resolver.defaults.igntc' ), 0, 'keeps value of resolver.defaults.igntc'; is $profile1->get( 'resolver.defaults.fallback' ), 1, 'keeps value of resolver.defaults.fallback'; is $profile1->get( 'net.ipv4' ), 1, 'keeps value of net.ipv4'; is $profile1->get( 'net.ipv6' ), 0, 'keeps value of net.ipv6'; @@ -609,9 +570,6 @@ subtest 'merge() with a profile with all properties set' => sub { $profile1->merge( $profile2 ); - is $profile1->get( 'resolver.defaults.usevc' ), 0, 'updates resolver.defaults.usevc'; - is $profile1->get( 'resolver.defaults.recurse' ), 0, 'updates resolver.defaults.recurse'; - is $profile1->get( 'resolver.defaults.igntc' ), 1, 'updates resolver.defaults.igntc'; is $profile1->get( 'resolver.defaults.fallback' ), 0, 'updates resolver.defaults.fallback'; is $profile1->get( 'net.ipv4' ), 0, 'updates net.ipv4'; is $profile1->get( 'net.ipv6' ), 1, 'updates net.ipv6'; @@ -635,11 +593,8 @@ subtest 'merge() does not update the other profile' => sub { $profile1->merge( $profile2 ); - is $profile2->get( 'resolver.defaults.usevc' ), undef, 'resolver.defaults.usevc was untouched in other'; is $profile2->get( 'resolver.defaults.retrans' ), undef, 'resolver.defaults.retrans was untouched in other'; - is $profile2->get( 'resolver.defaults.recurse' ), undef, 'resolver.defaults.recurse was untouched in other'; is $profile2->get( 'resolver.defaults.retry' ), undef, 'resolver.defaults.retry was untouched in other'; - is $profile2->get( 'resolver.defaults.igntc' ), undef, 'resolver.defaults.igntc was untouched in other'; is $profile2->get( 'resolver.defaults.fallback' ), undef, 'resolver.defaults.fallback was untouched in other'; is $profile2->get( 'resolver.source4' ), undef, 'resolver.source4 was untouched in other'; is $profile2->get( 'resolver.source6' ), undef, 'resolver.source6 was untouched in other'; @@ -655,33 +610,6 @@ subtest 'merge() does not update the other profile' => sub { }; subtest 'to_json() serializes each property' => sub { - subtest 'resolver.defaults.usevc' => sub { - my $profile = Zonemaster::Engine::Profile->new; - $profile->set( 'resolver.defaults.usevc', 1 ); - - my $json = $profile->to_json; - - eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"usevc":true}}}' ); - }; - - subtest 'resolver.defaults.recurse' => sub { - my $profile = Zonemaster::Engine::Profile->new; - $profile->set( 'resolver.defaults.recurse', 1 ); - - my $json = $profile->to_json; - - eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"recurse":true}}}' ); - }; - - subtest 'resolver.defaults.igntc' => sub { - my $profile = Zonemaster::Engine::Profile->new; - $profile->set( 'resolver.defaults.igntc', 0 ); - - my $json = $profile->to_json; - - eq_or_diff decode_json( $json ), decode_json( '{"resolver":{"defaults":{"igntc":false}}}' ); - }; - subtest 'resolver.defaults.fallback' => sub { my $profile = Zonemaster::Engine::Profile->new; $profile->set( 'resolver.defaults.fallback', 0 ); diff --git a/t/profiles/profile.json b/t/profiles/profile.json index 66dff4126..1ab94d30a 100644 --- a/t/profiles/profile.json +++ b/t/profiles/profile.json @@ -7,11 +7,8 @@ "resolver" : { "defaults" : { "debug" : false, - "igntc" : false, - "recurse" : false, "retrans" : 3, - "retry" : 2, - "usevc" :false + "retry" : 2 } }, "logfilter": { diff --git a/t/recursor-A.data b/t/recursor-A.data index f996aedcf..e6d5c1343 100644 Binary files a/t/recursor-A.data and b/t/recursor-A.data differ diff --git a/t/recursor.data b/t/recursor.data index 21b843723..86c52238e 100644 Binary files a/t/recursor.data and b/t/recursor.data differ diff --git a/t/recursor.t b/t/recursor.t index 3b754ff72..9deed59d2 100644 --- a/t/recursor.t +++ b/t/recursor.t @@ -4,6 +4,7 @@ use 5.14.2; use strict; use warnings; use List::Util qw[max]; +use Memoize; use Zonemaster::Engine::Nameserver; use Zonemaster::Engine::Util; @@ -21,15 +22,34 @@ isa_ok( $p, 'Zonemaster::Engine::Packet' ); ok( $p->answer > 0, 'answer records' ); is( name( ($p->answer)[0]->name ), 'www.iis.se', 'RR name ok' ); -my $p2 = Zonemaster::Engine::Recursor->recurse( 'www.wiccainfo.se' ); +my $p2 = Zonemaster::Engine::Recursor->recurse( 'zonemaster.net' ); isa_ok( $p2, 'Zonemaster::Engine::Packet' ); -is( scalar( $p2->answer ), 2, 'answer records' ); -isa_ok( ($p2->answer)[0], 'Zonemaster::LDNS::RR::CNAME' ); -is( name( ($p2->answer)[0]->owner ), 'www.wiccainfo.se', 'RR name ok' ); -is( name( ($p2->answer)[0]->cname ), 'spencer.faerywicca.se', 'RR cname ok' ); -isa_ok( ($p2->answer)[1], 'Zonemaster::LDNS::RR::A' ); -is( name( ($p2->answer)[1]->owner ), 'spencer.faerywicca.se', 'RR name ok' ); -is( ($p2->answer)[1]->address, '109.74.12.164', 'RR address ok' ); + +{ + no warnings 'redefine'; + my $count = 0; + + local *Zonemaster::Engine::Recursor::_recurse = sub { + $count++; + }; + + Zonemaster::Engine::Recursor->recurse( 'zonemaster.net', 'A', 'IN', [ Zonemaster::Engine::Recursor->root_servers() ] ); + is( $count, 0, 'memoize normalizer for recurse() works' ); + + Zonemaster::Engine::Recursor->recurse( 'zonemaster.net', 'A', 'IN', [ (Zonemaster::Engine->ns( 'a.root-servers.net.', '198.41.0.4' )) ] ); + is( $count, 1, 'memoize for recurse() works' ); + + Zonemaster::Engine::Recursor->clear_cache; + Zonemaster::Engine::Recursor->recurse( 'zonemaster.net' ); + is( $count, 2, 'memoize cache clear for recurse() works' ); +} + +sub is_parent { + my ( $name, $pname ) = @_; + + my $pn = Zonemaster::Engine::Recursor->parent( $name ); + is( $pn, $pname, "parent for $name is $pn" ); +} is_parent( 'iis.se', 'se' ); is_parent( 'www.iis.se', 'iis.se' ); @@ -43,26 +63,16 @@ is_parent( 'xx--doesnotexist.com', is_parent( 'pewc.eu', 'eu' ); is_parent( 'melbourneit.com.au', 'com.au' ); -sub is_parent { - my ( $name, $pname ) = @_; - - my $pn = Zonemaster::Engine::Recursor->parent( $name ); - is( $pn, $pname, "parent for $name is $pn" ); -} - my ( $name, $packet ) = Zonemaster::Engine::Recursor->parent( 'www.iis.se' ); isa_ok( $packet, 'Zonemaster::Engine::Packet' ); is( $name, 'iis.se', 'name ok' ); ok( $packet->no_such_record, 'expected packet content' ); -my @addr = Zonemaster::Engine::Recursor->get_addresses_for( 'ns.nic.se' ); -isa_ok( $_, 'Net::IP::XS' ) for @addr; -is( $addr[0]->short, '212.247.7.228', 'expected address' ); -is( $addr[1]->short, '2a00:801:f0:53::53', 'expected address' ); - -my $ns_count = Zonemaster::Engine::Nameserver->all_known_nameservers; -my $cache_count = keys %Zonemaster::Engine::Nameserver::Cache::object_cache; -ok( $cache_count < $ns_count, 'Fewer cache than ns' ); +if ( $ENV{ZONEMASTER_RECORD} ) { + my $ns_count = Zonemaster::Engine::Nameserver->all_known_nameservers; + my $cache_count = keys %Zonemaster::Engine::Nameserver::Cache::object_cache; + ok( $cache_count < $ns_count, 'Fewer cache than ns' ); +} if ( $ENV{ZONEMASTER_RECORD} ) { Zonemaster::Engine::Nameserver->save( $datafile ); diff --git a/t/undelegated.data b/t/undelegated.data index 54a0651a5..763cf7831 100644 Binary files a/t/undelegated.data and b/t/undelegated.data differ diff --git a/t/undelegated.t b/t/undelegated.t index 26171f2bd..64119782e 100644 --- a/t/undelegated.t +++ b/t/undelegated.t @@ -81,11 +81,11 @@ ok( !!( grep { $_->tag eq 'FAKE_DELEGATION_NO_IP' } @{ Zonemaster::Engine->logge Zonemaster::Engine->logger->clear_history; Zonemaster::Engine->add_fake_delegation( - 'nic.se' => { - 'ns.nic.se' => [ '212.247.7.228', '2a00:801:f0:53::53' ], - 'i.ns.se' => [ '194.146.106.22', '2001:67c:1010:5::53' ], - 'ns3.nic.se' => [ '212.247.8.152', '2a00:801:f0:211::152' ], - 'ns4.nic.se' => [ ] + 'nic.fr' => { + 'ns1.ext.nic.fr' => [ '193.51.208.13' ], + 'ns4.nic.fr' => [ '192.134.4.19', '2001:67c:2218:2::4:19' ], + 'ns6.ext.nic.fr' => [ '130.59.31.29', '2001:620:0:ff::2f' ], + 'ns7.ext.nic.fr' => [] }, fill_in_empty_oob_glue => 0, ); diff --git a/t/util.t b/t/util.t index 071738e3d..bdd27e3a8 100644 --- a/t/util.t +++ b/t/util.t @@ -2,7 +2,13 @@ use Test::More; use Test::Differences; use Test::Exception; -BEGIN { use_ok( 'Zonemaster::Engine::Util', qw( info name ns parse_hints ) ) } +use Encode qw(encode); +use utf8; + +BEGIN { + use_ok( 'Zonemaster::Engine' ); + use_ok( 'Zonemaster::Engine::Util', qw( info name ns parse_hints ) ) +} isa_ok( ns( 'name', '::1' ), 'Zonemaster::Engine::Nameserver' ); isa_ok( info( 'TAG', {} ), 'Zonemaster::Engine::Logger::Entry' ); @@ -120,4 +126,28 @@ EOF } }; +subtest 'escape_unprintable' => sub { + my @cases = ( + { + input => 'hello world!', + expected => 'hello world!', + }, + { + input => "escape this: \x1B\x7F\x80\xFF\x00\x0D\x0A\\", + expected => 'escape this: \027\127\128\255\000\013\010\\\\', + }, + { + input => encode('UTF-8', 'cassé'), + expected => 'cass\195\169' + } + ); + + for my $case ( @cases ) { + # The following line is a hack that ensures that $bytes is a + # byte-oriented string, instead of a character-oriented one. + my $bytes = encode( 'Latin1', $case->{input} ); + is Zonemaster::Engine::Util::escape_unprintable( $bytes ), $case->{expected}; + } +}; + done_testing; diff --git a/t/zone.data b/t/zone.data index ca6ea7b95..8dc2d243b 100644 Binary files a/t/zone.data and b/t/zone.data differ diff --git a/t/zonemaster.data b/t/zonemaster.data index 97940e775..2e145b3a0 100644 Binary files a/t/zonemaster.data and b/t/zonemaster.data differ diff --git a/util/data2dig b/util/data2dig index 85cf125c7..d1de77198 100755 --- a/util/data2dig +++ b/util/data2dig @@ -1,53 +1,78 @@ #!/usr/bin/env perl use strict; use warnings; -use feature 'say'; +use v5.14; + +use CBOR::XS; + +use Zonemaster::Engine::Nameserver; use Zonemaster::Engine::Packet; -use JSON::PP; -use MIME::Base64; -use Module::Find qw[useall]; -use Readonly; -use Scalar::Util qw[blessed]; -useall 'Zonemaster::LDNS::RR'; - -# Decoder taken from Zonemaster::Engine::Nameserver->restore -Readonly my $decoder => JSON::PP->new->filter_json_single_key_object( - 'Zonemaster::LDNS::Packet' => sub { - my ( $ref ) = @_; - ## no critic (Modules::RequireExplicitInclusion) - my $obj = Zonemaster::LDNS::Packet->new_from_wireformat( decode_base64( $ref->{data} ) ); - $obj->answerfrom( $ref->{answerfrom} ); - $obj->timestamp( $ref->{timestamp} ); - $obj->querytime( $ref->{querytime} ); - return $obj; - } - )->filter_json_single_key_object( - 'Zonemaster::Engine::Packet' => sub { - my ( $ref ) = @_; - return Zonemaster::Engine::Packet->new( { packet => $ref } ); +# Decode input into packets +my $cbor = CBOR::XS->new(); +my $saved_contents = $cbor->decode(do { local $/; <> }); + +my $format_version = $saved_contents->{format_version}; +say ";;;;"; +say ";;;; START OF METADATA"; +say ";;;; FORMAT VERSION: ", $saved_contents->{format_version}; +say ";;;; ENGINE VERSION: ", $saved_contents->{engine_version}; +say ";;;; END OF METADATA"; +say ";;;;"; +say ""; + +my @entries = + map { + my ( $name, $ip, $request, $response ) = @$_; + [ + $name, + $ip, + Zonemaster::Engine::Packet->new({ packet => Zonemaster::LDNS::Packet->new_from_wireformat($request) }), + Zonemaster::Engine::Nameserver::_deserialize_packet($response) + ] } - ); + map { my ($ns, $entries) = @$_; map { [ @$ns, @$_ ] } @$entries } + @{$saved_contents->{packets}}; +# Print delimited packets +my $delim = (";" x 78 . "\n") x 3; +for my $entry ( @entries ) { + my ( $ns, $ip, $request, $response ) = @$entry; + say $delim; -# Decode input into packets -my @packets; -while ( my $line = <> ) { - my ( $name, $addr, $data ) = split( / /, $line, 3 ); - my $tree = deserialize( $data ); - push @packets, packets( $tree ); + my $flags = $request->id(); + say ";; Query to $ns ($ip) ", + ($flags & (1 << 15) ? "over TCP" : "over UDP"); + + say request_to_string($request), "\n"; + if (defined $response) { + say $response->string; + } + else { + say ";; No response from name server."; + } + say ""; } -# Order packets chronologically -@packets = sort { $a->timestamp cmp $b->timestamp } @packets; -# Print delimited packets -my $delim = ";" x 78; -for my $packet ( @packets ) { - say $delim; - say $packet->string; - $delim = "\n" . ";" x 78; +# Modify the string representation of the request slightly so it doesn’t look +# too much like its response, and more like what dig +qr would show. +sub request_to_string { + my ( $request ) = @_; + + my @lines = split( "\n", $request->string ); + $lines[0] =~ s/, id: \d+$//; + my $result = join( "\n", @lines[0..3] ); + + if (my ( $edns ) = grep { /^;; EDNS: / } @lines) { + $result .= "\n$edns"; + } + if (my ( $msg_size ) = grep { /^;; MSG SIZE rcvd: \d+$/ } @lines) { + $result .= "\n" . ($msg_size =~ s/rcvd/sent/r); + } + + return $result; } @@ -64,46 +89,7 @@ for my $packet ( @packets ) { =head1 DESCRIPTION B exports saved Zonemaster::Engine cache files to human readable -format as chronologically ordered response packets in dig format. - - -=head1 SUBROUTINES - - -=head2 deserialize - -Deserialize a string in Zonemaster::Engine saved cache format. - -Returns a tree of nested HASHREFs with decoded Zonemaster::Engine::Packet -objects. - -=cut - -sub deserialize { - my $data = shift; - return $decoder->decode( $data ); -} - - -=head2 packets - -Return all Zonemaster::Engine::Packet objects from a tree of nested HASHREFs. +format as packets in dig format. =cut -sub packets { - my ( $data ) = @_; - if ( ref $data eq 'HASH' && %{$data} && not blessed $data ) { - my @packets; - for my $key ( sort keys %$data ) { - push @packets, packets( $data->{$key} ); - } - return @packets; - } - elsif ( blessed $data && $data->isa( 'Zonemaster::Engine::Packet' ) ) { - return ( $data ); - } - else { - return (); - } -}