File: Zone.pm

package info (click to toggle)
libzonemaster-perl 8.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 72,256 kB
  • sloc: perl: 16,941; makefile: 16
file content (486 lines) | stat: -rw-r--r-- 12,865 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
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