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:
486
zonemaster-engine/lib/Zonemaster/Engine/Zone.pm
Normal file
486
zonemaster-engine/lib/Zonemaster/Engine/Zone.pm
Normal file
@@ -0,0 +1,486 @@
|
||||
package Zonemaster::Engine::Zone;
|
||||
|
||||
use v5.16.0;
|
||||
use warnings;
|
||||
|
||||
use version; our $VERSION = version->declare("v1.1.9");
|
||||
|
||||
use Carp qw( confess croak );
|
||||
use List::MoreUtils qw[uniq];
|
||||
|
||||
use Zonemaster::Engine::DNSName;
|
||||
use Zonemaster::Engine::Recursor;
|
||||
use Zonemaster::Engine::NSArray;
|
||||
use Zonemaster::Engine::Constants qw[:ip];
|
||||
|
||||
sub new {
|
||||
my ( $class, $attrs ) = @_;
|
||||
|
||||
my $name = delete $attrs->{name} // confess "required argument 'name' not found";
|
||||
if ( %$attrs ) {
|
||||
confess "unexpected arguments: " . join ', ', sort keys %$attrs;
|
||||
}
|
||||
|
||||
if ( blessed $name ne 'Zonemaster::Engine::DNSName' ) {
|
||||
confess "argument 'name' must be a Zonemaster::Engine::DNSName";
|
||||
}
|
||||
|
||||
my $obj = { _name => $name };
|
||||
|
||||
return bless $obj, $class;
|
||||
}
|
||||
|
||||
sub name {
|
||||
my ( $self ) = @_;
|
||||
|
||||
return $self->{_name};
|
||||
}
|
||||
|
||||
sub parent {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_parent} ) {
|
||||
$self->{_parent} = $self->_build_parent;
|
||||
}
|
||||
|
||||
return $self->{_parent};
|
||||
}
|
||||
|
||||
sub glue_names {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_glue_names} ) {
|
||||
$self->{_glue_names} = $self->_build_glue_names;
|
||||
}
|
||||
|
||||
return $self->{_glue_names};
|
||||
}
|
||||
|
||||
sub glue {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_glue} ) {
|
||||
$self->{_glue} = $self->_build_glue;
|
||||
}
|
||||
|
||||
return $self->{_glue};
|
||||
}
|
||||
|
||||
sub ns_names {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_ns_names} ) {
|
||||
$self->{_ns_names} = $self->_build_ns_names;
|
||||
}
|
||||
|
||||
return $self->{_ns_names};
|
||||
}
|
||||
|
||||
sub ns {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_ns} ) {
|
||||
$self->{_ns} = $self->_build_ns;
|
||||
}
|
||||
|
||||
return $self->{_ns};
|
||||
}
|
||||
|
||||
sub glue_addresses {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( !exists $self->{_glue_addresses} ) {
|
||||
$self->{_glue_addresses} = $self->_build_glue_addresses;
|
||||
}
|
||||
|
||||
return $self->{_glue_addresses};
|
||||
}
|
||||
|
||||
###
|
||||
### Builders
|
||||
###
|
||||
|
||||
sub _build_parent {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( $self->name eq '.' ) {
|
||||
return $self;
|
||||
}
|
||||
|
||||
my $pname = Zonemaster::Engine::Recursor->parent( q{} . $self->name );
|
||||
return if not $pname;
|
||||
## no critic (Modules::RequireExplicitInclusion)
|
||||
return __PACKAGE__->new( { name => $pname } );
|
||||
}
|
||||
|
||||
sub _build_glue_names {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( not $self->parent ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
my $p = $self->parent->query_persistent( $self->name, 'NS' );
|
||||
|
||||
return [] if not defined $p;
|
||||
|
||||
return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
|
||||
$p->get_records_for_name( 'ns', $self->name->string ) ];
|
||||
}
|
||||
|
||||
sub _build_glue {
|
||||
my ( $self ) = @_;
|
||||
my @glue_names = @{ $self->glue_names };
|
||||
my $zname = $self->name->string;
|
||||
|
||||
if ( Zonemaster::Engine::Recursor->has_fake_addresses( $zname ) ) {
|
||||
my @ns_list;
|
||||
foreach my $ns ( @glue_names ) {
|
||||
foreach my $ip ( Zonemaster::Engine::Recursor->get_fake_addresses( $zname, $ns ) ) {
|
||||
push @ns_list, Zonemaster::Engine::Nameserver->new( { name => $ns, address => $ip } );
|
||||
}
|
||||
}
|
||||
return \@ns_list;
|
||||
}
|
||||
else {
|
||||
|
||||
my $aref = [];
|
||||
tie @$aref, 'Zonemaster::Engine::NSArray', @glue_names;
|
||||
|
||||
return $aref;
|
||||
}
|
||||
}
|
||||
|
||||
sub _build_ns_names {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( $self->name eq '.' ) {
|
||||
my %u;
|
||||
$u{$_} = $_ for map { $_->name } @{ $self->ns };
|
||||
return [ sort values %u ];
|
||||
}
|
||||
|
||||
my $p;
|
||||
my $i = 0;
|
||||
while ( my $s = $self->glue->[$i] ) {
|
||||
$p = $s->query( $self->name, 'NS' );
|
||||
last if ( defined( $p ) and ( $p->type eq 'answer' ) and ( $p->rcode eq 'NOERROR' ) );
|
||||
$i += 1;
|
||||
}
|
||||
return [] if not defined $p;
|
||||
|
||||
return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
|
||||
$p->get_records_for_name( 'ns', $self->name->string ) ];
|
||||
} ## end sub _build_ns_names
|
||||
|
||||
sub _build_ns {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( $self->name eq '.' ) { # Root is a special case
|
||||
return [ Zonemaster::Engine::Recursor->root_servers ];
|
||||
}
|
||||
|
||||
my $aref = [];
|
||||
tie @$aref, 'Zonemaster::Engine::NSArray', @{ $self->ns_names };
|
||||
|
||||
return $aref;
|
||||
}
|
||||
|
||||
sub _build_glue_addresses {
|
||||
my ( $self ) = @_;
|
||||
|
||||
if ( not $self->parent ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
my $p = $self->parent->query_one( $self->name, 'NS' );
|
||||
croak "Failed to get glue addresses" if not defined( $p );
|
||||
|
||||
return [ $p->get_records( 'a' ), $p->get_records( 'aaaa' ) ];
|
||||
}
|
||||
|
||||
sub _is_ip_version_disabled {
|
||||
my ( $ns, $type ) = @_;
|
||||
|
||||
if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv4}) and $ns->address->version == $IP_VERSION_4 ) {
|
||||
Zonemaster::Engine->logger->add(
|
||||
SKIP_IPV4_DISABLED => {
|
||||
ns => $ns->string,
|
||||
rrtype => $type
|
||||
}
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv6}) and $ns->address->version == $IP_VERSION_6 ) {
|
||||
Zonemaster::Engine->logger->add(
|
||||
SKIP_IPV6_DISABLED => {
|
||||
ns => $ns->string,
|
||||
rrtype => $type
|
||||
}
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
###
|
||||
### Public Methods
|
||||
###
|
||||
|
||||
sub query_one {
|
||||
my ( $self, $name, $type, $flags ) = @_;
|
||||
|
||||
# Return response from the first server that gives one
|
||||
my $i = 0;
|
||||
while ( my $ns = $self->ns->[$i] ) {
|
||||
if ( _is_ip_version_disabled( $ns, $type ) ) {
|
||||
next;
|
||||
}
|
||||
|
||||
my $p = $ns->query( $name, $type, $flags );
|
||||
return $p if defined( $p );
|
||||
}
|
||||
continue {
|
||||
$i += 1;
|
||||
}
|
||||
|
||||
return;
|
||||
} ## end sub query_one
|
||||
|
||||
sub query_all {
|
||||
my ( $self, $name, $type, $flags ) = @_;
|
||||
|
||||
my @servers = @{ $self->ns };
|
||||
|
||||
if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv4}) ) {
|
||||
my @nope = grep { $_->address->version == $IP_VERSION_4 } @servers;
|
||||
@servers = grep { $_->address->version == $IP_VERSION_6 } @servers;
|
||||
map {
|
||||
Zonemaster::Engine->logger->add(
|
||||
SKIP_IPV4_DISABLED => {
|
||||
ns => $_->string,
|
||||
rrtype => $type
|
||||
}
|
||||
)
|
||||
} @nope;
|
||||
}
|
||||
|
||||
if ( not Zonemaster::Engine::Profile->effective->get(q{net.ipv6}) ) {
|
||||
my @nope = grep { $_->address->version == $IP_VERSION_6 } @servers;
|
||||
@servers = grep { $_->address->version == $IP_VERSION_4 } @servers;
|
||||
map {
|
||||
Zonemaster::Engine->logger->add(
|
||||
SKIP_IPV6_DISABLED => {
|
||||
ns => $_->string,
|
||||
rrtype => $type
|
||||
}
|
||||
)
|
||||
} @nope;
|
||||
}
|
||||
|
||||
return [ map { $_->query( $name, $type, $flags ) } @servers ];
|
||||
}
|
||||
|
||||
sub query_auth {
|
||||
my ( $self, $name, $type, $flags ) = @_;
|
||||
|
||||
# Return response from the first server that replies with AA set
|
||||
my $i = 0;
|
||||
while ( my $ns = $self->ns->[$i] ) {
|
||||
if ( _is_ip_version_disabled( $ns, $type ) ) {
|
||||
next;
|
||||
}
|
||||
|
||||
my $p = $ns->query( $name, $type, $flags );
|
||||
if ( $p and $p->aa ) {
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
continue {
|
||||
$i += 1;
|
||||
}
|
||||
|
||||
return;
|
||||
} ## end sub query_auth
|
||||
|
||||
sub query_persistent {
|
||||
my ( $self, $name, $type, $flags ) = @_;
|
||||
|
||||
# Return response from the first server that has a record like the one asked for
|
||||
my $i = 0;
|
||||
while ( my $ns = $self->ns->[$i] ) {
|
||||
if ( _is_ip_version_disabled( $ns, $type ) ) {
|
||||
next;
|
||||
}
|
||||
|
||||
my $p = $ns->query( $name, $type, $flags );
|
||||
if ( $p and scalar( $p->get_records_for_name( $type, $name ) ) > 0 ) {
|
||||
return $p;
|
||||
}
|
||||
}
|
||||
continue {
|
||||
$i += 1;
|
||||
}
|
||||
|
||||
return;
|
||||
} ## end sub query_persistent
|
||||
|
||||
sub is_in_zone {
|
||||
my ( $self, $name ) = @_;
|
||||
|
||||
if ( not ref( $name ) or ref( $name ) ne 'Zonemaster::Engine::DNSName' ) {
|
||||
$name = Zonemaster::Engine::DNSName->new( $name );
|
||||
}
|
||||
|
||||
if ( scalar( @{ $self->name->labels } ) != $self->name->common( $name ) ) {
|
||||
return 0; # Zone name cannot be a suffix of tested name
|
||||
}
|
||||
|
||||
my $p = $self->query_auth( "$name", 'SOA' );
|
||||
if ( not $p ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $p->is_redirect ) {
|
||||
return 0; # Authoritative servers redirect us, so name must be out-of-zone
|
||||
}
|
||||
|
||||
my ( $soa ) = $p->get_records( 'SOA' );
|
||||
|
||||
if ( not $soa ) {
|
||||
return 0; # Auth server is broken, call it a "no".
|
||||
}
|
||||
|
||||
if ( Zonemaster::Engine::DNSName->new( $soa->name ) eq $self->name ) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
} ## end sub is_in_zone
|
||||
|
||||
1;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Zonemaster::Engine::Zone - Object representing a DNS zone
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my $zone = Zonemaster::Engine::Zone->new({ name => 'nic.se' });
|
||||
my $packet = $zone->parent->query_one($zone->name, 'NS');
|
||||
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Objects of this class represent zones in DNS. As far as possible, test
|
||||
implementations should access information about zones via these
|
||||
objects. Doing so will provide lazy-loading of the information,
|
||||
well-defined methods in which the information is fetched, logging and
|
||||
the ability to do things like testing zones that have not yet been
|
||||
delegated.
|
||||
|
||||
=head1 CONSTRUCTORS
|
||||
|
||||
=over
|
||||
|
||||
=item new
|
||||
|
||||
Construct a new instance.
|
||||
|
||||
=back
|
||||
|
||||
=head1 ATTRIBUTES
|
||||
|
||||
=over
|
||||
|
||||
=item name
|
||||
|
||||
A L<Zonemaster::Engine::DNSName> object representing the name of the zone.
|
||||
|
||||
=item parent
|
||||
|
||||
A L<Zonemaster::Engine::Zone> object for this domain's parent domain. As a
|
||||
special case, the root zone is considered to be its own parent (so
|
||||
look for that if you recurse up the tree).
|
||||
|
||||
=item ns_names
|
||||
|
||||
A reference to an array of L<Zonemaster::Engine::DNSName> objects, holding the
|
||||
names of the nameservers for the domain, as returned by the first
|
||||
responding nameserver in the glue list.
|
||||
|
||||
=item ns
|
||||
|
||||
A reference to an array of L<Zonemaster::Engine::Nameserver> objects for the
|
||||
domain, built by taking the list returned from L<ns_names()> and
|
||||
looking up addresses for the names. One element will be added to this
|
||||
list for each unique name/IP pair. Names for which no addresses could
|
||||
be found will not be in this list. The list is lazy-loading, so take
|
||||
care to only look at as many entries as you really need. There are
|
||||
zones with more than 20 nameserver, and looking up the addresses of
|
||||
them all can take som considerable time.
|
||||
|
||||
=item glue_names
|
||||
|
||||
A reference to a an array of L<Zonemaster::Engine::DNSName> objects, holding the names
|
||||
of this zones nameservers as listed at the first responding nameserver of the
|
||||
parent zone.
|
||||
|
||||
=item glue
|
||||
|
||||
A reference to an array of L<Zonemaster::Engine::Nameserver> objects for the
|
||||
domain, built by taking the list returned from L<glue_names()> and
|
||||
looking up addresses for the names. One element will be added to this
|
||||
list for each unique name/IP pair. Names for which no addresses could
|
||||
be found will not be in this list. In this case, the list is lazy-loading, so take
|
||||
care to only look at as many entries as you really need. In case of
|
||||
undelegated tests and fake delegation the IP associated with name servers
|
||||
for the tested zone will be the ones set by users (saved in
|
||||
%Zonemaster::Engine::Recursor::fake_addresses_cache), instead of the ones
|
||||
found recursively.
|
||||
|
||||
=item glue_addresses
|
||||
|
||||
A list of L<Zonemaster::LDNS::RR::A> and L<Zonemaster::LDNS::RR::AAAA> records returned in
|
||||
the Additional section of an NS query to the first listed nameserver for the
|
||||
parent domain.
|
||||
|
||||
=back
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=over
|
||||
|
||||
=item query_one($name[, $type[, $flags]])
|
||||
|
||||
Sends (or retrieves from cache) a query for the given name, type and flags sent to the first nameserver in the zone's ns list. If there is a
|
||||
response, it will be returned in a L<Zonemaster::Engine::Packet> object. If the type arguments is not given, it defaults to 'A'. If the flags are not given, they default to C<class> IN and C<dnssec>, C<usevc> and C<recurse> according to configuration (which is by default off on all three).
|
||||
|
||||
=item query_persistent($name[, $type[, $flags]])
|
||||
|
||||
Identical to L<query_one>, except that instead of returning the packet from the
|
||||
first server that returns one, it returns the first packet that actually
|
||||
contains a resource record matching the requested name and type.
|
||||
|
||||
=item query_auth($name[, $type[, $flags]])
|
||||
|
||||
Identical to L<query_one>, except that instead of returning the packet from the
|
||||
first server that returns one, it returns the first packet that has the AA flag set.
|
||||
|
||||
=item query_all($name, $type, $flags)
|
||||
|
||||
Sends (or retrieves from cache) queries to all the nameservers listed in the zone's ns list, and returns a reference to an array with the
|
||||
responses. The responses can be either L<Zonemaster::Engine::Packet> objects or C<undef> values. The arguments are the same as for L<query_one>.
|
||||
|
||||
=item is_in_zone($name)
|
||||
|
||||
Returns true if the given name is in the zone, false if not. If it could not be
|
||||
determined with a sufficient degree of certainty if the name is in the zone or
|
||||
not, C<undef> is returned.
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
Reference in New Issue
Block a user