File Coverage

blib/lib/ParaDNS/Resolver.pm
Criterion Covered Total %
statement 102 251 40.6
branch 14 72 19.4
condition 4 23 17.3
subroutine 19 36 52.7
pod 5 12 41.6
total 144 394 36.5


line stmt bran cond sub pod time code
1             package ParaDNS::Resolver;
2 1     1   5 use base qw(Danga::Socket);
  1         2  
  1         1156  
3              
4 1     1   30270 use fields qw(res dst queries);
  1         2  
  1         11  
5              
6 1     1   828 use Net::DNS;
  1         115204  
  1         133  
7 1     1   14 use Socket;
  1         2  
  1         780  
8 1     1   8 use strict;
  1         2  
  1         49  
9              
10 1     1   6 no warnings 'deprecated';
  1         3  
  1         61  
11              
12 1   50 1   6 use constant TRACE_LEVEL => ($ENV{PARADNS_DEBUG} || 0);
  1         2  
  1         77  
13 1   50 1   6 use constant NO_DNS0x20 => ($ENV{NO_DNS0x20} || 0);
  1         2  
  1         2340  
14             *trace = \&ParaDNS::trace;
15              
16             sub new {
17 1     1 1 2 my ParaDNS::Resolver $self = shift;
18 1         4 my $servers = shift;
19            
20 1 50       7 $self = fields::new($self) unless ref $self;
21            
22 1         219 my $res = Net::DNS::Resolver->new;
23            
24 1   50     634 my $sock = IO::Socket::INET->new(
25             Proto => 'udp',
26             LocalAddr => $res->{'srcaddr'},
27             LocalPort => ($res->{'srcport'} || undef),
28             ) || die "Cannot create socket: $!";
29 1         298 IO::Handle::blocking($sock, 0);
30            
31 1         4 $self->{dst} = [];
32            
33 1 50       4 if ($servers) {
34 0         0 foreach my $ns (@{$servers}) {
  0         0  
35 0         0 my ($s, $p) = split(/:/, $ns);
36 0 0       0 $p = 53 if !$p;
37 0         0 my $dst_sockaddr = sockaddr_in($p, inet_aton($s));
38 0         0 push @{$self->{dst}}, $dst_sockaddr;
  0         0  
39 0         0 trace(2, "Using override nameserver $s:$p\n");
40             }
41             }
42             else {
43 1         3 foreach my $ns (@{ $res->{nameservers} }) {
  1         5  
44 0         0 trace(2, "Using nameserver $ns:$res->{port}\n");
45 0         0 my $dst_sockaddr = sockaddr_in($res->{'port'}, inet_aton($ns));
46 0         0 push @{$self->{dst}}, $dst_sockaddr;
  0         0  
47             }
48             }
49            
50 1         3 $self->{res} = $res;
51            
52             # copied from SpamAssassin (I think all are irrelevant, but just in case...)
53 1         16 $self->{res}->retry(1); # If it fails, it fails
54 1         36 $self->{res}->retrans(0); # If it fails, it fails
55 1         35 $self->{res}->dnsrch(0); # ignore domain search-list
56 1         34 $self->{res}->defnames(0); # don't append stuff to end of query
57             # I think these values are irrelevant...
58 1         34 $self->{res}->tcp_timeout($ParaDNS::TIMEOUT); # timeout
59 1         34 $self->{res}->udp_timeout($ParaDNS::TIMEOUT); # timeout
60 1         44 $self->{res}->persistent_tcp(0); # bug 3997
61 1         35 $self->{res}->persistent_udp(0); # bug 3997
62            
63 1         26 $self->{queries} = {};
64            
65 1         12 $self->SUPER::new($sock);
66            
67 1     0   102 Danga::Socket->AddTimer(1, sub { $self->_do_cleanup });
  0         0  
68            
69 1         26 $self->watch_read(1);
70            
71 1         28 return $self;
72             }
73              
74             sub ns {
75 4     4 0 5 my ParaDNS::Resolver $self = shift;
76 4         5 my $index = shift;
77 4 50       5 return if $index > $#{$self->{dst}};
  4         18  
78 0         0 return $self->{dst}->[$index];
79             }
80              
81             sub pending {
82 0     0 0 0 my ParaDNS::Resolver $self = shift;
83            
84 0         0 return keys(%{$self->{queries}});
  0         0  
85             }
86              
87             # implements draft-vixie-dnsext-dns0x20-00
88             sub dnsext_dns0x20 {
89 4     4 0 5 my ($string) = @_;
90 4         5 my $rnd;
91 4         6 my $have_rnd_bits = 0;
92 4         12 my $result = '';
93 4         19 for my $ic (unpack("C*",$string)) {
94 57 100       203 if (chr($ic) =~ /^[A-Za-z]\z/) {
95 51 100       92 if ($have_rnd_bits < 1) {
96 4         40 $rnd = rand(0x7fffffff); $have_rnd_bits = 31;
  4         5  
97             }
98 51 100       89 $ic ^= 0x20 if $rnd & 1; # flip the 0x20 bit in name if dice says so
99 51         45 $rnd = $rnd >> 1; $have_rnd_bits--;
  51         48  
100             }
101 57         74 $result .= chr($ic);
102             }
103 4         12 return $result;
104             }
105              
106             sub _query {
107 4     4   5 my ParaDNS::Resolver $self = shift;
108 4         8 my ($asker, $host, $type, $now) = @_;
109            
110 4         10 $host = dnsext_dns0x20($host) unless NO_DNS0x20;
111            
112 4         26 my $packet = $self->{res}->make_query_packet($host, $type);
113            
114 4         4804 my $id = $packet->header->id;
115 4         66 while ($self->{queries}->{$id}) {
116             # ID already in use, try again :-(
117 0         0 trace(2, "Query ID $id already in use. Trying another\n") if TRACE_LEVEL >= 2;
118 0         0 $packet = $self->{res}->make_query_packet($host, $type);
119 0         0 $id = $packet->header->id;
120             }
121 4         16 my $packet_data = $packet->data;
122            
123 4 50       465 my $query = ParaDNS::Resolver::Query->new(
124             $self, $asker, $host, $type, $now, $id, $packet_data,
125             ) or return;
126 0         0 $self->{queries}->{$id} = $query;
127            
128 0         0 return 1;
129             }
130              
131             sub query_type {
132 4     4 0 6 my ParaDNS::Resolver $self = shift;
133 4         15 my ($asker, $type, @hosts) = @_;
134            
135 4         11 my $now = time();
136            
137 4         5 trace(2, "Trying to resolve $type: @hosts\n") if TRACE_LEVEL >= 2;
138              
139 4         8 foreach my $host (@hosts) {
140 4 50       13 $self->_query($asker, $host, $type, $now) || return;
141             }
142            
143 0         0 return 1;
144             }
145              
146             sub query_txt {
147 0     0 0 0 my ParaDNS::Resolver $self = shift;
148 0         0 my ($asker, @hosts) = @_;
149 0         0 return $self->query_type($asker, "TXT", @hosts);
150             }
151              
152             sub query_mx {
153 0     0 0 0 my ParaDNS::Resolver $self = shift;
154 0         0 my ($asker, @hosts) = @_;
155 0         0 return $self->query_type($asker, "MX", @hosts);
156             }
157              
158             sub query {
159 0     0 0 0 my ParaDNS::Resolver $self = shift;
160 0         0 my ($asker, @hosts) = @_;
161            
162 0         0 my $now = time();
163            
164 0         0 trace(2, "trying to resolve A/PTR: @hosts\n") if TRACE_LEVEL >= 2;
165              
166 0         0 foreach my $host (@hosts) {
167 0 0       0 $self->_query($asker, $host, 'A', $now) || return;
168             }
169            
170 0         0 return 1;
171             }
172              
173             sub _do_cleanup {
174 0     0   0 my ParaDNS::Resolver $self = shift;
175 0         0 my $now = time;
176            
177 0         0 my $idle = $ParaDNS::TIMEOUT;
178            
179 0         0 my $t0 = $now - $idle;
180            
181 0         0 my @to_delete;
182 0         0 keys %{$self->{queries}}; # reset internal iterator
  0         0  
183 0         0 while (my ($id, $obj) = each(%{$self->{queries}})) {
  0         0  
184 0 0       0 if ($obj->{timeout} < $t0) {
185 0         0 push @to_delete, $id;
186             }
187             }
188            
189 0         0 foreach my $id (@to_delete) {
190 0         0 my $query = delete $self->{queries}{$id};
191 0 0       0 $query->timeout() and next;
192             # add back in if timeout caused us to loop to next server
193 0         0 $self->{queries}->{$id} = $query;
194             }
195              
196 0     0   0 $self->AddTimer(1, sub { $self->_do_cleanup } );
  0         0  
197             }
198              
199             # ParaDNS
200 0     0 1 0 sub event_err { shift->close("dns socket error") }
201 0     0 1 0 sub event_hup { shift->close("dns socket error") }
202              
203             my %type_to_host = (
204             PTR => 'ptrdname',
205             A => 'address',
206             AAAA => 'address',
207             TXT => 'txtdata',
208             NS => 'nsdname',
209             CNAME => 'cname',
210             );
211              
212             sub event_read {
213 0     0 1 0 my ParaDNS::Resolver $self = shift;
214              
215 0         0 my $sock = $self->sock;
216 0         0 my $res = $self->{res};
217            
218 0         0 while (my $packet = $res->bgread($sock)) {
219 0         0 my $err = $res->errorstring;
220 0         0 my $answers = 0;
221 0         0 my $header = $packet->header;
222 0         0 my $id = $header->id;
223            
224 0         0 my $qobj = delete $self->{queries}->{$id};
225 0 0       0 if (!$qobj) {
226 0         0 trace(1, "No query for id: $id\n") if TRACE_LEVEL;
227 0         0 return;
228             }
229            
230 0         0 my $query = $qobj->{host};
231            
232 0         0 my ($question) = $packet->question; # only ever send one question
233 0 0       0 if (!$question) {
234 0         0 trace(1, "No question for id: $id. Should be: $query\n") if TRACE_LEVEL;
235 0         0 return;
236             }
237            
238 0 0 0     0 if ($question->qtype eq 'A' && $question->qname ne $query) {
239 0         0 trace(1, "Query mismatch for id: $id. $query ne " . $question->qname . "\n") if TRACE_LEVEL;
240 0         0 return;
241             }
242            
243 0         0 my $now = time();
244 0         0 foreach my $rr ($packet->answer) {
245 0 0       0 if (my $host_method = $type_to_host{$rr->type}) {
    0          
246 0         0 my $host = $rr->$host_method;
247 0         0 trace(2, "Answer: " . $rr->type . " $host\n") if TRACE_LEVEL;
248 0 0 0     0 if ($rr->type eq 'CNAME' && $qobj->recurse_cname) {
249             # TODO: Should probably loop over the other answers here to check
250             # for an answer to the question we're just about to ask...
251             # (on the other hand, this works)
252            
253 0         0 my $packet = $res->make_query_packet($host, $qobj->type);
254              
255 0         0 my $packet_data = $packet->data;
256 0         0 my $id = $packet->header->id;
257              
258 0 0       0 my $query = ParaDNS::Resolver::Query->new(
259             $self, $qobj->asker, $host, $qobj->type, time, $id, $packet_data,
260             ) or next;
261 0         0 $self->{queries}->{$id} = $query;
262 0         0 next;
263             }
264             #my $type = $rr->type;
265             #$type = 'A' if $type eq 'PTR';
266             # print "DNS Lookup $type $query = $host; TTL = ", $rr->ttl, "\n";
267 0         0 $qobj->run_callback($host, $rr->ttl);
268             }
269             elsif ($rr->type eq "MX") {
270 0         0 my $host = $rr->exchange;
271 0         0 my $preference = $rr->preference;
272 0         0 $qobj->run_callback([$host, $preference], $rr->ttl);
273             }
274             else {
275             # came back, but not a PTR or A record
276 0         0 $qobj->run_callback("UNKNOWN");
277             }
278 0         0 $answers++;
279             }
280 0 0       0 if (!$answers) {
281 0 0       0 if ($err eq "NXDOMAIN") {
    0          
    0          
    0          
282             # trace("found => NXDOMAIN\n");
283 0         0 $qobj->run_callback("NXDOMAIN");
284             }
285             elsif ($err eq "SERVFAIL") {
286             # try again???
287             # print "SERVFAIL looking for $query\n";
288             #$self->query($asker, $query);
289 0 0       0 $qobj->error($err) and next;
290             # add back in if error() resulted in query being re-issued
291 0         0 $self->{queries}->{$id} = $qobj;
292             }
293             elsif ($err eq "NOERROR") {
294 0         0 $qobj->run_callback($err);
295             }
296             elsif($err) {
297             #print("Unknown error: $err\n");
298 0 0       0 $qobj->error($err) and next;
299 0         0 $self->{queries}->{$id} = $qobj;
300             }
301             else {
302             # trace("no answers\n");
303 0         0 $qobj->run_callback("NOANSWER");
304             }
305             }
306             }
307             }
308              
309 1     1   7 use Carp qw(confess);
  1         2  
  1         104  
