| 43 |
|
|
| 44 |
=head1 NAME |
=head1 NAME |
| 45 |
|
|
| 46 |
greylist-tng.pl - weighted greylisting based on information we can obtain |
gandalf - weighted greylisting based on information we can obtain |
| 47 |
before DATA stage of the SMTP process. |
before DATA stage of the SMTP process. |
| 48 |
|
|
| 49 |
=head1 SYNOPSIS |
=head1 SYNOPSIS |
| 50 |
|
|
| 51 |
[options] |
gandalf [options] |
| 52 |
|
|
| 53 |
Options: |
Options: |
| 54 |
--debug, -d debugging level (Default 0) |
--daemonize, -d daemonize (default) |
| 55 |
|
--pidfile location of pidfile (/var/run/gandalf/gandalf.pid) |
| 56 |
|
--socket location of socket (/var/run/gandalf/gandalf.sock) |
| 57 |
|
--host host/port to use for tcp sockets |
| 58 |
|
--max-connections maximum connections to allow per socket |
| 59 |
|
--pidlock lockfile for pid (pidfile.lock) |
| 60 |
|
--verbose, -v verbosity level (Default 0) |
| 61 |
|
--debug, -D debugging level (Default 0) |
| 62 |
--help, -h display this help |
--help, -h display this help |
| 63 |
--man, -m display manual |
--man, -m display manual |
| 64 |
|
|
| 66 |
|
|
| 67 |
=over |
=over |
| 68 |
|
|
| 69 |
=item B<--debug, -d> |
=item B<--daemonize,-d> |
| 70 |
|
|
| 71 |
|
Whether to daemonize (default). --no-daemonize to disable |
| 72 |
|
|
| 73 |
|
=item B<--pidfile> |
| 74 |
|
|
| 75 |
|
Pidfile location; defaults to /var/run/gandalf/gandalf.pid |
| 76 |
|
|
| 77 |
|
=item B<--socket> |
| 78 |
|
|
| 79 |
|
Socket location; defaults to /var/run/gandalf/gandalf.sock if no other |
| 80 |
|
socket or host is specified. |
| 81 |
|
|
| 82 |
|
=item B<--host> |
| 83 |
|
|
| 84 |
|
Host to listen on; specified as hostname:portnum (or ip:portnum). |
| 85 |
|
|
| 86 |
|
=item B<--max-connections> |
| 87 |
|
|
| 88 |
|
Maximum connections to allow to tcp ports; defaults to 30. |
| 89 |
|
|
| 90 |
|
=item B<--pidlock> |
| 91 |
|
|
| 92 |
|
Lockfile for the pidfile; defaults to the location of the pidfile with |
| 93 |
|
.lock appended. |
| 94 |
|
|
| 95 |
|
=item B<--debug, -D> |
| 96 |
|
|
| 97 |
Debug verbosity. (Default 0) |
Debug verbosity. (Default 0) |
| 98 |
|
|
| 112 |
=cut |
=cut |
| 113 |
|
|
| 114 |
|
|
| 115 |
use Fcntl; |
use Fcntl qw(:flock); |
| 116 |
#use BerkeleyDB; |
#use BerkeleyDB; |
| 117 |
use Digest::MD5 qw(md5 md5_hex); |
use Digest::MD5 qw(md5 md5_hex); |
| 118 |
use IO::Socket::INET; |
use IO::Socket::INET; |
| 119 |
|
use IO::Socket::UNIX; |
| 120 |
use IO::Select; |
use IO::Select; |
| 121 |
|
use IO::File; |
| 122 |
use Net::DNS; |
use Net::DNS; |
| 123 |
use POSIX; |
use POSIX qw(strftime setsid); |
| 124 |
use Sys::Syslog qw(:DEFAULT setlogsock); |
use Sys::Syslog qw(:DEFAULT setlogsock); |
| 125 |
use Params::Validate qw(:types validate_with); |
use Params::Validate qw(:types validate_with); |
| 126 |
|
|
| 130 |
|
|
| 131 |
use constant {BAD => 0, GOOD => 1}; |
use constant {BAD => 0, GOOD => 1}; |
| 132 |
|
|
| 133 |
|
|
| 134 |
use vars qw($DEBUG); |
use vars qw($DEBUG); |
| 135 |
|
|
| 136 |
my %options = (debug => 0, |
my %options = (debug => 0, |
| 137 |
help => 0, |
help => 0, |
| 138 |
man => 0, |
man => 0, |
| 139 |
|
verbose => 0, |
| 140 |
|
daemonize => 1, |
| 141 |
|
pidfile => '/var/run/gandalf/gandalf.pid', |
| 142 |
|
pidlock => undef, |
| 143 |
|
socket => [], |
| 144 |
|
host => [], |
| 145 |
|
max_connections => 30, |
| 146 |
); |
); |
| 147 |
|
|
| 148 |
GetOptions(\%options,'debug|d+','help|h|?','man|m'); |
GetOptions(\%options,'debug|D+','help|h|?','man|m', |
| 149 |
|
'verbose|v+', |
| 150 |
|
'daemonize|daemon|d!', |
| 151 |
|
'pidfile=s', |
| 152 |
|
'pidlock=s', |
| 153 |
|
'max_connections|max-connections=s', |
| 154 |
|
'host=s@', |
| 155 |
|
'socket=s@', |
| 156 |
|
); |
| 157 |
|
if (not defined $options{pidlock}) { |
| 158 |
|
$options{pidlock} = $options{pidfile}.'.lock'; |
| 159 |
|
} |
| 160 |
|
|
| 161 |
|
if (not @{$options{socket}} and |
| 162 |
|
not @{$options{host}}) { |
| 163 |
|
push @{$options{socket}},'/var/run/gandalf/gandalf.sock'; |
| 164 |
|
} |
| 165 |
|
|
| 166 |
pod2usage() if $options{help}; |
pod2usage() if $options{help}; |
| 167 |
pod2usage({verbose=>2}) if $options{man}; |
pod2usage({verbose=>2}) if $options{man}; |
| 168 |
|
|
| 169 |
$DEBUG = $options{debug}; |
$DEBUG = $options{debug}; |
| 170 |
|
|
| 171 |
|
if ($options{daemonize}) { |
| 172 |
|
my $pidfh; |
| 173 |
|
if ($options{pidfile}) { |
| 174 |
|
my $pidlock = new IO::File->new($options{pidlock},'w') or |
| 175 |
|
die "Unable to open pidlock $options{pidlock} for writing: $!"; |
| 176 |
|
flock($pidlock,LOCK_EX) or |
| 177 |
|
die "Unable to lock pidlock $options{pidlock}: $!"; |
| 178 |
|
if (-e $options{pidfile}) { |
| 179 |
|
$pidfh = IO::File->new($options{pidfile},'r') or |
| 180 |
|
die "Unable to open pidfile $options{pidfile} for reading: $!"; |
| 181 |
|
local $/; |
| 182 |
|
my $pid = <$pidfh>; |
| 183 |
|
($pid) = $pid =~ /(\d+)/; |
| 184 |
|
if (defined $pid and kill(0,$pid)) { |
| 185 |
|
print STDERR "Copy of $0 running with pid $pid (pidfile: $options{pidfile})"; |
| 186 |
|
unlink($options{pidlock}); |
| 187 |
|
undef $pidlock; |
| 188 |
|
exit 1; |
| 189 |
|
} |
| 190 |
|
close $pidfh; |
| 191 |
|
unlink ($options{pidfile}) or |
| 192 |
|
die "Unable to unlink stale pidfile $options{pidfile}: $!"; |
| 193 |
|
} |
| 194 |
|
$pidfh = IO::File->new($options{pidfile},'w') or |
| 195 |
|
die "Unable to open $options{pidfile} for writing: $!"; |
| 196 |
|
} |
| 197 |
|
# daemonize |
| 198 |
|
chdir '/' or die "Can't chdir to /: $!"; |
| 199 |
|
open STDIN, '/dev/null' or die "Can't read /dev/null: $!"; |
| 200 |
|
open STDOUT, '>/dev/null' |
| 201 |
|
or die "Can't write to /dev/null: $!"; |
| 202 |
|
defined(my $pid = fork) or die "Can't fork: $!"; |
| 203 |
|
exit if $pid; |
| 204 |
|
setsid or die "Can't start a new session: $!"; |
| 205 |
|
if (defined $pidfh) { |
| 206 |
|
print {$pidfh} $$ or die "Unable to write to pidfile $options{pidfile}: $!"; |
| 207 |
|
close $pidfh or die "Unable to close pidfile $options{pidfile}: $!"; |
| 208 |
|
} |
| 209 |
|
open STDERR, '>&STDOUT' or die "Can't dup stdout: $!"; |
| 210 |
|
} |
| 211 |
|
|
| 212 |
|
|
| 213 |
my $tcp_port = 10025; |
my $tcp_port = 10025; |
| 214 |
my $bind_address = "localhost"; |
my $bind_address = "localhost"; |
| 682 |
# announce decision |
# announce decision |
| 683 |
} |
} |
| 684 |
|
|
| 685 |
sub parse_input($) { |
# sub parse_input($) { |
| 686 |
my $attr = shift(); |
# my $attr = shift(); |
| 687 |
my $response; |
# my $response; |
| 688 |
my $score=0; |
# my $score=0; |
| 689 |
|
# |
| 690 |
if (defined(read_database($attr))) { |
# if (defined(read_database($attr))) { |
| 691 |
$response = read_database($attr); |
# $response = read_database($attr); |
| 692 |
if ($response) { |
# if ($response) { |
| 693 |
return "dunno"; |
# return "dunno"; |
| 694 |
} else { |
# } else { |
| 695 |
return "defer_if_permit Service temporarily unavailable"; |
# return "defer_if_permit Service temporarily unavailable"; |
| 696 |
} |
# } |
| 697 |
} else { |
# } else { |
| 698 |
# Test: Is client's IP listed in some RBL lists? |
# # Test: Is client's IP listed in some RBL lists? |
| 699 |
$score = $score + test_rbldns($attr->{client_address}); |
# $score = $score + test_rbldns($attr->{client_address}); |
| 700 |
|
# |
| 701 |
# Test: Is sender listed in some RH RBL lists? |
# # Test: Is sender listed in some RH RBL lists? |
| 702 |
$score = $score + test_rhrbldns($attr->{sender}); |
# $score = $score + test_rhrbldns($attr->{sender}); |
| 703 |
|
# |
| 704 |
# Test: is HELO numeric? |
# # Test: is HELO numeric? |
| 705 |
if (test_helo_numeric($attr->{helo_name})) { |
# if (test_helo_numeric($attr->{helo_name})) { |
| 706 |
$score = $score + $helo_numeric_score[BAD]; |
# $score = $score + $helo_numeric_score[BAD]; |
| 707 |
} else { |
# } else { |
| 708 |
# Test: Reverse IP == HELO check? |
# # Test: Reverse IP == HELO check? |
| 709 |
$score = $score + test_helo_reverse($attr->{helo_name}, $attr->{reverse_client_name}); |
# $score = $score + test_helo_reverse($attr->{helo_name}, $attr->{reverse_client_name}); |
| 710 |
} |
# } |
| 711 |
|
# |
| 712 |
if ($client_seems_dialup == 1) { |
# if ($client_seems_dialup == 1) { |
| 713 |
$score = $score + test_helo_seems_dialup($attr->{helo_name}); |
# $score = $score + test_helo_seems_dialup($attr->{helo_name}); |
| 714 |
} |
# } |
| 715 |
|
# |
| 716 |
# Test: is our mail coming from a potential daemon? |
# # Test: is our mail coming from a potential daemon? |
| 717 |
$score = $score + test_sender_anonymous($attr->{sender}); |
# $score = $score + test_sender_anonymous($attr->{sender}); |
| 718 |
|
# |
| 719 |
# Test: is our mail coming from a domain with SPF records? |
# # Test: is our mail coming from a domain with SPF records? |
| 720 |
$score = $score + test_sender_spf($attr->{sender}); |
# $score = $score + test_sender_spf($attr->{sender}); |
| 721 |
|
# |
| 722 |
if ($score > $cutoff) { |
# if ($score > $cutoff) { |
| 723 |
write_database($attr); |
# write_database($attr); |
| 724 |
return "defer_if_permit Service temporarily unavailable"; |
# return "defer_if_permit Service temporarily unavailable"; |
| 725 |
} else { |
# } else { |
| 726 |
return "PREPEND X-Greylisting: score is $score"; |
# return "PREPEND X-Greylisting: score is $score"; |
| 727 |
} |
# } |
| 728 |
|
# |
| 729 |
} |
# } |
| 730 |
|
# |
| 731 |
|
# # no strict 'refs'; |
| 732 |
|
# # $response = weighted_check->(attr=>\%attr); |
| 733 |
|
# } |
| 734 |
|
|
| 735 |
# no strict 'refs'; |
$SIG{INT} = \&cleanup; |
|
# $response = weighted_check->(attr=>\%attr); |
|
|
} |
|
| 736 |
|
|
| 737 |
|
|
| 738 |
sub main { |
sub main { |
| 739 |
my $tcp_socket = IO::Socket::INET->new( |
# Create the sockets |
| 740 |
Proto => 'tcp', |
my @sockets; |
| 741 |
LocalHost => $bind_address, |
# handle unix sockets |
| 742 |
LocalPort => $tcp_port, |
for my $socket_file (@{$options{socket}}) { |
| 743 |
Listen => $max_connection, |
my $socket = |
| 744 |
Reuse => 1) or |
IO::Socket::UNIX->new(#Type => SOCK_STREAM, |
| 745 |
die "master: bind $tcp_port: $@ $!"; |
Local => $socket_file, |
| 746 |
|
Listen => 1, |
| 747 |
my $env = BerkeleyDB::Env->new( |
) |
| 748 |
-Home => $dbdir, |
or die "Unable to create socket for $socket_file"; |
| 749 |
-Flags => DB_CREATE|DB_RECOVER|DB_INIT_TXN|DB_INIT_MPOOL|DB_INIT_LOG, |
push @sockets,$socket; |
| 750 |
) or die "ERROR: can't create DB environment: $!\n"; |
} |
| 751 |
|
for my $socket_host (@{$options{host}}) { |
| 752 |
tie %db, 'BerkeleyDB::Btree', |
# figure out hostname/port |
| 753 |
-Filename => $dbname, |
my ($host,$port) = $socket_host =~ /(.+)\:(\d+)$/; |
| 754 |
-Flags => DB_CREATE, |
if (not defined $host or not defined $port) { |
| 755 |
-Env => $env, |
die "Socket host $socket_host not foohost:23 format"; |
| 756 |
or die "ERROR: can't create database $dbdir/$dbname: $!\n"; |
} |
| 757 |
|
my $socket = |
| 758 |
my %attr; |
IO::Socket::INET->new(Proto => 'tcp', |
| 759 |
# FIXME: catch TCPSocketInUseExeption e |
LocalHost => $host, |
| 760 |
my $read_set = new IO::Select(); |
LocalPort => $port, |
| 761 |
$read_set->add($tcp_socket); |
Listen => $options{max_connections}, |
| 762 |
# end FIXME |
Reuse => 1) |
| 763 |
# |
or die "master: bind $tcp_port: $@ $!"; |
| 764 |
# FIXME: endless loop... grrr |
push @sockets,$socket; |
| 765 |
while (1) { |
} |
| 766 |
# get a set of readable handles (blocks until at least one handle is ready) |
my %attr; |
| 767 |
my ($rh_set) = IO::Select->select($read_set, undef, undef, 0); |
# FIXME: catch TCPSocketInUseExeption e |
| 768 |
|
my $read_set = IO::Select->new() or |
| 769 |
|
die "Unable to create IO::Select"; |
| 770 |
|
$read_set->add(@sockets); |
| 771 |
|
my %listen_sockets; |
| 772 |
|
# keep track of which sockets are listen sockets |
| 773 |
|
@listen_sockets{@sockets} = (1) x @sockets; |
| 774 |
|
# end FIXME |
| 775 |
|
# |
| 776 |
|
my %request_sockets; |
| 777 |
|
# FIXME: endless loop... grrr |
| 778 |
|
my @reads; |
| 779 |
|
# get a set of readable handles (blocks until at least one handle is ready) |
| 780 |
|
while (@reads = $read_set->can_read()) { |
| 781 |
# take all readable handles in turn |
# take all readable handles in turn |
| 782 |
foreach my $rh (@$rh_set) { |
foreach my $rh (@reads) { |
| 783 |
# if it is the main socket then we have an incoming connection and |
# if it is the main socket then we have an incoming connection and |
| 784 |
# we should accept() it and then add the new socket to the $read_set |
# we should accept() it and then add the new socket to the $read_set |
| 785 |
if ($rh == $tcp_socket) { |
if ($listen_sockets{$rh}) { |
| 786 |
my $ns = $rh->accept(); |
my $ns = $rh->accept(); |
| 787 |
$read_set->add($ns); |
$read_set->add($ns); |
| 788 |
|
$request_sockets{$ns} = {}; |
| 789 |
} else { |
} else { |
| 790 |
# otherwise it is an ordinary socket and we should read and process the request |
# otherwise it is an ordinary socket and we should read and process the request |
| 791 |
my $buf = <$rh>; |
my $buf = <$rh>; |
| 794 |
# we get a newline, no more information will come from |
# we get a newline, no more information will come from |
| 795 |
# postfix for this mail |
# postfix for this mail |
| 796 |
# postfix now waits for my answer |
# postfix now waits for my answer |
| 797 |
if (%attr) { |
if ($request_sockets{$rh}{variables}) { |
| 798 |
if ($verbose) { |
if ($options{verbose}) { |
| 799 |
for (keys %attr) { |
while (my ($key,$value) = |
| 800 |
syslog $syslog_priority, "Key: %s, Value: %s", $_, $attr{$_}; |
each %{$request_sockets{$rh}{variables}} |
| 801 |
} |
) { |
| 802 |
|
syslog $syslog_priority, "Key: %s, Value: %s", $key,$value; |
| 803 |
|
} |
| 804 |
} |
} |
| 805 |
my $action = parse_input(\%attr); |
#my $action = parse_input(\%attr); |
| 806 |
|
my $action = "PREPEND X-Greylisting: disabled"; |
| 807 |
|
print STDERR "Handled $action\n"; |
| 808 |
$rh->send("action=".$action."\r\n"); |
$rh->send("action=".$action."\r\n"); |
| 809 |
$rh->send("\r\n"); |
$rh->send("\r\n"); |
| 810 |
%attr=(); |
delete $request_sockets{$rh}{variables}; |
| 811 |
} |
} |
| 812 |
} elsif ( $buf =~ /([^=]+)=(.*)\r\n/ ) { |
} elsif ( $buf =~ /([^=]+)=(.*)\r\n/ ) { |
| 813 |
# we get valid input from postfix |
# we get valid input from postfix |
| 814 |
$attr{substr($1, 0, 512)} = substr($2, 0, 512); |
$request_sockets{$rh}{variables}{substr($1, 0, 512)} = substr($2, 0, 512); |
| 815 |
} else { |
} else { |
| 816 |
syslog $syslog_priority, "warning: ignoring garbage: %.100s", $buf; |
syslog $syslog_priority, "warning: ignoring garbage: %.100s", $buf; |
| 817 |
} |
} |
| 823 |
} |
} |
| 824 |
} |
} |
| 825 |
} |
} |
|
|
|
|
untie %db; |
|
| 826 |
} |
} |
| 827 |
|
|
| 828 |
main(); |
main(); |
| 829 |
|
|
| 830 |
|
sub cleanup { |
| 831 |
|
print STDERR "got sig int\n"; |
| 832 |
|
exit 0; |
| 833 |
|
} |
| 834 |
|
|
| 835 |
|
END { |
| 836 |
|
for my $socket_file (@{$options{socket}}) { |
| 837 |
|
print STDERR "unlinking $socket_file\n"; |
| 838 |
|
unlink($socket_file) if -e $socket_file; |
| 839 |
|
} |
| 840 |
|
} |
| 841 |
|
|
| 842 |
__END__ |
__END__ |