Skip to content

Commit de5e35f

Browse files
committed
Add DNAME support - initial commit
- Add new attribute 'dname' to Engine::Zone objects - Update implementation to account for that new attribute and make queries to the appropriate delegation name, if one exists
1 parent b814356 commit de5e35f

File tree

2 files changed

+141
-12
lines changed

2 files changed

+141
-12
lines changed

lib/Zonemaster/Engine/Constants.pm

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package Zonemaster::Engine::Constants;
33
use v5.16.0;
44
use warnings;
55

6-
use version; our $VERSION = version->declare("v1.2.5");
6+
use version; our $VERSION = version->declare("v1.2.6");
77

88
use Carp;
99
use English qw( -no_match_vars ) ;
@@ -38,6 +38,10 @@ DNSSEC algorithms.
3838
3939
CNAME records.
4040
41+
=item dname
42+
43+
DNAME records.
44+
4145
=item name
4246
4347
Label and name lengths.
@@ -73,6 +77,7 @@ our @EXPORT_OK = qw[
7377
$BLACKLISTING_ENABLED
7478
$CNAME_MAX_CHAIN_LENGTH
7579
$CNAME_MAX_RECORDS
80+
$DNAME_MAX_RECORDS
7681
$DURATION_5_MINUTES_IN_SECONDS
7782
$DURATION_1_HOUR_IN_SECONDS
7883
$DURATION_4_HOURS_IN_SECONDS
@@ -100,6 +105,7 @@ our %EXPORT_TAGS = (
100105
qw($ALGO_STATUS_DEPRECATED $ALGO_STATUS_PRIVATE $ALGO_STATUS_RESERVED $ALGO_STATUS_UNASSIGNED $ALGO_STATUS_OTHER $ALGO_STATUS_NOT_ZONE_SIGN $ALGO_STATUS_NOT_RECOMMENDED)
101106
],
102107
cname => [ qw($CNAME_MAX_CHAIN_LENGTH $CNAME_MAX_RECORDS) ],
108+
dname => [ qw($DNAME_MAX_RECORDS) ],
103109
name => [qw($FQDN_MAX_LENGTH $LABEL_MAX_LENGTH)],
104110
ip => [qw($IP_VERSION_4 $IP_VERSION_6)],
105111
soa => [
@@ -136,6 +142,10 @@ An integer, used to define the maximum length of a CNAME chain when doing consec
136142
137143
An integer, used to define the maximum number of CNAME records in a response.
138144
145+
=item * C<$DNAME_MAX_RECORDS>
146+
147+
An integer, used to define the maximum number of DNAME records in a response.
148+
139149
=item * C<$DURATION_5_MINUTES_IN_SECONDS>
140150
141151
=item * C<$DURATION_1_HOUR_IN_SECONDS>
@@ -195,6 +205,8 @@ Readonly our $BLACKLISTING_ENABLED => 1;
195205
Readonly our $CNAME_MAX_CHAIN_LENGTH => 10;
196206
Readonly our $CNAME_MAX_RECORDS => 9;
197207

208+
Readonly our $DNAME_MAX_RECORDS => 9;
209+
198210
Readonly our $DURATION_5_MINUTES_IN_SECONDS => 5 * 60;
199211
Readonly our $DURATION_1_HOUR_IN_SECONDS => 60 * 60;
200212
Readonly our $DURATION_4_HOURS_IN_SECONDS => 4 * 60 * 60;

lib/Zonemaster/Engine/Zone.pm

Lines changed: 128 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ package Zonemaster::Engine::Zone;
33
use v5.16.0;
44
use warnings;
55

6-
use version; our $VERSION = version->declare("v1.1.9");
6+
use version; our $VERSION = version->declare("v1.2.0");
77

88
use Carp qw( confess croak );
99
use List::MoreUtils qw[uniq];
1010

1111
use Zonemaster::Engine::DNSName;
1212
use Zonemaster::Engine::Recursor;
1313
use Zonemaster::Engine::NSArray;
14-
use Zonemaster::Engine::Constants qw[:ip];
14+
use Zonemaster::Engine::Constants qw[:ip :dname];
1515

1616
sub new {
1717
my ( $class, $attrs ) = @_;
@@ -46,6 +46,16 @@ sub parent {
4646
return $self->{_parent};
4747
}
4848

49+
sub dname {
50+
my ( $self ) = @_;
51+
52+
if ( !exists $self->{_dname} ) {
53+
$self->{_dname} = $self->_build_dname;
54+
}
55+
56+
return $self->{_dname};
57+
}
58+
4959
sub glue_names {
5060
my ( $self ) = @_;
5161

@@ -96,6 +106,7 @@ sub glue_addresses {
96106
return $self->{_glue_addresses};
97107
}
98108

109+
99110
###
100111
### Builders
101112
###
@@ -113,25 +124,108 @@ sub _build_parent {
113124
return __PACKAGE__->new( { name => $pname } );
114125
}
115126

127+
sub _build_dname {
128+
my ( $self ) = @_;
129+
130+
if ( $self->name eq '.' or not $self->parent ) {
131+
return undef;
132+
}
133+
134+
my $p = $self->parent->query_persistent( $self->name, 'DNAME' );
135+
136+
return undef unless $p;
137+
138+
Zonemaster::Engine->logger->add( DNAME_FOUND => { name => $self->name } );
139+
140+
my @dname_rrs = $p->get_records( 'DNAME' );
141+
142+
# Remove duplicate DNAME RRs
143+
my ( %duplicate_dname_rrs, @original_rrs );
144+
for my $rr ( @dname_rrs ) {
145+
my $rr_hash = $rr->class . '/DNAME/' . lc($rr->owner) . '/' . lc($rr->dname);
146+
147+
if ( exists $duplicate_dname_rrs{$rr_hash} ) {
148+
$duplicate_dname_rrs{$rr_hash}++;
149+
}
150+
else {
151+
$duplicate_dname_rrs{$rr_hash} = 0;
152+
push @original_rrs, $rr;
153+
}
154+
}
155+
156+
unless ( scalar @original_rrs == scalar @dname_rrs ) {
157+
@dname_rrs = @original_rrs;
158+
}
159+
160+
# Break if there are too many records
161+
if ( scalar @dname_rrs > $DNAME_MAX_RECORDS ) {
162+
return undef;
163+
}
164+
165+
my ( %dnames, %seen_targets, %forbidden_targets );
166+
for my $rr ( @dname_rrs ) {
167+
my $rr_owner = Zonemaster::Engine::DNSName->new( lc( $rr->owner) );
168+
my $rr_target = Zonemaster::Engine::DNSName->new( lc( $rr->dname ) );
169+
170+
# Multiple DNAME records with same owner name
171+
if ( exists $forbidden_targets{$rr_owner} ) {
172+
return undef;
173+
}
174+
175+
# DNAME owner name is target, or target has already been seen in this response, or owner name cannot be a target
176+
if ( $rr_owner eq $rr_target or exists $seen_targets{$rr_target} or grep { $_ eq $rr_target } ( keys %forbidden_targets ) ) {
177+
return undef;
178+
}
179+
180+
$seen_targets{$rr_target} = 1;
181+
$forbidden_targets{$rr_owner} = 1;
182+
$dnames{$rr_owner} = $rr_target;
183+
}
184+
185+
# Get final DNAME target
186+
my $target = $self->name;
187+
my $dname_counter = 0;
188+
while ( $dnames{$target} ) {
189+
return undef if $dname_counter > $DNAME_MAX_RECORDS; # Loop protection (for good measure only - data in %dnames is sanitized already)
190+
$target = $dnames{$target};
191+
$dname_counter++;
192+
}
193+
194+
# Make sure that the DNAME chain from the RRs is not broken
195+
if ( $dname_counter != scalar @dname_rrs ) {
196+
return undef;
197+
}
198+
199+
return __PACKAGE__->new( { name => Zonemaster::Engine::DNSName->new( $target ) } );
200+
}
201+
116202
sub _build_glue_names {
117203
my ( $self ) = @_;
204+
my $zname = $self->name;
205+
my $p;
118206

119207
if ( not $self->parent ) {
120208
return [];
121209
}
122210

123-
my $p = $self->parent->query_persistent( $self->name, 'NS' );
211+
if ( $self->dname ) {
212+
$zname = $self->dname->name;
213+
$p = $self->dname->parent->query_persistent( $zname, 'NS' );
214+
}
215+
else {
216+
$p = $self->parent->query_persistent( $zname, 'NS' );
217+
}
124218

125219
return [] if not defined $p;
126220

127221
return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
128-
$p->get_records_for_name( 'ns', $self->name->string ) ];
222+
$p->get_records_for_name( 'ns', $zname->string ) ];
129223
}
130224

131225
sub _build_glue {
132226
my ( $self ) = @_;
133-
my @glue_names = @{ $self->glue_names };
134227
my $zname = $self->name->string;
228+
my @glue_names = @{$self->glue_names};
135229

136230
if ( Zonemaster::Engine::Recursor->has_fake_addresses( $zname ) ) {
137231
my @ns_list;
@@ -153,24 +247,34 @@ sub _build_glue {
153247

154248
sub _build_ns_names {
155249
my ( $self ) = @_;
250+
my $zname = $self->name;
251+
my $servers;
252+
my $p;
253+
my $i = 0;
156254

157255
if ( $self->name eq '.' ) {
158256
my %u;
159257
$u{$_} = $_ for map { $_->name } @{ $self->ns };
160258
return [ sort values %u ];
161259
}
162260

163-
my $p;
164-
my $i = 0;
165-
while ( my $s = $self->glue->[$i] ) {
166-
$p = $s->query( $self->name, 'NS' );
261+
if ( $self->dname ) {
262+
$zname = $self->dname->name;
263+
$servers = $self->dname->glue;
264+
}
265+
else {
266+
$servers = $self->glue;
267+
}
268+
269+
while ( my $s = $servers->[$i] ) {
270+
$p = $s->query( $zname, 'NS' );
167271
last if ( defined( $p ) and ( $p->type eq 'answer' ) and ( $p->rcode eq 'NOERROR' ) );
168272
$i += 1;
169273
}
170274
return [] if not defined $p;
171275

172276
return [ uniq sort map { Zonemaster::Engine::DNSName->new( lc( $_->nsdname ) ) }
173-
$p->get_records_for_name( 'ns', $self->name->string ) ];
277+
$p->get_records_for_name( 'ns', $zname ) ];
174278
} ## end sub _build_ns_names
175279

176280
sub _build_ns {
@@ -188,12 +292,21 @@ sub _build_ns {
188292

189293
sub _build_glue_addresses {
190294
my ( $self ) = @_;
295+
my $zname = $self->name;
296+
my $p;
191297

192298
if ( not $self->parent ) {
193299
return [];
194300
}
195301

196-
my $p = $self->parent->query_one( $self->name, 'NS' );
302+
if ( $self->dname ) {
303+
$zname = $self->dname->name;
304+
$p = $self->dname->parent->query_one( $zname, 'NS' );
305+
}
306+
else {
307+
$p = $self->parent->query_one( $zname, 'NS' );
308+
}
309+
197310
croak "Failed to get glue addresses" if not defined( $p );
198311

199312
return [ $p->get_records( 'a' ), $p->get_records( 'aaaa' ) ];
@@ -406,6 +519,10 @@ A L<Zonemaster::Engine::Zone> object for this domain's parent domain. As a
406519
special case, the root zone is considered to be its own parent (so
407520
look for that if you recurse up the tree).
408521
522+
=item dname
523+
524+
A L<Zonemaster::Engine::Zone> object which is this zone's DNAME target, if any.
525+
409526
=item ns_names
410527
411528
A reference to an array of L<Zonemaster::Engine::DNSName> objects, holding the

0 commit comments

Comments
 (0)