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..72bb5455f 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; diff --git a/lib/Zonemaster/Engine/DNSName.pm b/lib/Zonemaster/Engine/DNSName.pm index 45dc785e3..3375e0bfa 100644 --- a/lib/Zonemaster/Engine/DNSName.pm +++ b/lib/Zonemaster/Engine/DNSName.pm @@ -155,12 +155,6 @@ sub prepend { return $self->new( { labels => \@labels } ); } -sub TO_JSON { - my ( $self ) = @_; - - return $self->string; -} - 1; =head1 NAME @@ -240,10 +234,6 @@ See also L. Returns a new L object, representing the called one with the given label prepended. -=item TO_JSON - -Helper method for JSON encoding. - =back =cut diff --git a/lib/Zonemaster/Engine/Nameserver.pm b/lib/Zonemaster/Engine/Nameserver.pm index f270e8da7..28480402c 100644 --- a/lib/Zonemaster/Engine/Nameserver.pm +++ b/lib/Zonemaster/Engine/Nameserver.pm @@ -15,18 +15,19 @@ 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 +287,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 +356,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 +530,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 +581,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 +776,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 +820,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/Packet.pm b/lib/Zonemaster/Engine/Packet.pm index 8ee033ea9..3ec67f06b 100644 --- a/lib/Zonemaster/Engine/Packet.pm +++ b/lib/Zonemaster/Engine/Packet.pm @@ -157,12 +157,6 @@ sub answerfrom { return $from; } -sub TO_JSON { - my ( $self ) = @_; - - return { 'Zonemaster::Engine::Packet' => $self->packet }; -} - 1; =head1 NAME @@ -232,10 +226,6 @@ C and C, only RRs from those sections are returned. Wrapper for the underlying packet method, that replaces undefined values with the string CunknownE>. -=item TO_JSON - -Support method for L to be able to serialize these objects. - =back =head1 METHODS PASSED THROUGH diff --git a/t/Test-address.data b/t/Test-address.data index 8d5904274..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 79d60390c..2ca7fcd58 100644 Binary files a/t/Test-connectivity03.data and b/t/Test-connectivity03.data differ 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 6f5da6a60..137e86a9f 100644 Binary files a/t/Test-delegation.data and b/t/Test-delegation.data differ 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 daafaf91a..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 06f1e1f39..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 d6a7b9b31..0938aa005 100644 Binary files a/t/Test-dnssec10.data and b/t/Test-dnssec10.data differ 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 f6b12273c..9bf35c77b 100644 Binary files a/t/Test-zone01-A.data and b/t/Test-zone01-A.data differ diff --git a/t/Test-zone01-B.data b/t/Test-zone01-B.data index 09d2246aa..67652392b 100644 Binary files a/t/Test-zone01-B.data and b/t/Test-zone01-B.data differ 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 bf0c3ad48..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 c7254d0b6..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 6301c9754..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 e646c3c47..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 110c2327b..7149e06bb 100644 Binary files a/t/asn.data and b/t/asn.data differ diff --git a/t/methodsv2.data b/t/methodsv2.data index 281a29e09..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..4327e7949 100644 --- a/t/nameserver-axfr.t +++ b/t/nameserver-axfr.t @@ -7,14 +7,13 @@ use Test::Fatal; 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 5017af76b..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 ee740efe8..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"; 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 428a5894e..86c52238e 100644 Binary files a/t/recursor.data and b/t/recursor.data differ diff --git a/t/undelegated.data b/t/undelegated.data index bb1947d1f..763cf7831 100644 Binary files a/t/undelegated.data and b/t/undelegated.data differ 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 (); - } -}