feat: add full Zonemaster stack with Docker and Spanish UI

- Clone all 5 Zonemaster component repos (LDNS, Engine, CLI, Backend, GUI)
- Dockerfile.backend: 8-stage multi-stage build LDNS→Engine→CLI→Backend
- Dockerfile.gui: Astro static build served via nginx
- docker-compose.yml: backend (internal) + frontend (port 5353)
- nginx.conf: root redirects to /es/, /api/ proxied to backend
- zonemaster-gui/config.ts: defaultLanguage set to 'es' (Spanish)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:19:24 +02:00
commit 8d4eaa1489
1567 changed files with 204155 additions and 0 deletions

29
zonemaster-ldns/t/axfr.t Normal file
View File

@@ -0,0 +1,29 @@
use Test::More;
use Test::Fatal;
BEGIN { use_ok( 'Zonemaster::LDNS' ) }
SKIP: {
skip 'no network', 3 unless $ENV{TEST_WITH_NETWORK};
my $res = Zonemaster::LDNS->new( '46.21.106.227' );
my $res2 = Zonemaster::LDNS->new( '192.36.144.107' );
my $counter = 0;
my $return = $res->axfr( 'cyberpomo.com',
sub {
my ($rr) = @_;
$counter += 1;
if ($rr->type eq 'CNAME') {
return 0;
} else {
return 1;
}
});
ok(!$return, 'Terminated early');
ok(($counter > 1), 'Saw more than one entry (' . $counter . ')');
like( exception { $res2->axfr( 'iis.se', sub { return 1 })}, qr/NOTAUTH/, 'Expected exception');
}
done_testing;

101
zonemaster-ldns/t/dnssec.t Normal file
View File

