sub poll_responses()

in lib/Mail/SpamAssassin/DnsResolver.pm [779:926]


sub poll_responses {
  my ($self, $timeout) = @_;
  return if $self->{no_resolver};
  return if !$self->{sock};
  my $cnt = 0;
  my $cnt_cb = 0;

  my $rin = $self->{sock_as_vec};
  my $rout;

  for (;;) {
    my ($nfound, $timeleft, $eval_stat);
    # if a restartable signal is caught, retry 3 times before aborting
    my $eintrcount = 3;
    eval {  # use eval to caught alarm signal
      my $timer;  # collects timestamp when variable goes out of scope
      if (!defined($timeout) || $timeout > 0)
        { $timer = $self->{main}->time_method("poll_dns_idle") }
      $! = 0;
      ($nfound, $timeleft) = select($rout=$rin, undef, undef, $timeout);
      1;
    } or do {
      $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
    };
    if (defined $eval_stat) {
      # most likely due to an alarm signal, resignal if so
      die "dns: (2) $eval_stat\n"  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
      warn "dns: select aborted: $eval_stat\n";
      last;
    } elsif (!defined $nfound || $nfound < 0) {
      if ($!{EINTR} and $eintrcount > 0) {
        $eintrcount--;
        next;
      }
      if ($!) { warn "dns: select failed: $!\n" }
      else    { info("dns: select interrupted") }  # shouldn't happen
      last;
    } elsif (!$nfound) {
      if (!defined $timeout) { warn("dns: select returned empty-handed\n") }
      elsif ($timeout > 0) { dbg("dns: select timed out %.3f s", $timeout) }
      last;
    }
    $cnt += $nfound;

    my $now = time;
    $timeout = 0;  # next time around collect whatever is available, then exit
    last  if $nfound == 0;

    my $packet;
    # Bug 7265, use our own bgread() below
    # $packet = $self->{res}->bgread($self->{sock});
    eval {
      $packet = $self->bgread();  # Bug 7265, use our own bgread()
    } or do {
      undef $packet;
      my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
      # resignal if alarm went off
      die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
      info("dns: bad dns reply: %s", $eval_stat);
    };

    # bug 8225 - Do TCP fallback when UDP reply packet is too long, by retrying using Net::DNS::Resolver bgsend and bgread
    my ($id, $packet_id);
    if ($packet && $packet->header) {
      my $header = $packet->header;
      $packet_id = $header->id;  # set these here in case we need to retry for TCP fallback
      $id = $self->_packet_id($packet);  # which will change $packet to a different class object
      if ($header->rcode eq 'NOERROR' && $header->tc) {
        # Use original Resolver which can handle TCP fallback, but keep id from the custom packet
        my (undef, $qclass, $qtype, $qname) = split('/', $id);
        dbg("dns: TCP fallback retry with %s, %s, %s", $qname, $qtype, $qclass);
        my $orig_resolver =  $self->{main}->{resolver}->get_resolver();
        eval {
          my $handle = $orig_resolver->bgsend($qname, $qtype, $qclass);
          $packet = $orig_resolver->bgread($handle);
        } or do {
          undef $packet;
          my $eval_stat = $@ ne '' ? $@ : "errno=$!";  chomp $eval_stat;
          # resignal if alarm went off
          die $eval_stat  if $eval_stat =~ /__alarm__ignore__\(.*\)/s;
          info("dns: bad dns tcp fallback reply: %s", $eval_stat);
        };
      }
    }

    if (!$packet) {
      # error already reported above
#     my $dns_err = $self->{res}->errorstring;
#     die "dns (3) $dns_err\n"  if $dns_err =~ /__alarm__ignore__\(.*\)/s;
#     info("dns: bad dns reply: $dns_err");
    } else {
      my $header = $packet->header;
      if (!$header) {
        info("dns: dns reply is missing a header section");
      } else {
        my $rcode = $header->rcode;
        if ($rcode eq 'NOERROR') {  # success
          # NOERROR, may or may not have answer records
          dbg("dns: dns reply %s is OK, %d answer records",
              $packet_id, $header->ancount);
          if ($header->tc) {  # truncation flag turned on
            my $edns = $self->{conf}->{dns_options}->{edns} || 512;
            info("dns: reply to %s truncated (%s), %d answer records", $id,
                 $edns == 512 ? "EDNS off" : "EDNS $edns bytes",
                 $header->ancount);
          }
        } else {
          # some failure, e.g. NXDOMAIN, SERVFAIL, FORMERR, REFUSED, ...
          # btw, one reason for SERVFAIL is an RR signature failure in DNSSEC
          dbg("dns: dns reply to %s: %s", $id, $rcode);
        }

        # A hash lookup: the id must match exactly (case-sensitively).
        # The domain name part of the id was lowercased if dns0x20 is off,
        # and case-randomized when dns0x20 option is on.
        #
        my $cb = delete $self->{id_to_callback}->{$id};

        if ($cb) {
          $cb->($packet, $id, $now);
          $cnt_cb++;
        } else {  # no match, report the problem
          if ($rcode eq 'REFUSED' || $id =~ m{^\d+/NO_QUESTION_IN_PACKET\z}) {
            # the failure was already reported above
          } else {
            info("dns: no callback for id $id, ignored, packet on next debug line");
            # prevent filling normal logs with huge packet dumps
            dbg("dns: %s", $packet ? $packet->string : "undef");
          }
          # report a likely matching query for diagnostic purposes
          local $1;
          if ($id =~ m{^(\d+)/}) {
            my $dnsid = $1;  # the raw DNS packet id
            my @matches =
              grep(m{^\Q$dnsid\E/}o, keys %{$self->{id_to_callback}});
            if (!@matches) {
              info("dns: no likely matching queries for id %s", $dnsid);
            } else {
              info("dns: a likely matching query: %s", join(', ', @matches));
            }
          }
        }
      }
    }
  }

  return ($cnt, $cnt_cb);
}