File Coverage

blib/lib/Net/DNS/DomainController/Discovery.pm
Criterion Covered Total %
statement 59 60 98.3
branch 20 22 95.4
condition 3 3 100.0
subroutine 12 12 100.0
pod 6 6 100.0
total 100 103 98.0


line stmt bran cond sub pod time code
1             package Net::DNS::DomainController::Discovery;
2              
3 13     13   3244535 use 5.006;
  13         331  
4 13     13   84 use strict;
  13         29  
  13         369  
5 13     13   68 use warnings;
  13         40  
  13         369  
6 13     13   61 use Carp;
  13         25  
  13         979  
7              
8             =head1 NAME
9              
10             Net::DNS::DomainController::Discovery - Discover Microsoft Active Directory domain controllers via DNS queries
11              
12             =head1 VERSION
13              
14             Version 1.00
15              
16             =cut
17              
18             our $VERSION = '1.00';
19              
20             =head1 SYNOPSIS
21              
22             Issues DNS requests to provide a list of hostnames and IP addresses of the Microsoft
23             Active Directory domain controllers.
24              
25             use Net::DNS::DomainController::Discovery;
26              
27             my $foo = Net::DNS::DomainController::Discovery::domain_controllers('fabrikam.com');
28             ...
29              
30             Multiple domain names can be specified:
31              
32             my $foo = Net::DNS::DomainController::Discovery::domain_controllers('fabrikam.com', 'contoso.com');
33              
34             This module works only if the Active Directory domain controllers are registed with the domain name system (DNS).
35              
36             =cut
37              
38 13     13   99 use Exporter qw(import);
  13         36  
  13         778  
39             our @EXPORT_OK = qw(domain_controllers srv_to_name srv_fqdn_list fqdn_to_ipaddr);
40              
41 13     13   1040 use Net::DNS::Resolver;
  13         121605  
  13         8381  
