File: //usr/share/perl5/vendor_perl/Amavis/Lookup/DNSxL.pm
# SPDX-License-Identifier: GPL-2.0-or-later
package Amavis::Lookup::DNSxL;
use strict;
use re 'taint';
BEGIN {
use vars qw($dns_resolver); # implicit persistent Net::DNS::Resolver object
}
use Amavis::Conf qw(:platform);
use Amavis::Util qw(ll do_log);
sub new {
my($class, $zone, $expected, $resolver) = @_;
# $zone is either a DNSxL zone name, or a template where an %a is a
# place-holder for the IP address to be queried.
# The result of a type-A DNS query is matched against $expected, which is
# either a scalar string, or a ref to an array of strings, or a regexp obj.
require NetAddr::IP or die "Can't load module NetAddr::IP";
NetAddr::IP->VERSION(4.010); # need a method full6()
if ($resolver) {
# DNS resolver object provided by a caller, use that
} elsif ($dns_resolver) {
# reuse previously created internal resolver object
$resolver = $dns_resolver;
} else { # create a new internal resolver object with some sensible defaults
require Net::DNS or die "Can't load module Net::DNS";
$dns_resolver = Net::DNS::Resolver->new(
config_file => '/etc/resolv.conf', force_v4 => !$have_inet6,
defnames => 0, retry => 1, persistent_udp => 1,
tcp_timeout => 2, udp_timeout => 2, retrans => 1); # seconds
$dns_resolver or die "Failed to create a Net::DNS::Resolver object";
$dns_resolver->udppacketsize(1220);
$resolver = $dns_resolver;
}
defined $zone && $zone ne ''
or die "DNS zone name must not be empty, in Amavis::Lookup::DNSxL";
$expected = '127.0.0.2' if !defined $expected; # an RFC 5782 convention
my $self = {
zone => $zone, # DNSxL zone name (a base DNS domain name)
resolver => $resolver, # a Net::DNS::Resolver object or equivalent
expected => $expected, # a set of replies that qualify as a match
};
bless $self, $class;
}
# Query a DNSxL list given an IPv4 or IPv6 address, according to RFC 5782.
# Returns an IPv4 address in the 127.0.0.0/8 subnet as returned by a DNS
# type-A query when the result matches the provided expected value, or a
# zero when a query succeeded (NOERROR or NXDOMAIN) but there was no match.
# The argument $expected may be a string, a ref to array, or a regexp object.
# Returns undef on DNS failures (like a timeout, or no Net::DNS module).
#
sub lookup_ip {
my($self, $ipaddr) = @_;
my $result; # result of a DNS query, undef indicates a lookup failure
my $fullkey; # matching (expected) key
return ($result,$fullkey) if !$self->{resolver};
my $revip;
local($1,$2,$3,$4);
if ($ipaddr =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\z/) {
$revip = "$4.$3.$2.$1";
} elsif ($ipaddr =~ /:[0-9a-f]*:/i) { # triage
# looks like an IPv6 address, let NetAddr::IP check the details
my $ip_obj = NetAddr::IP->new6($ipaddr);
if (defined $ip_obj) { # a valid IPv6 address, apply RFC 5782 section 2.4
$revip = lc $ip_obj->network->full6; # string in a canonical form
$revip =~ s/://gs; $revip = join('.', reverse split(//,$revip));
}
}
if (!defined $revip) {
do_log(4,'invalid IP address for a DNSxL query: %s', $ipaddr);
return ($result,$fullkey);
}
my $query = $self->{zone};
$query =~ s/%a/$revip/gs or ($query = $revip . '.' .$query);
my $pkt = $self->{resolver}->send($query, 'A');
my $ll5 = ll(5);
$result = 0; # defined but false
if (!$pkt || !$pkt->header) {
undef $result;
$ll5 && do_log(5,'DNSxL query %s, no result', $query);
} elsif ($pkt->header->rcode eq 'NXDOMAIN') {
$ll5 && do_log(5,'DNSxL query %s, domain does not exist', $query);
} elsif ($pkt->header->rcode ne 'NOERROR') {
$ll5 && do_log(5,'DNSxL query %s, rcode %s', $query, $pkt->header->rcode);
} elsif ($pkt->header->ancount) {
my $expected = $self->{expected};
$expected = [ $expected ] if !ref $expected;
for my $rr ($pkt->answer) {
next if $rr->type ne 'A';
my $returned_addr = $rr->address;
$ll5 && do_log(5,'DNSxL query %s, DNS answer: %s',$query,$returned_addr);
# RFC 5782 section 2.3: values SHOULD be in the 127.0.0.0/8 range
next if $returned_addr !~ /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/s;
if (ref $expected eq 'ARRAY') {
# $expected is an array of strings: IPv4 addresses in dotted-quad
# form, with bytes possibly omitted from the left
for (@$expected) {
if ( ( /^\d+\z/ ? "127.0.0.$_"
: /^\d+\.\d+\z/ ? "127.0.$_"
: /^\d+\.\d+\.\d+\z/ ? "127.$_" : $_) eq $returned_addr) {
$fullkey = $_; $result = $returned_addr;
last;
}
}
last if defined $result;
} elsif (ref $expected eq 'Regexp') {
# $expected is a regular expresion
if ($returned_addr =~ /$expected/s) {
$fullkey = "$expected"; # stringified regexp object
$result = $returned_addr; last;
}
}
}
}
do_log(5,'DNSxL result: %s, matches %s',$result,$fullkey) if $ll5 && $result;
($result, $fullkey);
}
1;