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

View File

@@ -0,0 +1,336 @@
use strict;
use warnings;
use Test::More;
use Test::Exception;
BEGIN { use_ok( 'Zonemaster::Engine::Nameserver' ); }
use Zonemaster::Engine;
use Zonemaster::Engine::Util;
use Zonemaster::Engine::Constants qw( :misc );
my $datafile = 't/nameserver.data';
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 );
}
my $nsv6 = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'ns.nic.se', address => '2001:67c:124c:100a::45' } ] );
my $nsv4 = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'ns.nic.se', address => '91.226.36.45' } ] );
eval { Zonemaster::Engine::Nameserver->new( { name => 'dummy' } ); };
like( $@, qr/Attribute \(address\) is required/, 'create fails without address.' );
isa_ok( $nsv6->address, 'Net::IP::XS' );
isa_ok( $nsv6->name, 'Zonemaster::Engine::DNSName' );
is( $nsv6->dns->retry, 2 );
my $p1 = $nsv6->query( 'iis.se', 'SOA' );
my $p2 = $nsv6->query( 'iis.se', 'SOA', { dnssec => 1 } );
my $p3 = $nsv6->query( 'iis.se', 'SOA', { dnssec => 1 } );
my $p4 = $nsv4->query( 'iis.se', 'SOA', { dnssec => 1 } );
isa_ok( $p1, 'Zonemaster::Engine::Packet' );
isa_ok( $p2, 'Zonemaster::Engine::Packet' );
my ( $soa ) = grep { $_->type eq 'SOA' } $p1->answer;
is( scalar( $p1->answer ), 1, 'one answer RR present ' );
ok( $soa, 'it is a SOA RR' );
is( lc( $soa->rname ), 'hostmaster.nic.se.', 'RNAME has expected format' );
is( scalar( grep { $_->type eq 'SOA' or $_->type eq 'RRSIG' } $p2->answer ), 2, 'SOA and RRSIG RRs present' );
ok( $p3 eq $p2, 'Same packet object returned' );
ok( $p3 ne $p4, 'Same packet object not returned from other server' );
ok( $p3 ne $p1, 'Same packet object not returned with other flag' );
my $nscopy = Zonemaster::Engine->ns( 'ns.nic.se.', '2001:67c:124c:100a:0000::45' );
ok( $nsv6 eq $nscopy, 'Same nameserver object returned' );
my $nssame = Zonemaster::Engine->ns( 'foo.example.org', '2001:67c:124c:100a:0000::45' );
ok(
( $nssame ne $nsv6 and $nssame->cache eq $nsv6->cache ),
'Different name, same IP are different but has same cache'
);
SKIP: {
skip '/tmp not writable', 2 unless -w '/tmp';
my $name = "/tmp/nameserver_test_$$";
Zonemaster::Engine::Nameserver->save( $name );
my $count = keys %Zonemaster::Engine::Nameserver::object_cache;
undef %Zonemaster::Engine::Nameserver::object_cache;
is( scalar( keys %Zonemaster::Engine::Nameserver::object_cache ), 0, 'Nameserver cache is empty after clear.' );
Zonemaster::Engine::Nameserver->restore( $name );
is( scalar( keys %Zonemaster::Engine::Nameserver::object_cache ),
$count, 'Same number of top-level keys in cache after restore.' );
unlink $name;
}
my $broken = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'ns.not.existing', address => '192.0.2.17' } ] );
my $p = $broken->query( 'www.iis.se' );
ok( !$p, 'no response from broken server' );
my $googlens = ns( 'ns1.google.com', '216.239.32.10' );
my $save = Zonemaster::Engine::Profile->effective->get( q{no_network} );
Zonemaster::Engine::Profile->effective->set( q{no_network}, 1 );
delete( $googlens->cache->{'www.google.com'} );
eval { $googlens->query( 'www.google.com', 'TXT' ) };
like( $@,
qr{External query for www.google.com, TXT attempted to ns1.google.com/216.239.32.10 while running with no_network}
);
Zonemaster::Engine::Profile->effective->set( q{no_network}, $save );
@{ $nsv6->times } = ( qw[2 4 4 4 5 5 7 9] );
is( $nsv6->stddev_time, 2, 'known value check' );
is( $nsv6->average_time, 5 );
is( $nsv6->median_time, 4.5 );
is( $nsv6->max_time, 9 );
is( $nsv6->min_time, 2 );
@{ $nsv6->times } = ( qw[2 4 4 4 5 5 7] );
is( $nsv6->median_time, 4 );
foreach my $ns ( Zonemaster::Engine::Nameserver->all_known_nameservers ) {
isa_ok( $ns, 'Zonemaster::Engine::Nameserver' );
}
ok( scalar( keys %Zonemaster::Engine::Nameserver::Cache::object_cache ) >= 4 );
Zonemaster::Engine::Profile->effective->set( q{net.ipv4}, 0 );
Zonemaster::Engine::Profile->effective->set( q{net.ipv6}, 0 );
my $p5 = $nsv6->query( 'iis.se', 'SOA', { dnssec => 1 } );
my $p6 = $nsv4->query( 'iis.se', 'SOA', { dnssec => 1 } );
ok( !defined( $p5 ), 'IPv4 blocked' );
ok( !defined( $p6 ), 'IPv6 blocked' );
Zonemaster::Engine::Profile->effective->set( q{net.ipv4}, 1 );
Zonemaster::Engine::Profile->effective->set( q{net.ipv6}, 1 );
$p5 = $nsv6->query( 'iis.se', 'SOA', { dnssec => 1 } );
$p6 = $nsv4->query( 'iis.se', 'SOA', { dnssec => 1 } );
ok( defined( $p5 ), 'IPv4 not blocked' );
ok( defined( $p6 ), 'IPv6 not blocked' );
is( $p5->edns_size, 4096, 'EDNS0 size' );
is( $p5->edns_rcode, 0, 'EDNS0 rcode' );
$p5->unique_push( 'additional', Zonemaster::LDNS::RR->new( 'www.iis.se. 26 IN A 91.226.36.46' ) );
my ( $rr ) = $p5->additional;
isa_ok( $rr, 'Zonemaster::LDNS::RR::A' );
$nsv4->add_fake_ds( 'iis.se' => [ { keytag => 16696, algorithm => 5, type => 1, digest => 'DEADBEEF' } ] );
ok( $nsv4->fake_ds->{'iis.se'}, 'Fake DS data added' );
my $p7 = $nsv4->query( 'iis.se', 'DS', { class => 'IN' } );
isa_ok( $p7, 'Zonemaster::Engine::Packet' );
my ( $dsrr ) = $p7->answer;
isa_ok( $dsrr, 'Zonemaster::LDNS::RR::DS' );
is( $dsrr->keytag, 16696, 'Expected keytag' );
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' );
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";
dies_ok { $p = $ns->query( 'fr', 'SOA', { "edns_size" => -1 } ); } "dies when edns_size is lower than 0";
dies_ok { $p = $ns->query( 'fr', 'SOA', { "edns_details" => { "size" => -1 } } ); } "dies when edns_size (set with edns_details->size) is lower than 0";
};
Zonemaster::Engine::Profile->effective->set( q{resolver.source4}, q{127.0.0.1} );
my $ns_test = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'ns.nic.se', address => '212.247.7.228' } ] );
is($ns_test->dns->source, '127.0.0.1', 'Source IPv4 address set.');
Zonemaster::Engine::Profile->effective->set( q{resolver.source6}, q{::1} );
$ns_test = new_ok( 'Zonemaster::Engine::Nameserver' => [ { name => 'ns.nic.se', address => '2001:67c:124c:100a::45' } ] );
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 );
# 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' );
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' );
if ( $ENV{ZONEMASTER_RECORD} ) {
Zonemaster::Engine::Nameserver->save( $datafile );
}
done_testing;