42              
43             our $TestResolver;
44              
45             =head2 domain_controllers
46              
47             Use this function to obtain a list of Active Domain controllers registered in the DNS
48             for the domain names given as arguments.
49              
50             Returns a nested array of (domain name, hostname, ip address) tuples that contain all
51             the Active Directory domain controllers serving the domain name if registered in the DNS.
52              
53             If the domain does not contain any Active Domain domain controller service records,
54             no entries for the domain are returned.
55              
56             No records are returned for the domain controller names which do resolve neither
57             to an IPv4 nor IPv6 address.
58              
59             =cut
60              
61             sub domain_controllers {
62              
63 18 100   18 1 95069 croak "Active Directory domain name not provided" unless (@_);
64              
65 17         29 my $resolver;
66              
67             # uncoverable branch false
68 17 50       45 if (defined $TestResolver) {
69 17         29 $resolver = $TestResolver;
70             } else {
71 0         0 $resolver = Net::DNS::Resolver->new();
72             }
73              
74 17         32 my @dc;
75 17         37 foreach my $domain_name (@_) {
76 25         61 foreach my $fqdn (srv_fqdn_list( $resolver, dc_to_srv( $domain_name ))) {
77 25         549 foreach my $addr (fqdn_ipaddr_list( $resolver, 'AAAA', $fqdn )) {
78 4         66 push @dc, [ $domain_name, $fqdn, $addr ];
79             }
80 24         52 foreach my $addr (fqdn_ipaddr_list( $resolver, 'A', $fqdn )) {
81 20         268 push @dc, [ $domain_name, $fqdn, $addr ];
82             }
83             }
84             }
85 9         34 return @dc;
86             }
87             =head1 INTERNAL SUBROUTINES
88              
89             =head2 srv_to_name
90              
91             Extract server name from the SRV response.
92              
93             =cut
94              
95             sub srv_to_name {
96 30     30 1 9203 my $rr = shift;
97 30 100       75 if ( ! $rr ) {
98 1         256 confess "Need Net::DNS::RR record";
99             }
100 29 100       74 if ( $rr->type ne 'SRV' ) {
101 2         28 croak "Need Net::DNS::RR::SRV record (got \"". ${rr}->type . "\")";
102             }
103 27         264 return $rr->target;
104             }
105              
106             =head2 srv_fqdn_list
107              
108             Query SRV records and return server names if any.
109              
110             =cut
111              
112             sub srv_fqdn_list {
113 21     21 1 10724 my ($resolver, $domain_name) = @_;
114 21         125 my $resp = $resolver->query( $domain_name, 'SRV' );
115 21         6604 my @dc_name_list;
116              
117 21 100       62 if ( $resp ) {
118 19         116 return map { srv_to_name($_) } $resp->answer;
  27         307  
119             } else {
120 2         7 return ();
121             }
122             }
123              
124             =head2 fqdn_to_ipaddr
125              
126             Extract IP addresses from the resolver response.
127              
128             =cut
129              
130             sub fqdn_to_ipaddr {
131 30     30 1 12344 my $rr = shift;
132 30 100       103 if ( ! $rr ) {
133 1         250 confess "Need Net::DNS::RR record";
134             }
135 29 100 100     81 if ( $rr->type ne 'A' && $rr->type ne 'AAAA' ) {
136 3         89 croak "Need Net::DNS::RR::A or AAAA record (got \"" . ${rr}->type . "\")";
137             }
138 26         325 return $rr->address;
139             }
140              
141             =head2 fqdn_ipaddr_list
142              
143             Resolver server names using the appropriate record for the address family requested.
144             C<$type> parameter should be set C for IPv4, C for IPv6).
145              
146             =cut
147              
148             sub fqdn_ipaddr_list {
149 49     49 1 96 my ($resolver, $type, $fqdn) = @_;
150 49         115 my $resp = $resolver->query( $fqdn, $type );
151 49         8920 my @dc_ip_list;
152            
153 49 100       103 if ( $resp ) {
154 25         57 return map { fqdn_to_ipaddr($_) } $resp->answer;
  25         125  
155             } else {
156             return ()
157 24         63 }
158             }
159              
160             =head2 dc_to_srv
161              
162             Validate the domain name and add the magic string for the Active Directory domain controllers.
163              
164             =cut
165              
166             sub dc_to_srv {
167 25 50   25 1 58 croak "Active Directory domain name not provided" unless (@_);
168 25 100       323 croak "Active Directory domain name not defined" unless $_[0];
169 23 100       524 croak "Invalid domain name: \"$_[0]\"" unless $_[0] =~ /\A\b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\Z/;
170 19         77 return '_ldap._tcp.dc._msdcs.' . $_[0] . '.'
171             }
172              
173             =head1 AUTHOR
174              
175             Marcin CIESLAK, C<< >>
176              
177             =head1 BUGS
178              
179             Please report any bugs or feature requests to C, or through
180             the web interface at L. I will be notified, and then you'll
181             automatically be notified of progress on your bug as I make changes.
182              
183             =head1 SUPPORT
184              
185             Microsoft has a documentation how the Active Directory domain controllers should register themselves in the DNS:
186              
187             Microsoft Active Directory Technical Specifications [MS-ADTS]
188             Section 6.3.2.3 SRV Records
189             Published 14 February 2019 at L
190              
191             Archived on 23 March 2020: L
192              
193             You can find documentation for this module with the perldoc command.
194              
195             perldoc Net::DNS::DomainController::Discovery
196              
197             You can also look for information at:
198              
199             =over 4
200              
201             =item * Source code repository
202              
203             L
204              
205             =item * RT: CPAN's request tracker (report bugs here)
206              
207             L
208              
209             =item * CPAN search engine
210              
211             L
212              
213             =back
214              
215              
216             =head1 LICENSE AND COPYRIGHT
217              
218             This software is Copyright (c) 2020 by Marcin CIESLAK.
219              
220             This is free software, licensed under:
221              
222             The Artistic License 2.0 (GPL Compatible)
223              
224              
225             =cut
226              
227             1; # End of Net::DNS::DomainController::Discovery