310              
311             sub close {
312 0     0 1 0 my ParaDNS::Resolver $self = shift;
313            
314 0         0 $self->SUPER::close(shift);
315             # confess "ParaDNS::Resolver socket should never be closed!";
316             }
317              
318             package ParaDNS::Resolver::Query;
319              
320 1     1   13 use fields qw( resolver asker host type timeout id data repeat ns nqueries );
  1         1  
  1         9  
321              
322 1     1   172 use constant MAX_QUERIES => 10;
  1         2  
  1         63  
323              
324 1   50 1   6 use constant TRACE_LEVEL => ($ENV{PARADNS_DEBUG} || 0);
  1         2  
  1         869  
325             *trace = \&ParaDNS::trace;
326              
327             sub new {
328 4     4   7 my ParaDNS::Resolver::Query $self = shift;
329 4 50       19 $self = fields::new($self) unless ref $self;
330            
331 4         341 @$self{qw( resolver asker host type timeout id data )} = @_;
332             # repeat is number of retries
333 4         14 @$self{qw( repeat ns nqueries )} = ($ParaDNS::REQUERY,0,0);
334            
335 4         6 trace(2, "NS Query: $self->{host} ($self->{id})\n") if TRACE_LEVEL >= 2;
336            
337 4 50       12 $self->send_query || return;
338            
339 0         0 return $self;
340             }
341              
342             sub type {
343 0     0   0 my ParaDNS::Resolver::Query $self = shift;
344 0         0 $self->{type};
345             }
346              
347             sub asker {
348 0     0   0 my ParaDNS::Resolver::Query $self = shift;
349 0         0 $self->{asker};
350             }
351              
352             sub recurse_cname {
353 0     0   0 my ParaDNS::Resolver::Query $self = shift;
354            
355 0 0 0     0 if ($self->{type} eq 'A' || $self->{type} eq 'AAAA') {
356 0 0       0 if ($self->{nqueries} <= MAX_QUERIES) {
357 0         0 return 1;
358             }
359             }
360              
361 0         0 return 0;
362             }
363              
364             #sub DESTROY {
365             # my $self = shift;
366             # trace(2, "DESTROY $self\n");
367             #}
368              
369             sub timeout {
370 0     0   0 my ParaDNS::Resolver::Query $self = shift;
371            
372 0         0 trace(2, "NS Query timeout. Trying next host\n") if TRACE_LEVEL >= 2;
373 0 0       0 if ($self->send_query) {
374             # had another NS to send to, reset timeout
375 0         0 $self->{timeout} = time();
376 0         0 return;
377             }
378            
379             # can we loop/repeat?
380 0 0 0     0 if (($self->{nqueries} <= MAX_QUERIES) &&
381             ($self->{repeat} > 1))
382             {
383 0         0 trace(2, "NS Query timeout. Next host failed. Trying loop\n") if TRACE_LEVEL >= 2;
384 0         0 $self->{repeat}--;
385 0         0 $self->{ns} = 0;
386 0         0 return $self->timeout();
387             }
388            
389 0         0 trace(2, "NS Query timeout. All failed. Running callback(TIMEOUT)\n") if TRACE_LEVEL >= 2;
390             # otherwise we really must timeout.
391 0         0 $self->run_callback("TIMEOUT");
392 0         0 return 1;
393             }
394              
395             sub error {
396 0     0   0 my ParaDNS::Resolver::Query $self = shift;
397 0         0 my ($error) = @_;
398            
399 0         0 trace(2, "NS Query error. Trying next host\n") if TRACE_LEVEL >= 2;
400 0 0       0 if ($self->send_query) {
401             # had another NS to send to, reset timeout
402 0         0 $self->{timeout} = time();
403 0         0 return;
404             }
405            
406             # can we loop/repeat?
407 0 0 0     0 if (($self->{nqueries} <= MAX_QUERIES) &&
408             ($self->{repeat} > 1))
409             {
410 0         0 trace(2, "NS Query error. Next host failed. Trying loop\n") if TRACE_LEVEL >= 2;
411 0         0 $self->{repeat}--;
412 0         0 $self->{ns} = 0;
413 0         0 return $self->error($error);
414             }
415            
416 0         0 trace(2, "NS Query error. All failed. Running callback($error)\n") if TRACE_LEVEL >= 2;
417             # otherwise we really must timeout.
418 0         0 $self->run_callback($error);
419 0         0 return 1;
420             }
421              
422             sub run_callback {
423 0     0   0 my ParaDNS::Resolver::Query $self = shift;
424 0         0 trace(2, "NS Query callback($self->{host} = $_[0]\n") if TRACE_LEVEL >= 2;
425 0         0 $self->{asker}->run_callback($_[0], lc($self->{host}), $_[1]);
426             }
427              
428             sub send_query {
429 4     4   6 my ParaDNS::Resolver::Query $self = shift;
430            
431 4         5 my $res = $self->{resolver};
432 4         17 my $dst = $res->ns($self->{ns}++);
433 4 50       77 return unless defined $dst;
434 0 0         if (!$res->sock->send($self->{data}, 0, $dst)) {
435 0           warn("socket send failed: $!");
436 0           return;
437             }
438            
439 0           $self->{nqueries}++;
440 0           return 1;
441             }
442              
443             1;
444              
445             =head1 NAME
446              
447             ParaDNS::Resolver - an asynchronous DNS resolver class
448              
449             =head1 SYNOPSIS
450              
451             my $res = ParaDNS::Resolver->new();
452            
453             $res->query($obj, @hosts); # $obj implements $obj->run_callback()
454              
455             =head1 DESCRIPTION
456              
457             This is a low level DNS resolver class that works within the Danga::Socket
458             asynchronous I/O framework. Do not attempt to use this class standalone - use
459             the C class instead.
460              
461             =cut