home *** CD-ROM | disk | FTP | other *** search
/ ftp.f-secure.com / 2014.06.ftp.f-secure.com.tar / ftp.f-secure.com / support / hotfix / fsis / IS-SpamControl.fsfix / iufssc / lib / Mail / SpamAssassin / DnsResolver.pm < prev    next >
Text File  |  2006-11-29  |  16KB  |  616 lines

  1. # <@LICENSE>
  2. # Licensed to the Apache Software Foundation (ASF) under one or more
  3. # contributor license agreements.  See the NOTICE file distributed with
  4. # this work for additional information regarding copyright ownership.
  5. # The ASF licenses this file to you under the Apache License, Version 2.0
  6. # (the "License"); you may not use this file except in compliance with
  7. # the License.  You may obtain a copy of the License at:
  8. #     http://www.apache.org/licenses/LICENSE-2.0
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # </@LICENSE>
  15.  
  16. =head1 NAME
  17.  
  18. Mail::SpamAssassin::DnsResolver - DNS resolution engine
  19.  
  20. =head1 DESCRIPTION
  21.  
  22. This is a DNS resolution engine for SpamAssassin, implemented in order to
  23. reduce file descriptor usage by Net::DNS and avoid a response collision bug in
  24. that module.
  25.  
  26. =head1 METHODS
  27.  
  28. =over 4
  29.  
  30. =cut
  31.  
  32. # TODO: caching in this layer instead of in callers.
  33.  
  34. package Mail::SpamAssassin::DnsResolver;
  35.  
  36. use strict;
  37. use warnings;
  38. use bytes;
  39.  
  40. use Mail::SpamAssassin;
  41. use Mail::SpamAssassin::Logger;
  42.  
  43. use IO::Socket::INET;
  44. use Errno qw(EINVAL EADDRINUSE);
  45.  
  46. use constant HAS_SOCKET_INET6 => eval { require IO::Socket::INET6 };
  47.  
  48. our @ISA = qw();
  49.  
  50. # TODO: would be nice to have this in Conf.pm, but resolver is loaded
  51. # before configs are read! :(
  52. #=item dns_cache_items n
  53. #
  54. #Number of RBL responses to store in a query cache. Default 2000
  55. #
  56. #=cut
  57. #
  58. #  push (@cmds, {
  59. #    setting => 'dns_cache_items',
  60. #    default => 2000,
  61. #    type => $CONF_TYPE_NUMERIC
  62. #  });
  63. #
  64. #=item dns_cache_timeout n
  65. #
  66. #Default time to keep RBL responses in query cache. Default 120, but
  67. #overridden by the TTL value of each response.
  68. #
  69. #=cut
  70. #
  71. #  push (@cmds, {
  72. #    setting => 'dns_cache_timeout',
  73. #    default => 120,
  74. #    type => $CONF_TYPE_NUMERIC
  75. #  });
  76. #
  77. #=back
  78.  
  79. my $dns_cache_items = 2000;
  80. my $dns_cache_timeout = 255;
  81.  
  82. ###########################################################################
  83.  
  84. sub new {
  85.   my $class = shift;
  86.   $class = ref($class) || $class;
  87.  
  88.   my ($main) = @_;
  89.   my $self = {
  90.     'main'              => $main,
  91.     'conf'        => $main->{conf},
  92.     'id_to_callback'    => { },
  93.   };
  94.   bless ($self, $class);
  95.  
  96.   $self->load_resolver();
  97.   $self;
  98. }
  99.  
  100. ###########################################################################
  101.  
  102. =item $res->load_resolver()
  103.  
  104. Load the C<Net::DNS::Resolver> object.  Returns 0 if Net::DNS cannot be used,
  105. 1 if it is available.
  106.  
  107. =cut
  108.  
  109. use FS::Cache;
  110. sub load_resolver {
  111.   my ($self) = @_;
  112.  
  113.   if (defined $self->{res}) { return 1; }
  114.   $self->{no_resolver} = 1;
  115.  
  116.   eval {
  117.     require Net::DNS;
  118.     $self->{res} = Net::DNS::Resolver->new;
  119.     if (defined $self->{res}) {
  120.       $self->{no_resolver} = 0;
  121.       $self->{dns_cache} = new FS::Cache(
  122.         $dns_cache_items,
  123.         $dns_cache_timeout
  124.       );
  125.       $self->{retry} = 1;               # retries for non-nackgrounded query
  126.       $self->{retrans} = 3;   # initial timeout for "non-backgrounded" query run in background
  127.       $self->{res}->retry(1);           # If it fails, it fails
  128.       $self->{res}->retrans(0);         # If it fails, it fails
  129.       $self->{res}->dnsrch(0);          # ignore domain search-list
  130.       $self->{res}->defnames(0);        # don't append stuff to end of query
  131.       $self->{res}->tcp_timeout(3);     # timeout of 3 seconds only
  132.       $self->{res}->udp_timeout(3);     # timeout of 3 seconds only
  133.       $self->{res}->persistent_tcp(0);  # bug 3997
  134.       $self->{res}->persistent_udp(0);  # bug 3997
  135.     }
  136.     1;
  137.   };   #  or warn "dns: eval failed: $@ $!\n";
  138.  
  139.   dbg("dns: is Net::DNS::Resolver available? " .
  140.        ($self->{no_resolver} ? "no" : "yes"));
  141.   if (!$self->{no_resolver} && defined $Net::DNS::VERSION) {
  142.     dbg("dns: Net::DNS version: ".$Net::DNS::VERSION);
  143.   }
  144.  
  145.   return (!$self->{no_resolver});
  146. }
  147.  
  148. =item $resolver = $res->get_resolver()
  149.  
  150. Return the C<Net::DNS::Resolver> object.
  151.  
  152. =cut
  153.  
  154. sub get_resolver {
  155.   my ($self) = @_;
  156.   return $self->{res};
  157. }
  158.  
  159. =item $res->nameservers()
  160.  
  161. Wrapper for Net::DNS::Reslolver->nameservers to get or set list of nameservers
  162.  
  163. =cut
  164.  
  165. sub nameservers {
  166.   my $self = shift;
  167.   my $res = $self->{res};
  168.   $self->connect_sock_if_reqd();
  169.   return $res->nameservers(@_) if $res;
  170. }
  171.  
  172. =item $res->connect_sock()
  173.  
  174. Re-connect to the first nameserver listed in C</etc/resolv.conf> or similar
  175. platform-dependent source, as provided by C<Net::DNS>.
  176.  
  177. =cut
  178.  
  179. sub connect_sock {
  180.   my ($self) = @_;
  181.  
  182.   return if $self->{no_resolver};
  183.  
  184.   $self->{sock}->close() if $self->{sock};
  185.   my $sock;
  186.   my $errno;
  187.  
  188.   # IO::Socket::INET6 may choose wrong LocalAddr if family is unspecified,
  189.   # causing EINVAL failure when automatically assigned local IP address
  190.   # and remote address do not belong to the same address family:
  191.   use Mail::SpamAssassin::Constants qw(:ip);
  192.   my $ip64 = IP_ADDRESS;
  193.   my $ip4 = IPV4_ADDRESS;
  194.   my $ns = $self->{res}->{nameservers}[0];
  195.   my $ipv6 = 0;
  196.  
  197.   # now, attempt to set the family to AF_INET6 if we can.  Some
  198.   # platforms don't have it (bug 4412 comment 29)...
  199.   # also, only set $ipv6 to true if that succeeds.
  200.   my $family;
  201.   if (HAS_SOCKET_INET6 && $ns=~/^${ip64}$/o && $ns!~/^${ip4}$/o) {
  202.     eval '$family = AF_INET6; $ipv6 = 1;';
  203.   }
  204.   if (!defined $family) {
  205.     $family = AF_INET;       # that didn't work ;)
  206.   }
  207.  
  208.   dbg("dns: discovered name servers: " .
  209.     join(", ", @{$self->{res}->{nameservers}})
  210.   );
  211.   dbg("dns: name server: $ns, family: $family, ipv6: $ipv6");
  212.  
  213.   # find next available unprivileged port (1024 - 65535)
  214.   # starting at a random value to spread out use of ports
  215.   my $port_offset = int(rand(64511));  # 65535 - 1024
  216.   for (my $i = 0; $i<64511; $i++) {
  217.     my $lport = 1024 + (($port_offset + $i) % 64511);
  218.  
  219.     my %args = (
  220.         PeerAddr => $ns,
  221.         PeerPort => $self->{res}->{port},
  222.         Proto => 'udp',
  223.         LocalPort => $lport,
  224.         Type => SOCK_DGRAM,
  225.         Domain => $family,
  226.     );
  227.  
  228.     if (HAS_SOCKET_INET6) {
  229.       $sock = IO::Socket::INET6->new(%args);
  230.     } else {
  231.       $sock = IO::Socket::INET->new(%args);
  232.     }
  233.     $errno = $!;
  234.     if (defined $sock) {  # ok, got it
  235.       last;
  236.     } elsif ($! == EADDRINUSE) {  # in use, let's try another source port
  237.       dbg("dns: UDP port $lport already in use, trying another port");
  238.     } else {
  239.       # did we fail due to the attempted use of an IPv6 nameserver?
  240.       $self->_ipv6_ns_warning()  if (!$ipv6 && $errno==EINVAL);
  241.       warn "Error creating a DNS resolver socket: $errno";
  242.       goto no_sock;
  243.     }
  244.   }
  245.   if (!defined $sock) {
  246.     warn "Can't create a DNS resolver socket: $errno";
  247.     goto no_sock;
  248.   }
  249.  
  250.   $self->{sock} = $sock;
  251.   $self->{sock_as_vec} = $self->fhs_to_vec($self->{sock});
  252.   return;
  253.  
  254. no_sock:
  255.   $self->{no_resolver} = 1;
  256. }
  257.  
  258. sub connect_sock_if_reqd {
  259.   my ($self) = @_;
  260.   $self->connect_sock() if !$self->{sock};
  261. }
  262.  
  263. =item $res->get_sock()
  264.  
  265. Return the C<IO::Socket::INET> object used to communicate with
  266. the nameserver.
  267.  
  268. =cut
  269.  
  270. sub get_sock {
  271.   my ($self) = @_;
  272.   $self->connect_sock_if_reqd();
  273.   return $self->{sock};
  274. }
  275.  
  276. ###########################################################################
  277.  
  278. =item $packet = new_dns_packet ($host, $type, $class)
  279.  
  280. A wrapper for C<Net::DNS::Packet::new()> which traps a die thrown by it.
  281.  
  282. To use this, change calls to C<Net::DNS::Resolver::bgsend> from:
  283.  
  284.     $res->bgsend($hostname, $type);
  285.  
  286. to:
  287.  
  288.     $res->bgsend(Mail::SpamAssassin::DnsResolver::new_dns_packet($hostname, $type, $class));
  289.  
  290. =cut
  291.  
  292. sub new_dns_packet {
  293.   my ($self, $host, $type, $class) = @_;
  294.  
  295.   return if $self->{no_resolver};
  296.  
  297.   # construct a PTR query if it looks like an IPv4 address
  298.   if ((!defined($type) || $type eq 'PTR') && $host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
  299.     $host = "$4.$3.$2.$1.in-addr.arpa.";
  300.     $type = 'PTR';
  301.   }
  302.  
  303.   $self->connect_sock_if_reqd();
  304.   my $packet;
  305.   eval {
  306.     $packet = Net::DNS::Packet->new($host, $type, $class);
  307.  
  308.     # a bit noisy, so commented by default...
  309.     #dbg("dns: new DNS packet time=".time()." host=$host type=$type id=".$packet->id);
  310.   };
  311.  
  312.   if ($@) {
  313.     # this can happen if Net::DNS isn't available -- but in this
  314.     # case this function should never be called!
  315.     warn "dns: cannot create Net::DNS::Packet, but new_dns_packet() was called: $@ $!";
  316.   }
  317.   return $packet;
  318. }
  319.  
  320. # Internal function used only in this file
  321. ## compute an unique ID for a packet to match the query to the reply
  322. ## It must use only data that is returned unchanged by the nameserver.
  323. ## Argument is a Net::DNS::Packet that has a non-empty question section
  324. ## return is an object that can be used as a hash key
  325. sub _packet_id {
  326.   my ($self, $packet) = @_;
  327.   my $header = $packet->header;
  328.   my $id = $header->id;
  329.   my @questions = $packet->question;
  330.   my $ques = $questions[0];
  331.  
  332.   if (defined $ques) {
  333.     return $id . $ques->qname . $ques->qtype . $ques->qclass;
  334.   } else {
  335.     # odd.  this should not happen, but clearly some DNS servers
  336.     # can return something that Net::DNS interprets as having no
  337.     # question section.  Better support it; just return the
  338.     # (safe) ID part, along with a text token indicating that
  339.     # the packet had no question part.
  340.     return $id . "NO_QUESTION_IN_PACKET";
  341.   }
  342. }
  343.  
  344. ###########################################################################
  345.  
  346. =item $id = $res->bgsend($host, $type, $class, $cb)
  347.  
  348. Quite similar to C<Net::DNS::Resolver::bgsend>, except that when a response
  349. packet eventually arrives, and C<poll_responses> is called, the callback
  350. sub reference C<$cb> will be called.
  351.  
  352. Note that C<$type> and C<$class> may be C<undef>, in which case they
  353. will default to C<A> and C<IN>, respectively.
  354.  
  355. The callback sub will be called with two arguments -- the packet that was
  356. delivered and an id string that fingerprints the query packet and the expected reply.
  357. It is expected that a closure callback be used, like so:
  358.  
  359.   my $id = $self->{resolver}->bgsend($host, $type, undef, sub {
  360.         my $reply = shift;
  361.         my $reply_id = shift;
  362.         $self->got_a_reply ($reply, $reply_id);
  363.       });
  364.  
  365. The callback can ignore the reply as an invalid packet sent to the listening port
  366. if the reply id does not match the return value from bgsend.
  367.  
  368. =cut
  369.  
  370. sub cache_key {
  371.   my ($packet) = @_;
  372.   my @questions = $packet->question;
  373.   my $ques = $questions[0];
  374.  
  375.   return join("|", $ques->qname, $ques->qtype, $ques->qclass);
  376. }
  377.  
  378. sub bgsend {
  379.   my ($self, $host, $type, $class, $cb) = @_;
  380.   return if $self->{no_resolver};
  381.  
  382.   my $pkt = $self->new_dns_packet($host, $type, $class);
  383.  
  384.   # check if a cached reply exists
  385.   my $key = cache_key($pkt);
  386.   if (my $packet = $self->{dns_cache}->get($key)) {
  387.     $cb->($packet, $key);
  388.  
  389.     my ($hit, $miss) = $self->{dns_cache}->stats();
  390.     dbg(sprintf "dnscache: hit %d/%d %.1f%% %s",
  391.       $hit, $miss, 100.0 * $hit / ($hit + $miss), $key
  392.     );
  393.  
  394.     return $key;
  395.   }
  396.  
  397.   $self->connect_sock_if_reqd();
  398.   if (!defined($self->{sock}->send($pkt->data, 0))) {
  399.     warn "dns: sendto() failed: $!";
  400.     return;
  401.   }
  402.   my ($hit, $miss) = $self->{dns_cache}->stats();
  403.   dbg(sprintf "dnscache: miss %d/%d %.1f%% %s",
  404.     $hit, $miss, 100.0 * $hit / ($hit + $miss), $key
  405.   );
  406.  
  407.   my $id = $self->_packet_id($pkt);
  408.   $self->{id_to_callback}->{$id} = $cb;
  409.   return $id;
  410. }
  411.  
  412. sub min {
  413.   my $min = $_[0];
  414.   for (@_) {
  415.     $min = $_ if $min > $_
  416.   }
  417.   return $min;
  418. }
  419.  
  420. ###########################################################################
  421.  
  422. =item $nfound = $res->poll_responses()
  423.  
  424. See if there are any C<bgsend> response packets ready, and return
  425. the number of such packets delivered to their callbacks.
  426.  
  427. =cut
  428.  
  429. sub poll_responses {
  430.   my ($self, $timeout) = @_;
  431.   return if $self->{no_resolver};
  432.   return if !$self->{sock};
  433.  
  434.   my $rin = $self->{sock_as_vec};
  435.   my $rout;
  436.   my ($nfound, $timeleft) = select($rout=$rin, undef, undef, $timeout);
  437.  
  438.   if (!defined $nfound || $nfound < 0) {
  439.     warn "dns: select failed: $!";
  440.     return;
  441.   }
  442.  
  443.   if ($nfound == 0) {
  444.     return 0;           # nothing's ready yet
  445.   }
  446.  
  447.   my $packet = $self->{res}->bgread($self->{sock});
  448.   my $err = $self->{res}->errorstring;
  449.  
  450.   if (defined $packet &&
  451.       defined $packet->header &&
  452.       defined $packet->question &&
  453.       defined $packet->answer)
  454.   {
  455.     my $id = $self->_packet_id($packet);
  456.  
  457.     my $cb = delete $self->{id_to_callback}->{$id};
  458.     if (!$cb) {
  459.       dbg("dns: no callback for id: $id, ignored; packet: ".
  460.                     ($packet ? $packet->string : "undef"));
  461.       return 0;
  462.     }
  463.  
  464.     # cache result
  465.     my $ttl = min(map {$_->ttl} $packet->answer);
  466.     unless (defined $ttl) {
  467.       $ttl = min(map {$_->minimum} $packet->authority) || 0;
  468.       dbg("dnscache: no answer, setting ttl by SOA to $ttl");
  469.     }
  470.     $self->{dns_cache}->set(cache_key($packet), $packet, $ttl);
  471.  
  472.     $cb->($packet, $id);
  473.     return 1;
  474.   }
  475.   else {
  476.     dbg("dns: no packet! err=$err packet=".
  477.                     ($packet ? $packet->string : "undef"));
  478.   }
  479.  
  480.   return 0;
  481. }
  482.  
  483. ###########################################################################
  484.  
  485. =item $res->bgabort()
  486.  
  487. Call this to release pending requests from memory when aborting backgrounded requests
  488.  
  489. =cut
  490.  
  491. sub bgabort {
  492.   my ($self) = @_;
  493.   $self->{id_to_callback} = {};
  494. }
  495.  
  496. ###########################################################################
  497.  
  498. =item $packet = $res->send($name, $type, $class)
  499.  
  500. Emulates C<Net::DNS::Resolver::send()>.
  501.  
  502. =cut
  503.  
  504. sub send {
  505.   my ($self, $name, $type, $class) = @_;
  506.   return if $self->{no_resolver};
  507.  
  508.   my $retrans = $self->{retrans};
  509.   my $retries = $self->{retry};
  510.   my $timeout = $retrans;
  511.   my $answerpkt;
  512.   for (my $i = 0;
  513.        (($i < $retries) && !defined($answerpkt));
  514.        ++$i, $retrans *= 2, $timeout = $retrans) {
  515.  
  516.     $timeout = 1 if ($timeout < 1);
  517.     # note nifty use of a closure here.  I love closures ;)
  518.     $self->bgsend($name, $type, $class, sub {
  519.       $answerpkt = shift;
  520.     });
  521.  
  522.     my $now = time;
  523.     my $deadline = $now + $timeout;
  524.  
  525.     while (($now < $deadline) && (!defined($answerpkt))) {
  526.       $self->poll_responses(1);
  527.       $now = time;
  528.     }
  529.   }
  530.   return $answerpkt;
  531. }
  532.  
  533. ###########################################################################
  534.  
  535. =item $res->finish_socket()
  536.  
  537. Reset socket when done with it.
  538.  
  539. =cut
  540.  
  541. sub finish_socket {
  542.   my ($self) = @_;
  543.   if ($self->{sock}) {
  544.     $self->{sock}->close();
  545.     delete $self->{sock};
  546.   }
  547. }
  548.  
  549. ###########################################################################
  550.  
  551. =item $res->finish()
  552.  
  553. Clean up for destruction.
  554.  
  555. =cut
  556.  
  557. sub finish {
  558.   my ($self) = @_;
  559.   $self->finish_socket();
  560.   if (!$self->{no_resolver}) {
  561.     delete $self->{res};
  562.   }
  563.   delete $self->{main};
  564. }
  565.  
  566. ###########################################################################
  567. # non-public methods.
  568.  
  569. # should move to Util.pm (TODO)
  570. sub fhs_to_vec {
  571.   my ($self, @fhlist) = @_;
  572.   my $rin = '';
  573.   foreach my $sock (@fhlist) {
  574.     my $fno = fileno($sock);
  575.     warn "dns: oops! fileno now undef for $sock" unless defined($fno);
  576.     vec ($rin, $fno, 1) = 1;
  577.   }
  578.   return $rin;
  579. }
  580.  
  581. # call Mail::SA::init() instead
  582. sub reinit_post_fork {
  583.   my ($self) = @_;
  584.   # and a new socket, so we don't have 5 spamds sharing the same
  585.   # socket
  586.   $self->connect_sock();
  587. }
  588.  
  589. sub _ipv6_ns_warning {
  590.   my ($self) = @_;
  591.  
  592.   # warn about the attempted use of an IPv6 nameserver without
  593.   # IO::Socket::INET6 installed (bug 4412)
  594.   my $firstns = $self->{res}->{nameservers}[0];
  595.  
  596.   use Mail::SpamAssassin::Constants qw(:ip);
  597.   my $ip64 = IP_ADDRESS;
  598.   my $ip4 = IPV4_ADDRESS;
  599.  
  600.   # was the nameserver in IPv6 format?
  601.   if ($firstns =~ /^${ip64}$/o && $firstns !~ /^${ip4}$/o) {
  602.     my $addr = inet_aton($firstns);
  603.     if (!defined $addr) {
  604.       die "IO::Socket::INET6 module is required to use IPv6 nameservers such as '$firstns': $@\n";
  605.     }
  606.   }
  607. }
  608.  
  609. 1;
  610.  
  611. =back
  612.  
  613. =cut
  614.