our
$KNOWN_BAD_DIALUP_RANGES
;
our
$LAST_DNS_CHECK
= 0;
our
@EXISTING_DOMAINS
=
qw{
akamai.com
bing.com
cloudflare.com
digitalpoint.com
facebook.com
google.com
linkedin.com
netflix.com
php.net
wikipedia.org
yahoo.com
}
;
our
$IS_DNS_AVAILABLE
=
undef
;
sub
do_rbl_lookup {
my
(
$self
,
$rule
,
$set
,
$type
,
$host
,
$subtest
) =
@_
;
if
(
defined
$subtest
) {
if
(
$subtest
=~ /^sb:/) {
info(
"dns: ignored $rule, SenderBase rules are deprecated"
);
return
0;
}
if
(
$subtest
!~ /^\d+(?:\.\d+\.\d+\.\d+)?$/) {
my
(
$rec
,
$err
) = compile_regexp(
$subtest
, 0);
if
(!
$rec
) {
warn
(
"dns: invalid rule $rule subtest regexp '$subtest': $err\n"
);
return
0;
}
$subtest
=
$rec
;
}
}
dbg(
"dns: launching rule %s, set %s, type %s, %s"
,
$rule
,
$set
,
$type
,
defined
$subtest
?
"subtest $subtest"
:
'no subtest'
);
my
$ent
= {
rulename
=>
$rule
,
type
=>
"DNSBL"
,
set
=>
$set
,
subtest
=>
$subtest
,
};
my
$ret
=
$self
->{async}->bgsend_and_start_lookup(
$host
,
$type
,
undef
,
$ent
,
sub
{
my
(
$ent
,
$pkt
) =
@_
;
$self
->process_dnsbl_result(
$ent
,
$pkt
) },
master_deadline
=>
$self
->{master_deadline}
);
return
0
if
defined
$ret
;
return
;
}
sub
do_dns_lookup {
my
(
$self
,
$rule
,
$type
,
$host
) =
@_
;
my
$ent
= {
rulename
=>
$rule
,
type
=>
"DNSBL"
,
};
$self
->{async}->bgsend_and_start_lookup(
$host
,
$type
,
undef
,
$ent
,
sub
{
my
(
$ent
,
$pkt
) =
@_
;
$self
->process_dnsbl_result(
$ent
,
$pkt
) },
master_deadline
=>
$self
->{master_deadline}
);
}
sub
dnsbl_hit {
my
(
$self
,
$rule
,
$question
,
$answer
) =
@_
;
my
$log
=
""
;
if
(
substr
(
$rule
, 0, 2) eq
"__"
) {
}
elsif
(
$answer
->type eq
'TXT'
) {
$log
=
join
(
''
,
$answer
->txtdata);
utf8::encode(
$log
)
if
utf8::is_utf8(
$log
);
local
$1;
$log
=~ s{ (?<! [<(\[] ) (https? : // \S+)}{<$1>}xgi;
}
else
{
local
($1,$2,$3,$4,$5);
if
(
$question
->string =~ /^((?:[0-9a-fA-F]\.){32})(\S+\w)/) {
$log
=
' listed in '
.
lc
($2);
my
$ipv6addr
=
join
(
''
,
reverse
split
(/\./,
lc
$1));
$ipv6addr
=~ s/\G(....)/$1:/g;
chop
$ipv6addr
;
$ipv6addr
=~ s/:0{1,3}/:/g;
$log
=
$ipv6addr
.
$log
;
}
elsif
(
$question
->string =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\S+\w)/) {
$log
=
"$4.$3.$2.$1 listed in "
.
lc
($5);
}
elsif
(
$question
->string =~ /^(\S+)(?<!\.)/) {
$log
=
"listed in "
.
lc
($1);
}
}
if
(
$log
) {
$self
->test_log(
$log
,
$rule
);
}
if
(!
$self
->{tests_already_hit}->{
$rule
}) {
dbg(
"dns: rbl rule $rule hit"
);
$self
->got_hit(
$rule
,
"RBL: "
,
ruletype
=>
"dnsbl"
);
}
}
sub
dnsbl_uri {
my
(
$self
,
$question
,
$answer
) =
@_
;
my
$rdatastr
;
if
(
$answer
->UNIVERSAL::can(
'txtdata'
)) {
$rdatastr
=
join
(
''
,
$answer
->txtdata);
}
else
{
$rdatastr
=
$answer
->rdstring;
$rdatastr
=~ s{ \\ ( [0-9]{3} | (?![0-9]{3}) . ) }
{
length
($1)==3 && $1 <= 255 ?
chr
($1) : $1 }xgse;
}
utf8::encode(
$rdatastr
)
if
utf8::is_utf8(
$rdatastr
);
my
$qname
=
$question
->qname;
if
(
defined
$qname
&&
defined
$rdatastr
) {
my
$qclass
=
$question
->qclass;
my
$qtype
=
$question
->qtype;
my
@vals
;
push
(
@vals
,
"class=$qclass"
)
if
$qclass
ne
"IN"
;
push
(
@vals
,
"type=$qtype"
)
if
$qtype
ne
"A"
;
my
$uri
=
"dns:$qname"
. (
@vals
?
"?"
.
join
(
";"
,
@vals
) :
""
);
$self
->{dnsuri}{
$uri
}{
$rdatastr
} = 1;
dbg(
"dns: hit <$uri> $rdatastr"
);
}
}
sub
process_dnsbl_result {
my
(
$self
,
$ent
,
$pkt
) =
@_
;
return
if
!
$pkt
;
my
$question
= (
$pkt
->question)[0];
return
if
!
$question
;
my
$rulename
=
$ent
->{rulename};
if
(!
$self
->get_async_pending_rules(
$rulename
)) {
$self
->rule_ready(
$rulename
);
if
(
exists
$self
->{rbl_subs}{
$ent
->{set}}) {
foreach
(@{
$self
->{rbl_subs}{
$ent
->{set}}}) {
$self
->rule_ready(
$_
->[1]);
}
}
}
foreach
my
$answer
(
$pkt
->answer) {
next
if
!
$answer
;
$self
->dnsbl_uri(
$question
,
$answer
);
my
$answ_type
=
$answer
->type;
next
if
$answ_type
ne
'A'
&&
$answ_type
ne
'TXT'
;
my
$rdatastr
;
if
(
$answer
->UNIVERSAL::can(
'txtdata'
)) {
$rdatastr
=
join
(
''
,
$answer
->txtdata);
}
else
{
$rdatastr
=
$answer
->rdstring;
$rdatastr
=~ s{ \\ ( [0-9]{3} | (?![0-9]{3}) . ) }
{
length
($1)==3 && $1 <= 255 ?
chr
($1) : $1 }xgse;
}
utf8::encode(
$rdatastr
)
if
utf8::is_utf8(
$rdatastr
);
next
if
$answ_type
eq
'A'
&&
$rdatastr
!~ /^127\./;
if
(
defined
$ent
->{subtest}) {
if
(
$self
->check_subtest(
$rdatastr
,
$ent
->{subtest})) {
$self
->dnsbl_hit(
$rulename
,
$question
,
$answer
);
}
}
else
{
$self
->dnsbl_hit(
$rulename
,
$question
,
$answer
);
}
if
(
exists
$self
->{rbl_subs}{
$ent
->{set}}) {
$self
->process_dnsbl_set(
$ent
->{set},
$question
,
$answer
,
$rdatastr
);
}
}
return
1;
}
sub
process_dnsbl_set {
my
(
$self
,
$set
,
$question
,
$answer
,
$rdatastr
) =
@_
;
foreach
my
$args
(@{
$self
->{rbl_subs}{
$set
}}) {
my
$subtest
=
$args
->[0];
my
$rule
=
$args
->[1];
next
if
$self
->{tests_already_hit}->{
$rule
};
if
(
$self
->check_subtest(
$rdatastr
,
$subtest
)) {
$self
->dnsbl_hit(
$rule
,
$question
,
$answer
);
}
}
}
sub
check_subtest {
my
(
$self
,
$rdatastr
,
$subtest
) =
@_
;
if
(
ref
(
$subtest
) eq
'Regexp'
) {
if
(
$rdatastr
=~
$subtest
) {
return
1;
}
}
elsif
(
$subtest
=~ /^\d+$/) {
if
(
$rdatastr
=~ m/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ &&
Mail::SpamAssassin::Util::my_inet_aton(
$rdatastr
) &
$subtest
)
{
return
1;
}
}
else
{
if
(
$subtest
eq
$rdatastr
) {
return
1;
}
}
return
0;
}
sub
harvest_until_rule_completes {}
sub
harvest_dnsbl_queries {
my
(
$self
) =
@_
;
dbg(
"dns: harvest_dnsbl_queries"
);
for
(
my
$first
=1; ;
$first
=0) {
my
(
$alldone
,
$anydone
) =
$self
->{async}->complete_lookups(
$first
? 0 : 1.0, 1);
last
if
$alldone
||
$self
->{deadline_exceeded} ||
$self
->{shortcircuited};
dbg(
"dns: harvest_dnsbl_queries - check_tick"
);
$self
->{main}->call_plugins (
"check_tick"
, {
permsgstatus
=>
$self
});
}
$self
->{async}->abort_remaining_lookups();
$self
->{async}->log_lookups_timing();
1;
}
sub
harvest_completed_queries {
my
(
$self
) =
@_
;
my
$last_poll_time
=
$self
->{async}->last_poll_responses_time();
return
if
defined
$last_poll_time
&&
time
-
$last_poll_time
< 0.1;
my
(
$alldone
,
$anydone
) =
$self
->{async}->complete_lookups(0, 0);
if
(
$anydone
) {
dbg(
"dns: harvested completed queries"
);
}
}
sub
set_rbl_tag_data {
my
(
$self
) =
@_
;
return
if
!
$self
->{dnsuri};
my
$rbl_tag
=
$self
->{tag_data}->{RBL};
$rbl_tag
=
''
if
!
defined
$rbl_tag
;
while
(
my
(
$dnsuri
,
$answers
) =
each
%{
$self
->{dnsuri}}) {
$rbl_tag
.=
"<$dnsuri>"
.
" ["
.
join
(
", "
,
keys
%$answers
) .
"]\n"
;
}
if
(
defined
$rbl_tag
&&
$rbl_tag
ne
''
) {
chomp
$rbl_tag
;
$self
->set_tag(
'RBL'
,
$rbl_tag
);
}
}
sub
rbl_finish {
my
(
$self
) =
@_
;
$self
->set_rbl_tag_data();
delete
$self
->{rbl_subs};
delete
$self
->{dnsuri};
}
sub
load_resolver {
my
(
$self
) =
@_
;
$self
->{resolver} =
$self
->{main}->{resolver};
return
$self
->{resolver}->load_resolver();
}
sub
clear_resolver {
my
(
$self
) =
@_
;
dbg(
"dns: clear_resolver"
);
$self
->{main}->{resolver}->{res} =
undef
;
return
0;
}
sub
lookup_ns {
warn
"dns: deprecated lookup_ns called, query ignored\n"
;
return
;
}
sub
test_dns_a_aaaa {
my
(
$self
,
$dom
) =
@_
;
return
if
(
$self
->server_failed_to_respond_for_domain (
$dom
));
my
(
$a
,
$aaaa
) = (0, 0);
if
(
$self
->{conf}->{dns_options}->{v4}) {
eval
{
my
$query
=
$self
->{resolver}->
send
(
$dom
,
'A'
);
if
(
$query
) {
foreach
my
$rr
(
$query
->answer) {
if
(
$rr
->type eq
'A'
) {
$a
= 1;
last
; }
}
}
1;
} or
do
{
my
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
dbg(
"dns: test A lookup failed horribly, perhaps bad resolv.conf setting? (%s)"
,
$eval_stat
);
return
(
undef
,
undef
);
};
if
(!
$a
) {
dbg(
"dns: test A lookup returned no results, use \"dns_options nov4\" if resolver doesn't support A queries"
);
}
}
else
{
$a
= 1;
}
if
(
$self
->{conf}->{dns_options}->{v6}) {
eval
{
my
$query
=
$self
->{resolver}->
send
(
$dom
,
'AAAA'
);
if
(
$query
) {
foreach
my
$rr
(
$query
->answer) {
if
(
$rr
->type eq
'AAAA'
) {
$aaaa
= 1;
last
; }
}
}
1;
} or
do
{
my
$eval_stat
= $@ ne
''
? $@ :
"errno=$!"
;
chomp
$eval_stat
;
dbg(
"dns: test AAAA lookup failed horribly, perhaps bad resolv.conf setting? (%s)"
,
$eval_stat
);
return
(
undef
,
undef
);
};
if
(!
$aaaa
) {
dbg(
"dns: test AAAA lookup returned no results, use \"dns_options nov6\" if resolver doesn't support AAAA queries"
);
}
}
else
{
$aaaa
= 1;
}
return
(
$a
,
$aaaa
);
}
sub
is_dns_available {
my
(
$self
) =
@_
;
my
$dnsopt
=
$self
->{conf}->{dns_available};
return
1
if
$IS_DNS_AVAILABLE
&&
$dnsopt
eq
"yes"
;
return
0
if
defined
$IS_DNS_AVAILABLE
&&
$dnsopt
eq
"no"
;
if
(!
$self
->{conf}->{dns_options}->{v4} &&
!
$self
->{conf}->{dns_options}->{v6})
{
warn
'dns: error: dns_options "nov4" and "nov6" are both set, '
.
' only use either, or use "dns_available no" to really disable DNS'
.
"\n"
;
$IS_DNS_AVAILABLE
= 0;
$self
->{conf}->{dns_available} =
"no"
;
return
0;
}
if
(
$dnsopt
eq
"test"
) {
my
$diff
=
time
-
$LAST_DNS_CHECK
;
if
(
$diff
> (
$self
->{conf}->{dns_test_interval}||600)) {
$IS_DNS_AVAILABLE
=
undef
;
if
(
$LAST_DNS_CHECK
) {
dbg(
"dns: is_dns_available() last checked %.1f seconds ago; re-checking"
,
$diff
);
}
else
{
dbg(
"dns: is_dns_available() initial check"
);
}
}
$LAST_DNS_CHECK
=
time
;
}
return
$IS_DNS_AVAILABLE
if
defined
$IS_DNS_AVAILABLE
;
$IS_DNS_AVAILABLE
= 0;
if
(
$dnsopt
eq
"no"
) {
dbg(
"dns: dns_available set to no in config file, skipping test"
);
return
$IS_DNS_AVAILABLE
;
}
if
(
$self
->{main}->{local_tests_only}) {
dbg(
"dns: using local tests only, DNS not available"
);
return
$IS_DNS_AVAILABLE
;
}
if
(!
$self
->load_resolver()) {
dbg(
"dns: could not load resolver, DNS not available"
);
return
$IS_DNS_AVAILABLE
;
}
if
(
$dnsopt
eq
"yes"
) {
if
(
$self
->{conf}->{dns_options}->{rotate}) {
my
@nameservers
=
$self
->{resolver}->available_nameservers();
Mail::SpamAssassin::Util::fisher_yates_shuffle(\
@nameservers
);
dbg(
"dns: shuffled NS list: "
.
join
(
", "
,
@nameservers
));
$self
->{resolver}->available_nameservers(
@nameservers
);
}
$IS_DNS_AVAILABLE
= 1;
dbg(
"dns: dns_available set to yes in config file, skipping test"
);
return
$IS_DNS_AVAILABLE
;
}
my
@domains
;
my
@rtypes
;
push
@rtypes
,
'A'
if
$self
->{main}->{conf}->{dns_options}->{v4};
push
@rtypes
,
'AAAA'
if
$self
->{main}->{conf}->{dns_options}->{v6};
if
(
$dnsopt
=~ /^test:\s*(\S.*)$/) {
@domains
=
split
(/\s+/, $1);
dbg(
"dns: testing %s records for user specified domains: %s"
,
join
(
"/"
,
@rtypes
),
join
(
", "
,
@domains
));
}
else
{
@domains
=
@EXISTING_DOMAINS
;
dbg(
"dns: testing %s records for built-in domains: %s"
,
join
(
"/"
,
@rtypes
),
join
(
", "
,
@domains
));
}
my
@nameservers
=
$self
->{resolver}->configured_nameservers();
if
(
$self
->{conf}->{dns_options}->{rotate}) {
Mail::SpamAssassin::Util::fisher_yates_shuffle(\
@nameservers
);
dbg(
"dns: shuffled NS list, testing: "
.
join
(
", "
,
@nameservers
));
}
else
{
dbg(
"dns: testing resolver nameservers: "
.
join
(
", "
,
@nameservers
));
}
my
@good_nameservers
;
foreach
my
$ns
(
@nameservers
) {
$self
->{resolver}->available_nameservers(
$ns
);
for
(
my
$retry
= 0;
$retry
< 3 &&
@domains
;
$retry
++) {
my
$domain
=
splice
(
@domains
,
rand
(
@domains
), 1);
dbg(
"dns: trying $domain, server $ns ..."
.
(
$retry
?
" (retry $retry)"
:
""
));
my
(
$ok_a
,
$ok_aaaa
) =
$self
->test_dns_a_aaaa(
$domain
);
$self
->{resolver}->finish_socket();
if
(!
defined
$ok_a
|| !
defined
$ok_aaaa
) {
last
;
}
elsif
(!
$ok_a
&& !
$ok_aaaa
) {
dbg(
"dns: lookup of $domain using $ns failed, no results found"
);
}
else
{
dbg(
"dns: lookup of $domain using $ns succeeded => DNS available"
.
" (set dns_available to override)"
);
push
(
@good_nameservers
,
$ns
);
last
;
}
}
}
if
(!
@good_nameservers
) {
dbg(
"dns: all NS queries failed => DNS unavailable "
.
"(set dns_available to override)"
);
}
else
{
$IS_DNS_AVAILABLE
= 1;
dbg(
"dns: NS list: "
.
join
(
", "
,
@good_nameservers
));
$self
->{resolver}->available_nameservers(
@good_nameservers
);
}
dbg(
"dns: is DNS available? "
.
$IS_DNS_AVAILABLE
);
return
$IS_DNS_AVAILABLE
;
}
sub
server_failed_to_respond_for_domain {
my
(
$self
,
$dom
) =
@_
;
if
(
$self
->{dns_server_too_slow}->{
$dom
}) {
dbg(
"dns: server for '$dom' failed to reply previously, not asking again"
);
return
1;
}
return
0;
}
sub
set_server_failed_to_respond_for_domain {
my
(
$self
,
$dom
) =
@_
;
dbg(
"dns: server for '$dom' failed to reply, marking as bad"
);
$self
->{dns_server_too_slow}->{
$dom
} = 1;
}
sub
enter_helper_run_mode {
my
(
$self
) =
@_
;
dbg(
"dns: entering helper-app run mode"
);
$self
->{old_slash} = $/;
%{
$self
->{old_env}} = ();
if
(
%ENV
) {
while
(
my
(
$key
,
$value
) =
each
%ENV
) {
$self
->{old_env}->{
$key
} =
$value
if
defined
$value
;
}
}
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
my
$newhome
;
if
(
$self
->{main}->{home_dir_for_helpers}) {
$newhome
=
$self
->{main}->{home_dir_for_helpers};
}
else
{
$newhome
= (Mail::SpamAssassin::Util::portable_getpwuid ($>))[7];
}
if
(
$newhome
) {
$ENV
{
'HOME'
} = Mail::SpamAssassin::Util::untaint_file_path (
$newhome
);
}
$self
->{old_sigchld_handler} =
$SIG
{CHLD};
$SIG
{CHLD} =
'DEFAULT'
;
}
sub
leave_helper_run_mode {
my
(
$self
) =
@_
;
dbg(
"dns: leaving helper-app run mode"
);
$/ =
$self
->{old_slash};
%ENV
= %{
$self
->{old_env}};
if
(
defined
$self
->{old_sigchld_handler}) {
$SIG
{CHLD} =
$self
->{old_sigchld_handler};
}
else
{
$SIG
{CHLD} =
'DEFAULT'
;
}
}
sub
cleanup_kids {
my
(
$self
,
$pid
) =
@_
;
if
(
$SIG
{CHLD} &&
$SIG
{CHLD} ne
'IGNORE'
) {
waitpid
(
$pid
, 0);
}
}
sub
register_async_rule_start {}
sub
register_async_rule_finish {}
sub
mark_all_async_rules_complete {}
sub
is_rule_complete {}
sub
get_async_pending_rules {
my
(
$self
,
$rule
) =
@_
;
if
(
defined
$rule
) {
return
0
if
!
exists
$self
->{async}->{pending_rules}{
$rule
};
return
scalar
keys
%{
$self
->{async}->{pending_rules}{
$rule
}};
}
else
{
return
grep
{ %{
$self
->{async}->{pending_rules}{
$_
}} }
keys
%{
$self
->{async}->{pending_rules}};
}
}
1;