@@ -0,0 +1,101 @@
use Test::More;
BEGIN { use_ok( 'Zonemaster::LDNS' ); }
my $key1 = Zonemaster::LDNS::RR->new(
"iis.se. 2395 IN DNSKEY 257 3 5 AwEAAcq5u+qe5VibnyvSnGU20panweAk2QxflGVuVQhzQABQV4SIdAQs +LNVHF61lcxe504jhPmjeQ656X6t+dHpRz1DdPO/ukcIITjIRoJHqS+X XyL6gUluZoDU+K6vpxkGJx5m5n4boRTKCTUAR/9rw2+IQRRTtb6nBwsC 3pmf9IlJQjQMb1cQTb0UO7fYgXDZIYVul2LwGpKRrMJ6Ul1nepkSxTMw Q4H9iKE9FhqPeIpzU9dnXGtJ+ZCx9tWSZ9VsSLWBJtUwoE6ZfIoF1ioq qxfGl9JV1/6GkDxo3pMN2edhkp8aqoo/R+mrJYi0vE8jbXvhZ12151Dy wuSxbGjAlxk="
);
my $key2 = Zonemaster::LDNS::RR->new(
"iis.se. 1591 IN DNSKEY 256 3 5 BQEAAAABuWpCewwMRD7yPzy6TGsymMAc82IHVGB+vjKVIAYKbPG7QxuLEtEzUxDJo09gLN2/N0OF+NnTkmDMj8KA+eIgtqmMuq5kdDVc+eSNLJZ0 am0o27UEkXmW20iV0d6B/KW1X1nufzBSaacUzkBKyDfK4cN3aVsYIDXT H7Jw1agEzrM="
);
my $soa = Zonemaster::LDNS::RR->new( "iis.se. 3600 IN SOA ns.nic.se. hostmaster.iis.se. 1384853101 10800 3600 1814400 14400" );
my $sig = Zonemaster::LDNS::RR->new(
"iis.se. 3600 IN RRSIG SOA 5 2 3600 20131129082501 20131119082501 59213 iis.se. ShhhfRT82jfA/J1AAqiie/4r7JuiYOpK6dIwugOtlf0/UpVsOYEIukpe Bq9i7fsa0GNWz/o9gqF8DnsCHzgxZnAngTrJpZAlsrC/FP/6v8WfnFsP LDw9g6Ow8Z6TL9JmZr22YPp27Rwujdb5AnzdurEvQxIAqW66CCCy2pc9 //s="
);
is( $sig->keytag, $key2->keytag );
ok( !$sig->verify( [$soa], [ $key1, $key2 ] ), 'Signature does not verify (expired).' );
ok( !$sig->verify( [$soa], [$key1] ), 'Signature does not verify (wrong key).' );
is(
$sig->verify_str( [$soa], [ $key1, $key2 ] ),
'DNSSEC signature has expired',
'Expected unsuccessful verification message.'
);
is(
$sig->verify_str( [$soa], [$key1] ),
'No keys with the keytag and algorithm from the RRSIG found',
'Expected unsuccessful verification message.'
);
my $msg = '';
my $res = $sig->verify_time( [$soa], [ $key1, $key2 ], 1385628478, $msg );
ok( $res, 'Verified OK in the past.' );
is( $msg, 'All OK', 'Expected verification message' );
my $ds1 = $key1->ds( 'sha1' );
isa_ok( $ds1, 'Zonemaster::LDNS::RR::DS', 'sha1' );
ok( $ds1->verify( $key1 ) ) if $ds1;
my $ds2 = $key1->ds( 'sha256' );
isa_ok( $ds2, 'Zonemaster::LDNS::RR::DS', 'sha256' );
ok( $ds2->verify( $key1 ) ) if $ds2;
my $ds3 = $key1->ds( 'sha384' );
isa_ok( $ds3, 'Zonemaster::LDNS::RR::DS', 'sha384' );
ok( $ds3->verify( $key1 ) ) if $ds3;
my $ds4 = $key1->ds( 'gost' );
if ( $ds4 ) { # We may not have GOST available.
isa_ok( $ds4, 'Zonemaster::LDNS::RR::DS', 'gost' );
ok( $ds4->verify( $key1 ) ) if $ds4;
}
is($key1->keysize, 2048, 'Key is 2048 bits long');
is($key2->keysize, 1024, 'Key is 1024 bits long');
my $nsec = Zonemaster::LDNS::RR->new('xx.se. 7200 IN NSEC xx0r.se. NS RRSIG NSEC');
isa_ok($nsec, 'Zonemaster::LDNS::RR::NSEC');
ok($nsec->covers('xx-example.se'), 'Covers xx-example.se');
ok(!$nsec->covers('.'), 'Does not cover the root domain');
my $nsec3 = Zonemaster::LDNS::RR->new('NR2E513KM693MBTNVHH56ENF54F886T0.com. 86400 IN NSEC3 1 1 0 - NR2FUHQVR56LH70L6F971J3L6N1RH2TU NS DS RRSIG');
isa_ok($nsec3, 'Zonemaster::LDNS::RR::NSEC3');
ok($nsec3->covers('xx-example.com'), 'Covers xx-example.com');
is($nsec3->covers('.'), undef, 'Does not cover the root domain');
subtest 'malformed NSEC3 do not cover anything' => sub {
# Malformed resource record lacking a next hashed owner name field in its
# RDATA. The only way to synthesize such a datum is to use the RFC 3597
# syntax.
my $example = Zonemaster::LDNS::RR->new(
q{example. 0 IN NSEC3 \# 15 01 00 0001 01 AB 00 0006 400000000002}
);
is( $example->covers("example"), undef );
# NSEC3 resource record whose owner name is the root name. This should
# normally not happen.
$example = Zonemaster::LDNS::RR->new(
q{. 0 IN NSEC3 1 0 1 ab 01234567 A RRSIG}
);
is( $example->covers("example"), undef );
};
SKIP: {
skip 'no network', 3 unless $ENV{TEST_WITH_NETWORK};
$res = Zonemaster::LDNS->new( '212.247.7.228' );
$res->dnssec( 1 );
my $p1 = eval { $res->query('www.iis.se', 'A') };
skip 'Remote server not responding', 3 if not $p1;
ok( $p1->needs_edns, 'Needs EDNS0');
ok( $p1->has_edns, 'Alias is there');
ok( ($p1->edns_size > 0), 'EDNS0 size larger than zero' );
}
done_testing;

View File

@@ -0,0 +1,25 @@
$ORIGIN .
$TTL 86400 ; 1 day
example.org IN SOA kennedy.example.org. hostmaster.example.org. (
2014061900 ; serial
28800 ; refresh (8 hours)
7200 ; retry (2 hours)
604800 ; expire (1 week)
86400 ; minimum (1 day)
)
NS ns2.example.net.
NS kennedy.example.org.
NS tara.example.org.
NS illyria.example.org.
MX 10 kennedy.example.org.
$ORIGIN example.org.
home A 183.68.21.31
staging CNAME kadath.example.net.
willow A 179.199.7.2
www CNAME home
tara A 109.174.1.145
kennedy A 146.121.6.227
sameen A 146.121.6.47
illyria A 146.121.0.115
illyria AAAA 2a02:70:4::44b
spencer A 119.74.162.14

30
zonemaster-ldns/t/idn.t Normal file
View File

@@ -0,0 +1,30 @@
use Test::More;
use Test::Fatal;
use Encode;
use Devel::Peek;
use utf8;
BEGIN { use_ok( "Zonemaster::LDNS" => qw[:all] ) }
no warnings 'uninitialized';
if (exception {to_idn("whatever")} =~ /libidn2 not installed/) {
ok(!has_idn(), 'No IDN');
done_testing;
exit;
}
ok(has_idn(), 'Has IDN');
my $encoded = to_idn( 'annaröd.se' );
is( $encoded, 'xn--annard-0xa.se', 'One name encoded right' );
my @before = ('annaröd.se', 'rindlöw.se', 'räksmörgås.se', 'nic.中國', 'iis.se');
my @many = to_idn @before;
is_deeply(
\@many,
[qw( xn--annard-0xa.se xn--rindlw-0xa.se xn--rksmrgs-5wao1o.se nic.xn--fiqz9s iis.se )],
'Many encoded right'
);
like( exception { to_idn( "ö" x 63 ) }, qr/IDN2_PUNYCODE_/i, 'croaks on IDN encoding failures' );
done_testing;

View File

@@ -0,0 +1,15 @@
use Test::More;
use Test::Fatal;
use strict;
use warnings;
BEGIN { use_ok("Zonemaster::LDNS" => qw(load_zonefile))}
my @rrs = load_zonefile("t/example.org");
is(scalar(@rrs), 16, 'All records loaded');
is($rrs[0]->type, 'SOA', 'SOA record first');
is($rrs[-1]->type, 'A', 'A record last');
is(lc($rrs[-1]->name), 'spencer.example.org.', 'Expected name last');
done_testing();

View File

@@ -0,0 +1,37 @@
#!perl
use v5.14.2;
use strict;
use warnings;
use utf8;
use Test::More tests => 2;
use Test::NoWarnings;
use File::Basename qw( dirname );
chdir dirname( dirname( __FILE__ ) ) or BAIL_OUT( "chdir: $!" );
my $makebin = 'make';
sub make {
my @make_args = @_;
undef $ENV{MAKEFLAGS};
my $command = join( ' ', $makebin, '-s', @make_args );
my $output = `$command 2>&1`;
if ( $? == -1 ) {
BAIL_OUT( "failed to execute: $!" );
}
elsif ( $? & 127 ) {
BAIL_OUT( "child died with signal %d, %s coredump\n", ( $? & 127 ), ( $? & 128 ) ? 'with' : 'without' );
}
return $output, $? >> 8;
}
subtest "distcheck" => sub {
my ( $output, $status ) = make "distcheck";
is $status, 0, $makebin . ' distcheck exits with value 0';
is $output, "", $makebin . ' distcheck gives empty output';
};

122
zonemaster-ldns/t/netldns.t Normal file
View File

@@ -0,0 +1,122 @@
use Test::More;
use Devel::Peek;
use version;
BEGIN { use_ok( 'Zonemaster::LDNS' ) }
my $lib_v = version->parse(Zonemaster::LDNS::lib_version());
ok( $lib_v >= v1.6.16, 'ldns version at least 1.6.16' );
SKIP: {
skip 'no network', 59 unless $ENV{TEST_WITH_NETWORK};
my $s = Zonemaster::LDNS->new( '8.8.8.8' );
isa_ok( $s, 'Zonemaster::LDNS' );
my $p2 = $s->query( 'iis.se', 'NS', 'IN' );
isa_ok( $p2, 'Zonemaster::LDNS::Packet' );
is( $p2->rcode, 'NOERROR' );
is( $p2->opcode, 'QUERY', 'expected opcode' );
my $pround = Zonemaster::LDNS::Packet->new_from_wireformat( $p2->wireformat );
isa_ok( $pround, 'Zonemaster::LDNS::Packet' );
is( $pround->opcode, $p2->opcode, 'roundtrip opcode OK' );
is( $pround->rcode, $p2->rcode, 'roundtrip rcode OK' );
ok( $p2->id() > 0, 'packet ID set' );
ok( $p2->qr(), 'QR bit set' );
ok( !$p2->aa(), 'AA bit not set' );
ok( !$p2->tc(), 'TC bit not set' );
ok( $p2->rd(), 'RD bit set' );
ok( !$p2->cd(), 'CD bit not set' );
ok( $p2->ra(), 'RA bit set' );
ok( !$p2->ad(), 'AD bit not set' );
ok( !$p2->do(), 'DO bit not set' );
cmp_ok( $p2->querytime, '>=', 0);
is( $p2->answerfrom, '8.8.8.8', 'expected answerfrom' );
$p2->answerfrom( '1.2.3.4' );
is( $p2->answerfrom, '1.2.3.4', 'setting answerfrom works' );
ok($p2->timestamp > 0, 'has a timestamp to begin with');
$p2->timestamp( 4711 );
is( $p2->timestamp, 4711, 'setting timestamp works' );
$p2->timestamp( 4711.4711 );
ok( $p2->timestamp - 4711.4711 < 0.0001, 'setting timestamp works with microseconds too' );
eval { $s->query( 'nic.se', 'gurksallad', 'CH' ) };
like( $@, qr/Unknown RR type: gurksallad/ );
eval { $s->query( 'nic.se', 'SOA', 'gurksallad' ) };
like( $@, qr/Unknown RR class: gurksallad/ );
eval { $s->query( 'nic.se', 'soa', 'IN' ) };
ok( !$@ );
my @answer = $p2->answer;
cmp_ok( scalar( @answer ), '<=', 6, 'at most 6 NS records in answer (iis.se)' );
cmp_ok( scalar( @answer ), '>=', 2, 'at least 2 NS records in answer (iis.se)' );
my %known_ns = map { $_ => 1 } qw[nsp.dnsnode.net. nsa.dnsnode.net. nsu.dnsnode.net.];
foreach my $rr ( @answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::NS' );
is( lc($rr->owner), 'iis.se.', 'expected owner name' );
ok( $rr->ttl > 0, 'positive TTL (' . $rr->ttl . ')' );
is( $rr->type, 'NS', 'type is NS' );
is( $rr->class, 'IN', 'class is IN' );
ok( $known_ns{ lc($rr->nsdname) }, 'known nsdname (' . $rr->nsdname . ')' );
}
my $p = $s->query( 'zonemaster.fr', 'MX' );
isa_ok( $p, 'Zonemaster::LDNS::Packet' );
is( $p->rcode, 'NOERROR', 'expected rcode' );
@answer = sort { $a->preference <=> $b->preference } $p->answer;
is( $answer[0]->preference, 7, 'expected MX preference 7' );
is( lc($answer[0]->exchange), 'mx1.nic.fr.', 'known MX exchange mx1.nic.fr' );
is( $answer[1]->preference, 8, 'expected MX preference 8' );
is( lc($answer[1]->exchange), 'mx2.nic.fr.', 'known MX exchange mx2.nic.fr' );
my $lroot = Zonemaster::LDNS->new( '199.7.83.42' );
my $se = $lroot->query( 'se', 'NS' );
is( scalar( $se->question ), 1, 'one question' );
is( scalar( $se->answer ), 0, 'zero answers' );
my $authority = scalar $se->authority;
cmp_ok( $authority, '<=', 13, 'at most 13 NS (authority)' );
cmp_ok( $authority, '>=', 6, 'at least 6 NS (authority)' );
my $add = scalar( $se->additional );
cmp_ok( $add, '<=', 26, 'at most 20 additional' );
cmp_ok( $add, '>=', 8, 'at least 8 additional' );
my $rr = Zonemaster::LDNS::RR->new_from_string(
'se. 172800 IN SOA catcher-in-the-rye.nic.se. registry-default.nic.se. 2013111305 1800 1800 864000 7200' );
my $rr2 =
Zonemaster::LDNS::RR->new( 'se. 172800 IN TXT "SE zone update: 2013-11-13 15:08:28 +0000 (EPOCH 1384355308) (auto)"' );
ok( $se->unique_push( 'answer', $rr ), 'unique_push returns ok' );
is( $se->answer, 1, 'one record in answer section' );
ok( !$se->unique_push( 'answer', $rr ), 'unique_push returns false' );
is( $se->answer, 1, 'still one record in answer section' );
ok( $se->unique_push( 'ansWer', $rr2 ), 'unique_push returns ok again' );
is( $se->answer, 2, 'two records in answer section' );
}
my $made = Zonemaster::LDNS::Packet->new( 'foo.com', 'SOA', 'IN' );
isa_ok( $made, 'Zonemaster::LDNS::Packet' );
foreach my $flag (qw[do qr tc aa rd cd ra ad]) {
ok(!$made->$flag(), uc($flag).' not set');
$made->$flag(1);
ok($made->$flag(), uc($flag).' set');
}
is($made->edns_size, 0, 'Initial EDNS0 UDP size is 0');
ok($made->edns_size(4096));
is($made->edns_size, 4096, 'EDNS0 UDP size set to 4096');
ok(!$made->edns_size(2**17), 'Setting to too big did not work'); # Too big
is($made->edns_rcode, 0, 'Extended RCODE is 0');
$made->edns_rcode(1);
is($made->edns_rcode, 1, 'Extended RCODE is 1');
is($made->type, 'answer');
done_testing;

22
zonemaster-ldns/t/nsid.t Normal file
View File

@@ -0,0 +1,22 @@
use strict;
use warnings;
use Test::More;
use Zonemaster::LDNS;
SKIP: {
skip 'no network', 1 unless $ENV{TEST_WITH_NETWORK};
my $host = '192.134.4.1'; #ns1.nic.fr with nsid: ns1.th3.nic.fr
my $expected_nsid = "ns1.th3.nic.fr";
my $pkt = Zonemaster::LDNS::Packet->new('domain.example');
$pkt->nsid; # set the NSID EDNS option
my $res = Zonemaster::LDNS->new($host)->query_with_pkt($pkt);
my $nsid = $res->get_nsid();
is( $nsid, $expected_nsid, 'Correct NSID' );
};
done_testing();

133
zonemaster-ldns/t/packet.t Normal file
View File

@@ -0,0 +1,133 @@
use strict;
use warnings;
use Test::More;
use MIME::Base64;
use Test::Differences;
use Test::Fatal;
use_ok('Zonemaster::LDNS');
my $p = new_ok('Zonemaster::LDNS::Packet' => ['www.example.org', 'SOA', 'IN']);
foreach my $r (qw[NOERROR FORMERR SERVFAIL NXDOMAIN NOTIMPL REFUSED YXDOMAIN YXRRSET NXRRSET NOTAUTH NOTZONE]) {
is($p->rcode($r), $r, $r);
}
like( exception {$p->rcode('gurksallad')}, qr/Unknown RCODE: gurksallad/, 'Expected exception' );
foreach my $r (qw[QUERY IQUERY STATUS NOTIFY UPDATE]) {
is($p->opcode($r), $r, $r);
}
like( exception {$p->opcode('gurksallad')}, qr/Unknown OPCODE: gurksallad/, 'Expected exception' );
is( scalar $p->question, 1, 'One RR in question section' );
isa_ok( ($p->question)[0], 'Zonemaster::LDNS::RR' );
is($p->id(4711), 4711, 'Setting ID');
is($p->id(2147488359), 4711, 'Wraparound ID');
is($p->querytime(4711), 4711, 'Setting query time');
is($p->querytime(2147488359), 2147488359, 'Setting larger query time');
is($p->answerfrom, undef, 'No answerfrom');
$p->answerfrom('127.0.0.1');
is($p->answerfrom, '127.0.0.1', 'Localhost');
subtest "croak when stringifying packet with malformed CAA" => sub {
my $will_croak = sub {
# Constructing a synthetic packet that would have the following string
# representation in dig-like format:
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 13944
# ;; flags: qr aa ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; bad-caa.example. IN CAA
#
# ;; ANSWER SECTION:
# bad-caa.example. 3600 IN CAA \# 4 C0000202
#
# ;; AUTHORITY SECTION:
#
# ;; ADDITIONAL SECTION:
my $packet_bin = pack(
'H*',
'367884000001000100000000' . # header
'076261642d636161076578616d706c650001010001' . # question
'c00c0101000100000e100004c0000202' # bad answer
);
my $packet = Zonemaster::LDNS::Packet->new_from_wireformat( $packet_bin );
# This must croak
$packet->string;
};
like( exception { $will_croak->() }, qr/^Failed to convert packet to string/ );
};
subtest "Answer section" => sub {
# Parse a packet with a single incomplete MX record
my $data = decode_base64( "EjSFgAABAAIAAAAAB2V4YW1wbGUCc2UAAA8AAcAMAA8AAQABUYAAAgAKwAwAAQABAAFRgAAEwAACAQ==");
my $p = Zonemaster::LDNS::Packet->new_from_wireformat( $data );
my $rr_count = scalar $p->answer;
is $rr_count, 1, "keep complete RRs but ignore incomplete ones";
# Retrieve section as a Zonemaster::LDNS::RRList object
my $rrlist = $p->answer_rrlist;
is( $rrlist->count, 1, 'One RR in RRList' );
ok( $rrlist->string, 'example.se. 86400 IN A 192.0.2.1');
isa_ok( $rrlist->pop, 'Zonemaster::LDNS::RR' );
};
subtest "Authority section" => sub {
# Parse a packet with a single incomplete MX record
my $data = decode_base64( "EjSFgAABAAAAAgAAB2V4YW1wbGUCc2UAAA8AAcAMAA8AAQABUYAAAgAKwAwAAQABAAFRgAAEwAACAQ==" );
my $p = Zonemaster::LDNS::Packet->new_from_wireformat( $data );
my $rr_count = scalar $p->authority;
is $rr_count, 1, "keep complete RRs but ignore incomplete ones";
# Retrieve section as a Zonemaster::LDNS::RRList object
my $rrlist = $p->authority_rrlist;
is( $rrlist->count, 1, 'One RR in RRList' );
ok( $rrlist->string, 'example.se. 86400 IN A 192.0.2.1');
isa_ok( $rrlist->pop, 'Zonemaster::LDNS::RR' );
};
subtest "Additional section" => sub {
# Parse a packet with a single incomplete MX record
my $data = decode_base64( "EjSFgAABAAAAAAACB2V4YW1wbGUCc2UAAA8AAcAMAA8AAQABUYAAAgAKwAwAAQABAAFRgAAEwAACAQ==" );
my $p = Zonemaster::LDNS::Packet->new_from_wireformat( $data );
my $rr_count = scalar $p->additional;
is $rr_count, 1, "keep complete RRs but ignore incomplete ones";
# Retrieve section as a Zonemaster::LDNS::RRList object
my $rrlist = $p->additional_rrlist;
is( $rrlist->count, 1, 'One RR in RRList' );
ok( $rrlist->string, 'example.se. 86400 IN A 192.0.2.1');
isa_ok( $rrlist->pop, 'Zonemaster::LDNS::RR' );
};
subtest "All sections" => sub {
my $p = Zonemaster::LDNS::Packet->new( 'example.se', 'SOA', 'IN' );
my $rrlist = $p->question_rrlist();
is( $rrlist->count, 1, 'One RR in RRList' );
isa_ok( $rrlist->get(0), 'Zonemaster::LDNS::RR' );
$p->unique_push( 'authority', Zonemaster::LDNS::RR->new_from_string( 'example.se. IN SOA dnsmaster.example.se hostmaster.example.se 2024000000 7200 1800 2419200 5400' ) );
$p->unique_push( 'answer', Zonemaster::LDNS::RR->new_from_string( 'example.se. IN NS ns1.example.se' ) );
$p->unique_push( 'additional', Zonemaster::LDNS::RR->new_from_string( 'ns1.example.se. IN A 192.0.2.1' ) );
$rrlist = $p->all;
is( $rrlist->count, 3, 'Three RRs in RRList' );
while ( my $rr = $rrlist->pop ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR' );
}
};
done_testing();

View File

@@ -0,0 +1,17 @@
use Test::More;
use Test::Fatal;
use strict;
use warnings;
BEGIN { use_ok("Zonemaster::LDNS")}
SKIP: {
skip 'no network', 2 unless $ENV{TEST_WITH_NETWORK};
my $s = Zonemaster::LDNS->new( '8.8.8.8' );
isa_ok( $s, 'Zonemaster::LDNS' );
like( exception { $s->query( 'xx--example..', 'A' ) }, qr/Invalid domain name: xx--example../, 'Died on invalid name');
}
done_testing;

View File

@@ -0,0 +1,108 @@
use Test::More;
use Zonemaster::LDNS;
SKIP: {
skip 'no network', 20 unless $ENV{TEST_WITH_NETWORK};
my $r = Zonemaster::LDNS->new( '8.8.8.8' );
$r->recurse( 0 );
ok( !$r->recurse, 'recursive off' );
$r->recurse( 1 );
ok( $r->recurse, 'recursive on' );
$r->retrans( 17 );
is( $r->retrans, 17, 'retrans set' );
$r->retry( 17 );
is( $r->retry, 17, 'retry set' );
$r->debug( 1 );
ok( $r->debug, 'debug set' );
$r->debug( 0 );
ok( !$r->debug, 'debug unset' );
$r->dnssec( 1 );
ok( $r->dnssec, 'dnssec set' );
$r->dnssec( 0 );
ok( !$r->dnssec, 'dnssec unset' );
$r->cd( 1 );
ok( $r->cd, 'dnssec set' );
$r->cd( 0 );
ok( !$r->cd, 'dnssec unset' );
$r->usevc( 1 );
ok( $r->usevc, 'usevc set' );
$r->usevc( 0 );
ok( !$r->usevc, 'usevc unset' );
$r->igntc( 1 );
ok( $r->igntc, 'igntc set' );
$r->igntc( 0 );
ok( !$r->igntc, 'igntc unset' );
$r->edns_size( 4711 );
is($r->edns_size, 4711 , 'ENDS0 UDP size set');
$r->edns_size( 0 );
is($r->edns_size, 0 , 'ENDS0 UDP size set to zero');
is($r->timeout, 5, 'Expected default timeout');
$r->timeout(3.33);
ok(($r->timeout - 3.33) < 0.01, 'Expected set timeout');
my $addr = '192.0.2.1'; # Reserved RFC5737
ok($r->source($addr), "Source set.");
is($r->source, $addr, 'Source got.');
}
subtest 'recursion' => sub {
SKIP: {
skip 'no network', 3 unless $ENV{TEST_WITH_NETWORK};
my $r = Zonemaster::LDNS->new( '8.8.4.4' );
my $p1 = $r->query( 'www.iis.se' );
is( scalar($p1->answer), 1);
$r->recurse(0);
my $p2 = $r->query( 'www.nic.se' );
is( scalar($p2->answer), 0, 'Got a reply');
ok(!$p2->rd, 'RD flag set');
}
};
subtest 'global' => sub {
SKIP: {
skip 'no network', 3 unless $ENV{TEST_WITH_NETWORK};
my $res = new_ok( 'Zonemaster::LDNS' );
my $p = eval { $res->query( 'www.iis.se' ) } ;
if (not $p) {
diag $@;
}
else {
isa_ok( $p, 'Zonemaster::LDNS::Packet' );
isa_ok( $_, 'Zonemaster::LDNS::RR' ) for $p->answer;
}
}
};
# subtest 'sections' => sub {
# my $res = Zonemaster::LDNS->new( '194.146.106.22' );
# my $p = eval { $res->query( 'www.iis.se' ) };
# plan skip_all => 'No response, cannot test' if not $p;
#
# is( scalar( $p->answer ), 1, 'answer count in scalar context' );
# is( scalar( $p->authority ), 3, 'authority count in scalar context' );
# is( scalar( $p->additional ), 6, 'additional count in scalar context' );
# is( scalar( $p->question ), 1, 'question count in scalar context' );
# };
subtest 'broken' => sub {
my $b0rken = eval { Zonemaster::LDNS->new( 'gurksallad' ) };
ok( !$b0rken );
like( $@, qr/Failed to parse IP address: gurksallad/ );
};
done_testing;

540
zonemaster-ldns/t/rr.t Normal file
View File

@@ -0,0 +1,540 @@
use v5.16;
use Test::More;
use Test::Fatal qw(exception lives_ok);
use Devel::Peek;
use MIME::Base32 qw(encode_base32hex);
use MIME::Base64;
use Test::Differences;
BEGIN { use_ok( 'Zonemaster::LDNS' ) }
# The following tests use packets that are saved in a base64-encoded form. In
# order to save a packet in Base64 while showing its presentation format, just
# run the following script:
#
# $ perl -MZonemaster::LDNS -MMIME::Base64
# my $p = Zonemaster::LDNS->new("86.54.11.100")->query("iis.se", "SOA");
# say $p->string();
# say encode_base64($p->wireformat());
# ^D
subtest 'rdf' => sub {
# The packet below has the following equivalent presentation format:
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 19192
# ;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; iis.se. IN SOA
#
# ;; ANSWER SECTION:
# iis.se. 3600 IN SOA nsa.dnsnowhois.stagede.net. hostmaster.nic.se. (
# 1770736690 ; serial
# 14400 ; refresh (4 hours)
# 3600 ; retry (1 hour)
# 2592000 ; expire (4 weeks 2 days)
# 480 ; minimum (8 minutes)
# )
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
SviBgAABAAEAAAAAA2lpcwJzZQAABgABwAwABgABAAAOEABBA25zYQpkbnNub3dob2lzB3N0YWdl
ZGUDbmV0AApob3N0bWFzdGVyA25pY8AQaYtMMgAAOEAAAA4QACeNAAAAAeA=
DATA
foreach my $rr ( $p->answer ) {
is( $rr->rd_count, 7 );
foreach my $n (0..($rr->rd_count-1)) {
ok(length($rr->rdf($n)) >= 4);
}
like( exception { $rr->rdf(7) }, qr/Trying to fetch nonexistent RDATA at position/, 'died on overflow');
}
};
subtest 'SOA' => sub {
# The packet below has the following equivalent presentation format:
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 19192
# ;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; iis.se. IN SOA
#
# ;; ANSWER SECTION:
# iis.se. 3600 IN SOA nsa.dnsnowhois.stagede.net. hostmaster.nic.se. (
# 1770736690 ; serial
# 14400 ; refresh (4 hours)
# 3600 ; retry (1 hour)
# 2592000 ; expire (4 weeks 2 days)
# 480 ; minimum (8 minutes)
# )
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
SviBgAABAAEAAAAAA2lpcwJzZQAABgABwAwABgABAAAOEABBA25zYQpkbnNub3dob2lzB3N0YWdl
ZGUDbmV0AApob3N0bWFzdGVyA25pY8AQaYtMMgAAOEAAAA4QACeNAAAAAeA=
DATA
foreach my $rr ( $p->answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::SOA' );
is( lc($rr->mname), 'nsa.dnsnowhois.stagede.net.' );
is( lc($rr->rname), 'hostmaster.nic.se.' );
is( $rr->serial, 1770736690, 'serial' );
is( $rr->refresh, 14400, 'refresh' );
is( $rr->retry, 3600, 'retry' );
is( $rr->expire, 2592000, 'expire' );
is( $rr->minimum, 480, 'minimum' );
}
};
subtest 'A' => sub {
# The packet below has the following equivalent presentation format:
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 10728
# ;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; a.ns.se. IN A
#
# ;; ANSWER SECTION:
# a.ns.se. 86400 IN A 192.36.144.107
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
KeiBgAABAAEAAAAAAWECbnMCc2UAAAEAAcAMAAEAAQABUYAABMAkkGs
DATA
foreach my $rr ( $p->answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::A' );
is( $rr->address, '192.36.144.107', 'expected address string' );
is( $rr->type, 'A' );
is( length($rr->rdf(0)), 4 );
}
};
subtest 'AAAA' => sub {
# The packet below has the following equivalent presentation format:
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 35420
# ;; flags: qr rd ra ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; a.ns.se. IN AAAA
#
# ;; ANSWER SECTION:
# a.ns.se. 86400 IN AAAA 2a01:3f0:0:301::53
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
ilyBgAABAAEAAAAAAWECbnMCc2UAABwAAcAMABwAAQABUYAAECoBA/AAAAMBAAAAAAAAAFM=
DATA
foreach my $rr ( $p->answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::AAAA' );
is( $rr->address, '2a01:3f0:0:301::53', 'expected address string' );
is( length($rr->rdf(0)), 16 );
}
};
subtest 'TXT' => sub {
my @data = (
q{txt.test. 3600 IN TXT "Handling TXT RRs can be challenging"},
q{txt.test. 3600 IN TXT "because " "the data can " "be spl" "it up like " "this!"}
);
my @rrs = map { Zonemaster::LDNS::RR->new($_) } @data;
foreach my $rr ( @rrs ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::TXT' );
}
is( $rrs[0]->txtdata(), q{Handling TXT RRs can be challenging} );
is( $rrs[1]->txtdata(), q{because the data can be split up like this!} );
};
subtest 'DNSKEY' => sub {
subtest 'Good RR' => sub {
my @data = (
q{dnskey.test. 0 IN DNSKEY 257 3 8 BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=},
);
my @rrs = map { Zonemaster::LDNS::RR->new($_) } @data;
foreach my $rr ( @rrs ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::DNSKEY' );
}
is( $rrs[0]->flags(), q{257} );
is( $rrs[0]->protocol(), q{3} );
is( $rrs[0]->algorithm(), q{8} );
ok( $rrs[0]->keydata() );
is( $rrs[0]->hexkeydata(), q{BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=} );
is( $rrs[0]->keytag(), q{27018} );
is( $rrs[0]->ds('sha256'), Zonemaster::LDNS::RR->new(q{dnskey.test. 0 IN DS 27018 8 2 cd9b8881a72400a89b0f5ec4b096b07469aa3b316e870291d9e4e0f30f7dd4ed} ));
is( $rrs[0]->keysize(), q{344} );
};
subtest 'Empty RDATA' => sub {
my $data = decode_base64( "BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=");
my $p = Zonemaster::LDNS::Packet->new_from_wireformat( $data );
my ( $rr, @extra ) = $p->answer_unfiltered;
eq_or_diff \@extra, [], "no extra RRs found";
if ( !defined $rr ) {
BAIL_OUT( "no RR found" );
}
is $rr->keydata, "", "we're able to extract the public key field even when it's empty";
is $rr->hexkeydata, undef, "hexkeydata() returns undef when the public key field is empty";
is $rr->keysize, -1, "insufficient data to calculate key size is reported as -1";
my ( @rrs ) = $p->answer;
eq_or_diff \@rrs, [], "DNSKEY record with empty public key is filtered out by answer()";
};
};
subtest 'CDNSKEY' => sub {
subtest 'Good RR' => sub {
my @data = (
q{cdnskey.test. 0 IN CDNSKEY 257 3 8 BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=},
);
my @rrs = map { Zonemaster::LDNS::RR->new($_) } @data;
foreach my $rr ( @rrs ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::CDNSKEY' );
}
is( $rrs[0]->flags(), q{257} );
is( $rrs[0]->protocol(), q{3} );
is( $rrs[0]->algorithm(), q{8} );
ok( $rrs[0]->keydata() );
is( $rrs[0]->hexkeydata(), q{BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=} );
is( $rrs[0]->keytag(), q{0} ); # RR type not supported by LDNS
is( $rrs[0]->ds('sha256'), undef ); # RR type not supported by LDNS
is( $rrs[0]->keysize(), q{344} );
};
subtest 'Empty RDATA' => sub {
my $data = decode_base64( "BleFgAABAAEAAAAADW5sYWdyaWN1bHR1cmUCbmwAAAEAAcAMADAAAQAAAAAABAEBAwg=");
my $p = Zonemaster::LDNS::Packet->new_from_wireformat( $data );
my ( $rr, @extra ) = $p->answer_unfiltered;
eq_or_diff \@extra, [], "no extra RRs found";
if ( !defined $rr ) {
BAIL_OUT( "no RR found" );
}
is $rr->keydata, "", "we're able to extract the public key field even when it's empty";
is $rr->hexkeydata, undef, "hexkeydata() returns undef when the public key field is empty";
is $rr->keysize, -1, "insufficient data to calculate key size is reported as -1";
my ( @rrs ) = $p->answer;
eq_or_diff \@rrs, [], "CDNSKEY record with empty public key is filtered out by answer()";
};
};
subtest 'RRSIG' => sub {
# The packet below has the following equivalent presentation format.
# Note however that this was obtained by querying 192.36.144.107 directly
# instead of going through a public resolver.
#
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 39105
# ;; flags: qr aa rd; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1
# ;; WARNING: recursion requested but not available
#
# ;; OPT PSEUDOSECTION:
# ; EDNS: version: 0, flags:; udp: 1232
# ;; QUESTION SECTION:
# ;se. IN RRSIG
#
# ;; ANSWER SECTION:
# se. 172800 IN RRSIG SOA 8 1 172800 (
# 20260226065448 20260212052448 65293 se.
# [omitted] )
# se. 172800 IN RRSIG NS 8 1 172800 (
# 20260225210943 20260211193943 65293 se.
# [omitted] )
# se. 172800 IN RRSIG TXT 8 1 172800 (
# 20260226065448 20260212052448 65293 se.
# [omitted] )
# se. 7200 IN RRSIG NSEC 8 1 7200 (
# 20260225210943 20260211193943 65293 se.
# [omitted] )
# se. 3600 IN RRSIG DNSKEY 8 1 3600 (
# 20260225210943 20260211193943 59407 se.
# [omitted] )
# se. 172800 IN RRSIG ZONEMD 8 1 172800 (
# 20260226065448 20260212052448 65293 se.
# [omitted] )
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
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
AAAAAA==
DATA
foreach my $rr ( $p->answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::RRSIG' );
is( $rr->signer, 'se.' );
is( $rr->labels, 1 );
if ( $rr->typecovered eq 'DNSKEY' ) {
ok( $rr->keytag == 59407 );
}
}
};
subtest 'NSEC' => sub {
# The packet below has the following equivalent presentation format.
# Note however that this was obtained by querying 192.36.144.107 directly
# instead of going through a public resolver.
#
# ;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 54748
# ;; flags: qr aa rd ; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
# ;; QUESTION SECTION:
# ;; se. IN NSEC
#
# ;; ANSWER SECTION:
# se. 7200 IN NSEC 0.se. NS SOA TXT RRSIG NSEC DNSKEY ZONEMD
my $p = Zonemaster::LDNS::Packet->new_from_wireformat(decode_base64(<<DATA));
1dyFAAABAAEAAAAAAnNlAAAvAAHADAAvAAEAABwgABABMAJzZQAACCIAgAAAA4AB
DATA
foreach my $rr ( $p->answer ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::NSEC' );
ok( $rr->typehref->{TXT} );
ok( !$rr->typehref->{MX} );
ok( $rr->typehref->{TXT} );
is( $rr->typelist, 'NS SOA TXT RRSIG NSEC DNSKEY ZONEMD ' );
}
};
subtest 'From string' => sub {
my $made = Zonemaster::LDNS::RR->new_from_string( 'nic.se IN NS a.ns.se' );
isa_ok( $made, 'Zonemaster::LDNS::RR::NS' );
my $made2 = Zonemaster::LDNS::RR->new_from_string( 'nic.se IN NS a.ns.se' );
is( $made->compare( $made2 ), 0, 'direct comparison works' );
my $made3 = Zonemaster::LDNS::RR->new_from_string( 'mic.se IN NS a.ns.se' );
my $made4 = Zonemaster::LDNS::RR->new_from_string( 'oic.se IN NS a.ns.se' );
is( $made->compare( $made3 ), 1, 'direct comparison works' );
is( $made->compare( $made4 ), -1, 'direct comparison works' );
is( $made eq $made2, 1, 'indirect comparison works' );
is( $made <=> $made3, 1, 'indirect comparison works' );
is( $made cmp $made4, -1, 'indirect comparison works' );
is( "$made", "nic.se. 3600 IN NS a.ns.se." );
};
subtest 'DS' => sub {
subtest 'Good RR' => sub {
my @data = (
q{nic.se 3600 IN DS 22643 13 2 aa0b38f6755c2777992a74935d50a2a3480effef1a60bf8643d12c307465c9da},
);
my @rrs = map { Zonemaster::LDNS::RR->new($_) } @data;
foreach my $rr ( @rrs ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::DS' );
}
my $key = Zonemaster::LDNS::RR->new( 'nic.se IN DNSKEY 257 3 13 lkpZSlU70pd1LHrXqZttOAYKmX046YqYQg1aQJsv1y0xKr+qJS+3Ue1tM5VCYPU3lKuzq93nz0Lm/AV9jeoumQ==' );
my $other = Zonemaster::LDNS::RR->new( 'nic.se IN NS a.ns.se' );
is( $rrs[0]->keytag(), q{22643} );
is( $rrs[0]->algorithm(), q{13} );
is( $rrs[0]->digtype(), q{2} );
ok( $rrs[0]->digest() );
is( $rrs[0]->hexdigest(), q{aa0b38f6755c2777992a74935d50a2a3480effef1a60bf8643d12c307465c9da} );
ok( $rrs[0]->verify( $key ) );
ok( !$rrs[0]->verify( $other ) );
};
};
subtest 'CDS' => sub {
subtest 'Good RR' => sub {
my @data = (
q{nic.se 3600 IN CDS 22643 13 2 aa0b38f6755c2777992a74935d50a2a3480effef1a60bf8643d12c307465c9da},
);
my @rrs = map { Zonemaster::LDNS::RR->new($_) } @data;
foreach my $rr ( @rrs ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR::CDS' );
}
my $key = Zonemaster::LDNS::RR->new( 'nic.se IN CDNSKEY 257 3 13 lkpZSlU70pd1LHrXqZttOAYKmX046YqYQg1aQJsv1y0xKr+qJS+3Ue1tM5VCYPU3lKuzq93nz0Lm/AV9jeoumQ==' );
my $other = Zonemaster::LDNS::RR->new( 'nic.se IN NS a.ns.se' );
is( $rrs[0]->keytag(), q{22643} );
is( $rrs[0]->algorithm(), q{13} );
is( $rrs[0]->digtype(), q{2} );
ok( $rrs[0]->digest() );
is( $rrs[0]->hexdigest(), q{aa0b38f6755c2777992a74935d50a2a3480effef1a60bf8643d12c307465c9da} );
ok( !$rrs[0]->verify( $key ) ); # RR type not supported by LDNS
ok( !$rrs[0]->verify( $other ) );
};
};
subtest 'NSEC3 without salt' => sub {
my $name = 'com';
my $nsec3 = Zonemaster::LDNS::RR->new_from_string(
'VD0J8N54V788IUBJL9CN5MUD416BS5I6.com. 86400 IN NSEC3 1 1 0 - VD0N3HDL5MG940MOUBCF5MNLKGDT9RFT NS DS RRSIG' );
isa_ok( $nsec3, 'Zonemaster::LDNS::RR::NSEC3' );
is( $nsec3->algorithm, 1 );
is( $nsec3->flags, 1 );
ok( $nsec3->optout );
is( $nsec3->iterations, 0 );
is( $nsec3->salt, '' );
is( encode_base32hex( $nsec3->next_owner ), "VD0N3HDL5MG940MOUBCF5MNLKGDT9RFT" );
is( $nsec3->typelist, 'NS DS RRSIG ' );
eq_or_diff( $nsec3->hash_name( $name ), 'ck0pojmg874ljref7efn8430qvit8bsm' );
is_deeply( [ sort keys %{ $nsec3->typehref } ], [qw(DS NS RRSIG)] );
};
subtest 'NSEC3 with salt' => sub {
my $name = 'bad-values.dnssec03.xa';
my $nsec3 = Zonemaster::LDNS::RR->new_from_string(
'BP7OICBR09FICEULBF46U8DMJ1J1V8R3.bad-values.dnssec03.xa. 900 IN NSEC3 1 1 3 8104 c91qe244nd0q5qh3jln35a809mik8d39 A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM' );
isa_ok( $nsec3, 'Zonemaster::LDNS::RR::NSEC3' );
is( $nsec3->algorithm, 1 );
is( $nsec3->flags, 1 );
ok( $nsec3->optout );
is( $nsec3->iterations, 3 );
is( unpack('H*', $nsec3->salt), '8104' );
is( encode_base32hex( $nsec3->next_owner ), "C91QE244ND0Q5QH3JLN35A809MIK8D39" );
is( $nsec3->typelist, 'A NS SOA MX TXT RRSIG DNSKEY NSEC3PARAM ' );
eq_or_diff( $nsec3->hash_name( $name ), 'u6sj7nlrc80gcqem0ip18mji3vk60djt' );
is_deeply( [ sort keys %{ $nsec3->typehref } ], [qw(A DNSKEY MX NS NSEC3PARAM RRSIG SOA TXT)] );
};
subtest 'NSEC3 with unknown algorithm' => sub {
my $name = 'nsec3-mismatches-apex-1.dnssec10.xa.';
my $nsec3 = Zonemaster::LDNS::RR->new_from_string(
'G4CPF6T01H8B5U5O996K8HI4OTEE12AA.nsec3-mismatches-apex-1.dnssec10.xa. 86400 IN NSEC3 3 0 0 - UP848IGD2MT1JGD0ISJEB6LAS1DCB11R NS SOA RRSIG DNSKEY NSEC3PARAM TYPE65534' );
isa_ok( $nsec3, 'Zonemaster::LDNS::RR::NSEC3' );
is( $nsec3->algorithm, 3 );
is( $nsec3->flags, 0 );
is( $nsec3->optout, '' );
is( $nsec3->iterations, 0 );
is( unpack('H*', $nsec3->salt), '' );
is( encode_base32hex( $nsec3->next_owner ), "UP848IGD2MT1JGD0ISJEB6LAS1DCB11R" );
is( $nsec3->typelist, 'NS SOA RRSIG DNSKEY NSEC3PARAM TYPE65534 ' );
eq_or_diff( $nsec3->hash_name( $name ), undef );
is_deeply( [ sort keys %{ $nsec3->typehref } ], [qw(DNSKEY NS NSEC3PARAM RRSIG SOA TYPE65534)] );
};
subtest 'NSEC3PARAM without salt and non-zero flags' => sub {
my $name = 'empty-nsec3param.example';
my $nsec3param = Zonemaster::LDNS::RR->new_from_string(
'empty-nsec3param.example. 86400 IN NSEC3PARAM 1 165 0 -' );
isa_ok( $nsec3param, 'Zonemaster::LDNS::RR::NSEC3PARAM' );
is( $nsec3param->algorithm, 1 );
is( $nsec3param->flags, 0xA5 );
is( $nsec3param->iterations, 0 );
is( $nsec3param->salt, '', 'Salt');
is( lc($nsec3param->owner), 'empty-nsec3param.example.' );
eq_or_diff( $nsec3param->hash_name( $name ), 'l73q01jb3imjq6krmm5h00evfsdpmbvl' );
};
subtest 'NSEC3PARAM with salt' => sub {
my $name = 'whitehouse.gov.';
my $nsec3param = Zonemaster::LDNS::RR->new_from_string( 'whitehouse.gov. 3600 IN NSEC3PARAM 1 0 2 B2C19AB526819347' );
isa_ok( $nsec3param, 'Zonemaster::LDNS::RR::NSEC3PARAM' );
is( $nsec3param->algorithm, 1 );
is( $nsec3param->flags, 0 );
is( $nsec3param->iterations, 2, "Iterations" );
is( uc(unpack( 'H*', $nsec3param->salt )), 'B2C19AB526819347', "Salt" );
is( lc($nsec3param->owner), 'whitehouse.gov.' );
eq_or_diff( $nsec3param->hash_name( $name ), '2mo42ugf34bnruvljv91vv9vqd4kckda' );
};
subtest 'NSEC3PARAM with unknown algorithm' => sub {
my $name = 'GOOD-NSEC3-1.dnssec10.xa';
my $nsec3param = Zonemaster::LDNS::RR->new_from_string( 'good-nsec3-1.dnssec10.xa. 0 IN NSEC3PARAM 3 0 0 -' );
isa_ok( $nsec3param, 'Zonemaster::LDNS::RR::NSEC3PARAM' );
is( $nsec3param->algorithm, 3 );
is( $nsec3param->flags, 0 );
is( $nsec3param->iterations, 0 );
is( unpack('H*', $nsec3param->salt), '' );
is( lc($nsec3param->owner), 'good-nsec3-1.dnssec10.xa.' );
eq_or_diff( $nsec3param->hash_name( $name ), undef );
};
subtest 'SIG' => sub {
my $sig = Zonemaster::LDNS::RR->new_from_string('sig.example. 3600 IN SIG A 1 2 3600 19970102030405 19961211100908 2143 sig.example. AIYADP8d3zYNyQwW2EM4wXVFdslEJcUx/fxkfBeH1El4ixPFhpfHFElxbvKoWmvjDTCmfiYy2X+8XpFjwICHc398kzWsTMKlxovpz2FnCTM=');
isa_ok( $sig, 'Zonemaster::LDNS::RR::SIG' );
can_ok( 'Zonemaster::LDNS::RR::SIG', qw(check_rd_count) );
is( $sig->check_rd_count(), '1' );
};
subtest 'SRV' => sub {
my $srv = Zonemaster::LDNS::RR->new( '_nicname._tcp.se. 172800 IN SRV 0 0 43 whois.nic-se.se.' );
is( $srv->type, 'SRV' );
};
subtest 'SPF' => sub {
my @data = (
q{frobbit.se. 1127 IN SPF "v=spf1 ip4:85.30.129.185/24 mx:mail.frobbit.se ip6:2a02:80:3ffe::0/64 ~all"},
q{spf.example. 3600 IN SPF "v=spf1 " "ip4:192.0.2.25/24 " "mx:mail.spf.example " "ip6:2001:db8::25/64 -all"}
);
my @rr = map { Zonemaster::LDNS::RR->new($_) } @data;
for my $spf (@rr) {
isa_ok( $spf, 'Zonemaster::LDNS::RR::SPF' );
}
is( $rr[0]->spfdata(), 'v=spf1 ip4:85.30.129.185/24 mx:mail.frobbit.se ip6:2a02:80:3ffe::0/64 ~all' );
is( $rr[1]->spfdata(), 'v=spf1 ip4:192.0.2.25/24 mx:mail.spf.example ip6:2001:db8::25/64 -all' );
};
subtest 'DNAME' => sub {
my $rr = Zonemaster::LDNS::RR->new( 'examplë.fake 3600 IN DNAME example.fake' );
isa_ok( $rr, 'Zonemaster::LDNS::RR::DNAME' );
is($rr->dname(), 'example.fake.');
};
subtest 'SVCB' => sub {
my $rr = Zonemaster::LDNS::RR->new( q{_8443._foo.api.example.com. 7200 IN SVCB 0 svc4.example.net.} );
isa_ok( $rr, 'Zonemaster::LDNS::RR::SVCB' );
lives_ok { $rr->check_rd_count() } '$rr->check_rd_count() does not crash';
};
subtest 'HTTPS' => sub {
my $rr = Zonemaster::LDNS::RR->new( q{example.com. 3600 IN HTTPS 0 svc.example.net.} );
isa_ok( $rr, 'Zonemaster::LDNS::RR::HTTPS' );
lives_ok { $rr->check_rd_count() } '$rr->check_rd_count() does not crash';
};
subtest 'generic type' => sub {
my $rr = Zonemaster::LDNS::RR->new( q{type1234.example. 3600 IN TYPE1234 \# 4 ABCDABCD} );
isa_ok( $rr, 'Zonemaster::LDNS::RR' );
lives_ok { $rr->check_rd_count() } '$rr->check_rd_count() does not crash';
};
subtest 'croak when given malformed CAA records' => sub {
my $will_croak = sub {
# This will croak if LDNS.xs is compiled with -DUSE_ITHREADS
my $bad_caa = Zonemaster::LDNS::RR->new(
'bad-caa.example. 3600 IN CAA \# 4 C0000202' );
# This will always croak
$bad_caa->string();
};
like( exception(sub { $will_croak->() }), qr/^Failed to convert RR to string/ );
};
done_testing;

125
zonemaster-ldns/t/rrlist.t Normal file
View File

@@ -0,0 +1,125 @@
use strict;
use warnings;
use Test::More;
use Test::Exception;
use Test::Differences;
use Zonemaster::LDNS;
subtest "Empty RRList" => sub {
my $empty_impl = Zonemaster::LDNS::RRList->new();
my $empty_expl = Zonemaster::LDNS::RRList->new([]);
my $singleton = Zonemaster::LDNS::RRList->new([
Zonemaster::LDNS::RR->new_from_string('test. 0 IN TXT "hello"')
]);
isa_ok($empty_impl, 'Zonemaster::LDNS::RRList');
isa_ok($empty_expl, 'Zonemaster::LDNS::RRList');
isa_ok($singleton, 'Zonemaster::LDNS::RRList');
# We really want to make sure that new() and new([]) have the same
# semantics.
ok( $empty_impl eq $empty_expl, "two distinct empty RRLists are equal to each other" );
ok( $empty_expl eq $empty_impl, "eq on two empty lists is commutative" );
eq_or_diff( $empty_impl->string, '', "stringifying an implicitly empty list gives empty string" );
eq_or_diff( $empty_expl->string, '', "stringifying an explicitly empty list gives empty string" );
ok( $empty_impl ne $singleton, "the implicitly empty list isnt equal to a non-empty one" );
ok( $empty_expl ne $singleton, "the explicitly empty list isnt equal to a non-empty one" );
$singleton->pop();
ok( $empty_impl eq $singleton, "now both lists are empty" );
is( $empty_impl->count(), 0, "count() on implicitly empty list is 0" );
is( $empty_expl->count(), 0, "count() on explicitly empty list is 0" );
is( $empty_impl->get(0), undef, "get(0) on empty list gives undef" );
is( $empty_impl->get(42), undef, "get(42) on empty list also gives undef" );
ok( !$empty_impl->is_rrset(), "an empty list is not an RRset" );
ok( !$empty_expl->is_rrset(), "an empty list is not an RRset" );
};
subtest "Good RRList" => sub {
my $rr1 = Zonemaster::LDNS::RR->new_from_string( 'example. 10 IN NS ns1.example.' );
my $rr2 = Zonemaster::LDNS::RR->new_from_string( 'example. 10 IN NS ns2.example.' );
my $rrlist = Zonemaster::LDNS::RRList->new( [ $rr1, $rr2 ] );
is( $rrlist->count, 2, 'Two in RRList' );
eq_or_diff $rrlist->string,
"example.\t10\tIN\tNS\tns1.example.\nexample.\t10\tIN\tNS\tns2.example.",
'RRList string match';
isa_ok( $rrlist->get(0), 'Zonemaster::LDNS::RR');
isa_ok( $rrlist->get(0), 'Zonemaster::LDNS::RR::NS');
my $rrlist_reversed = Zonemaster::LDNS::RRList->new( [ $rr2, $rr1 ] );
ok( $rrlist eq $rrlist_reversed, 'Equal RRLists' );
subtest "RRset" => sub {
subtest "Same TTL and owner name" => sub {
ok( $rrlist->is_rrset, 'Is a RRset with same TTL and owner name' );
};
subtest "Different CLASS" => sub {
my $rr3 = Zonemaster::LDNS::RR->new_from_string( 'example. 10 CH NS ns3.example.' );
my $rrlist2 = Zonemaster::LDNS::RRList->new( [ $rr1, $rr3 ] );
ok( !$rrlist2->is_rrset, 'Is not a RRset with different CLASS' );
};
subtest "Different TYPE" => sub {
my $rr3 = Zonemaster::LDNS::RR->new_from_string( 'example. 10 IN TXT ns3.example.' );
my $rrlist2 = Zonemaster::LDNS::RRList->new( [ $rr1, $rr3 ] );
ok( !$rrlist2->is_rrset, 'Is not a RRset with different TYPE' );
};
SKIP: {
# Skipped due to a bug in LDNS, see https://github.com/NLnetLabs/ldns/pull/251
skip "Further is_rrset() testing disabled due to an issue in LDNS", 1;
subtest "Different TTL" => sub {
my $rr3 = Zonemaster::LDNS::RR->new_from_string( 'example. 20 IN NS ns3.example.' );
my $rrlist2 = Zonemaster::LDNS::RRList->new( [ $rr1, $rr3 ] );
ok( !$rrlist2->is_rrset, 'Is not a RRset with different TTL' );
};
subtest "Case varying owner name" => sub {
my $rr3 = Zonemaster::LDNS::RR->new_from_string( 'eXamPle. 20 IN NS ns3.example.' );
my $rrlist2 = Zonemaster::LDNS::RRList->new( [ $rr2, $rr3 ] );
ok( $rrlist2->is_rrset, 'Is a RRset with case varying owner names' );
};
}
};
my $rr3 = Zonemaster::LDNS::RR->new_from_string( 'example. 10 IN A 127.0.0.1' );
ok( $rrlist->push( $rr3 ), 'Push OK' );
is( $rrlist->count, 3, 'Three RRs in RRList' );
isa_ok( $rrlist->get(2), 'Zonemaster::LDNS::RR');
isa_ok( $rrlist->get(2), 'Zonemaster::LDNS::RR::A');
is( $rrlist->get(3), undef, 'No RR here');
while ( my $rr = $rrlist->pop ) {
isa_ok( $rr, 'Zonemaster::LDNS::RR' );
}
is( $rrlist->count, 0, 'Zero RRs in RRList' );
ok( !$rrlist->is_rrset, 'Is not a RRset' );
is( $rrlist->get(0), undef, 'No RR here');
};
subtest "Bad RRList" => sub {
my $rr1 = 'example. IN NS ns1.example.';
my $rr2 = 'example. IN NS ns2.example.';
my @rrs = ( $rr1, $rr2 );
throws_ok { Zonemaster::LDNS::RRList->new( \@rrs ) } qr/Incorrect type in list/, 'crashes on incorrect type';
};
done_testing;

View File

@@ -0,0 +1,18 @@
use Test::More;
use JSON::PP;
use_ok( 'Zonemaster::LDNS' );
my $p = Zonemaster::LDNS::Packet->new( 'www.iis.se', 'A', 'IN' );
$p->answerfrom( '127.0.0.1' );
$p->timestamp( '1384423749.28615' );
my $json = JSON::PP->new->allow_blessed->convert_blessed;
my $ref = decode_json $json->encode( $p );
is( $ref->{'Zonemaster::LDNS::Packet'}{answerfrom}, '127.0.0.1' );
my $decode = JSON::PP->new->filter_json_single_key_object(
'Zonemaster::LDNS::Packet' => sub { is $_[0]->{answerfrom}, '127.0.0.1'; return; } );
$decode->decode( $json->encode( $p ) );
done_testing;

22
zonemaster-ldns/t/utils.t Normal file
View File

@@ -0,0 +1,22 @@
use Test::More;
BEGIN { use_ok( 'Zonemaster::LDNS' ) }
SKIP: {
skip 'no network', 5 unless $ENV{TEST_WITH_NETWORK};
my $res = new_ok( 'Zonemaster::LDNS', ['8.8.4.4'] );
my @addrs = sort $res->name2addr( 'b.ns.se' );
my $count = $res->name2addr( 'b.ns.se' );
is_deeply( \@addrs, [ "192.36.133.107", "2001:67c:254c:301::53" ], 'expected addresses' );
is( $count, 2, 'expected count' );
my @names = sort $res->addr2name( '8.8.8.8' );
$count = $res->addr2name( '8.8.8.8' );
is_deeply( [map {lc($_)} @names], ['dns.google.'], 'expected names' );
is( $count, 1, 'expected name count' );
}
done_testing;

View File

@@ -0,0 +1,66 @@
use strict;
use warnings;
use Test::More;
BEGIN { use_ok('Zonemaster::LDNS') }
=head2 wireformat_ok( string )
Test that the given resource record stays the same when serialized and deserialized
through wireformat using Zonemaster::LDNS::Packet::wireformat() and
Zonemaster::LDNS::Packet::new_from_wireformat().
The initial resource record is constructed from the given B<string> using
Zonemaster::LDNS::RR::new().
=cut
sub wireformat_ok {
my $string = shift;
my $rr1 = Zonemaster::LDNS::RR->new($string);
my $packet1 = Zonemaster::LDNS::Packet->new('example.com.');
$packet1->unique_push( 'answer', $rr1 );
my $wireformat = $packet1->wireformat();
my $packet2 = Zonemaster::LDNS::Packet->new_from_wireformat($wireformat);
my $rr2 = ( $packet2->answer )[0];
cmp_ok $rr1, 'eq', $rr2, "Wireformat round-trip for: " . $string;
return;
}
# Test wireformat round trips for different record types
wireformat_ok('example.com. A 192.0.2.1');
wireformat_ok('example.com. AAAA 2001:db8::3');
wireformat_ok('abc.example.com. AFSDB 1 afs-server.example.com.');
wireformat_ok('example.com. CAA 0 issue "ca.example.net; account=123456"');
wireformat_ok('smith CERT PGP 0 0 aNvv4w==');
wireformat_ok('example.com. CNAME joe.example.com.');
wireformat_ok('example.com. DNAME example.net.');
wireformat_ok('example.com. DNSKEY 256 3 5 742iU/TpPSEDhm2SNKLijfUppn1U aNvv4w==');
wireformat_ok('example. DS 12345 3 1 123456789abcdef67890123456789abcdef67890');
wireformat_ok('example.com. HINFO PC-Intel-700mhz "Redhat Linux 7.1"');
wireformat_ok('geo.example.com. LOC 42 21 43.528 N 71 05 06.284 W 12m');
wireformat_ok('example.com. MX 10 mail.example.com.');
wireformat_ok('example.com. NAPTR 100 10 u sip+E2U !^.*$!sip:info@info.example.test!i .');
wireformat_ok('example.com. NS ns1.example.com.');
wireformat_ok('example.com. NSEC aaa.example.com. NS SOA RRSIG NSEC DNSKEY');
wireformat_ok('example. NSEC3 1 1 12 aabbccdd 2vptu5timamqttgl4luu9kg21e0aor3s A RRSIG');
wireformat_ok('example.com. NSEC3PARAM 1 0 1 B606B568');
wireformat_ok('2.2.0.192.in-addr.arpa. PTR www.example.com.');
wireformat_ok('my.example.com. RP who.example.com txtrec.example.com');
wireformat_ok('www.example.com. RRSIG AAAA 5 3 60 20171006161502 20170926161502 25665 example.com. khOgZGrdkaggUfmZbOFjZLXWZsA8 u+Y=');
wireformat_ok('example.com. SOA ns1.example.com. hostmaster.example.com. 2003080800 172800 900 1209600 3600');
wireformat_ok('example.com. SPF 10 5 80 hostname.example.com');
wireformat_ok('_http._tcp.example.com. SRV 0 5 80 www.example.com.');
wireformat_ok('random.example.com. SSHFP 1 1 23D3C516AAF4C8E867D0A2968B2EB999 B3168216');
wireformat_ok('example.com. TLSA 3 1 1 d2abde240d7cd3ee6b4b28c54df034b9 7983a1d16e8a410e4561cb106618e971');
wireformat_ok('example.com. TXT "system manager: jdoe@example.com"');
wireformat_ok('host.example.com. WKS 192.0.2.3 TCP (ftp smtp telnet)');
done_testing;