Hi,
this patch adds a MAXLOAD checking for qpsmtpd-forksever, this currently
only works in Linux (if you aren't running Linux, send a diff to enable
this on your system :)). It also adresses the problem with the broken
clients.
To disable MAXLOAD and MAXCONNIP checks, use '--max-conn-ip 0' and
'--max-load 0'.
If you set '--deny-timeout 0' connections will be dropped immediately,
like the prevoius MAXCONNIP patch does... well not immediately, but
after a fork() to keep the code simpler, as most people will not set
--deny-timeout to 0 ;-) The deny-timeout just sits there and waits for a
QUIT, if the client doesn't send the QUIT within $DENYTIMEOUT seconds,
the connection is dropped. I think this is what the rblsmtpd does,
right?
Hanno
--- qpsmtpd-forkserver.orig Mon Jul 5 21:20:15 2004
+++ qpsmtpd-forkserver Sat Jul 10 11:02:26 2004
@@ -22,6 +22,12 @@
my $LOCALADDR = '0.0.0.0'; # ip address to bind to
my $USER = 'smtpd'; # user to suid to
my $MAXCONNIP = 5; # max simultaneous connections from one IP
+my $MAXLOAD = 5.0; # deny if current load is > than 5.0 (Linux)
+my $DENYTIMEOUT = 60; # wait 60 secs for client to disconnect
+ # if $MAXCONNIP or $MAXLOAD triggered the deny.
+ # set to 0 to drop connections after the
+ # deny message was sent (0 will confuse broken
+ # SMTPDs)
sub usage {
print <<"EOT";
@@ -29,7 +35,17 @@
-l, --listen-address addr : listen on a specific address; default 0.0.0.0
-p, --port P : listen on a specific port; default 25
-c, --limit-connections N : limit concurrent connections to N; default 15
- -u, --user U : run as a particular user (defualt 'smtpd')
+ -u, --user U : run as a particular user (default 'smtpd')
+ -m, --max-conn-ip N : max N connections from one IP (default 5)
+ -d, --deny-timeout N : wait max N seconds for client to 'QUIT' if
+ connection is denied for some reason (default 60)
+ if set to 0, drop connection after sending a deny
+ reason (this will confuse broken SMTPDs)
+ -L, --max-load F.F : deny connection if current laod is > F.F
+ (default 5.0)
+
+use '--max-conn-ip 0' and '--max-load 0.0' to disable these checks
+
EOT
exit 0;
}
@@ -38,13 +54,19 @@
'l|listen-address=s' => \$LOCALADDR,
'c|limit-connections=i' => \$MAXCONN,
'p|port=i' => \$PORT,
- 'u|user=s' => \$USER) || &usage;
+ 'u|user=s' => \$USER,
+ 'm|max-conn-ip=i' => \$MAXCONNIP,
+ 'd|deny-timeout=i' => \$DENYTIMEOUT,
+ 'L|max-load=f' => \$MAXLOAD, ) || &usage;
# detaint the commandline
if ($PORT =~ /^(\d+)$/) { $PORT = $1 } else { &usage }
if ($LOCALADDR =~ /^([\d\w\-.]+)$/) { $LOCALADDR = $1 } else { &usage }
if ($USER =~ /^([\w\-]+)$/) { $USER = $1 } else { &usage }
if ($MAXCONN =~ /^(\d+)$/) { $MAXCONN = $1 } else { &usage }
+if ($MAXCONNIP =~ /^(\d+)$/) { $MAXCONNIP = $1 } else { &usage }
+if ($MAXLOAD =~ /^(\d+(\.\d+)?)$/) { $MAXLOAD = $1 } else { &usage }
+if ($DENYTIMEOUT =~ /^(\d+)$/) { $DENYTIMEOUT = $1 } else { &usage }
delete $ENV{ENV};
$ENV{PATH} = '/bin:/usr/bin:/var/qmail/bin';
@@ -72,7 +94,7 @@
# establish SERVER socket, bind and listen.
my $server = IO::Socket::INET->new(LocalPort => $PORT,
- LocalAddr => $LOCALADDR,
+ LocalAddr => $LOCALADDR,
Proto => 'tcp',
Reuse => 1,
Listen => SOMAXCONN )
@@ -111,30 +133,54 @@
# possible something condition...
next;
}
+
my ($port, $iaddr) = sockaddr_in($hisaddr);
- if ($MAXCONNIP) {
- my $num_conn = 0;
- foreach my $rip (values %childstatus) {
- if ($rip eq $iaddr) {
- ++$num_conn;
- }
+ my $deny_reason = 0;
+
+ if ($MAXLOAD) {
+ my $cur_load = 0;
+ if ($^O =~ /^linux$/i) {
+ if (open PROC_LOADAVG, '/proc/loadavg') {
+ ($cur_load = <PROC_LOADAVG>) =~ s/^\s*(\d+\.\d+)\s.*$/$1/;
+ close PROC_LOADAVG;
}
- ++$num_conn; # count this connection, too :)
- if ($num_conn > $MAXCONNIP) {
- my $rem_ip = inet_ntoa($iaddr);
- ::log(LOGINFO,"Too many connections from $rem_ip: "
- ."$num_conn > $MAXCONNIP. Denying connection.");
- $client->autoflush(1);
- print $client "451 Sorry, too many connections from $rem_ip, try again
later\r\n";
- close $client;
- next;
+ }
+ # elsif ($^O =~ /... /) { ### any volunteers? ;-)
+ #
+ # }
+ if ($cur_load > $MAXLOAD) {
+ my $rem_ip = inet_ntoa($iaddr);
+ ::log(LOGINFO,"Load too high ($cur_load > $MAXLOAD), denying "
+ ."connection from $rem_ip");
+ $deny_reason = "451 Sorry, current work load is too high, "
+ ."please try again later\r\n";
+ goto PRE_FORK;
+ }
+ } # END if ($MAXLOAD) {
+
+ if ($MAXCONNIP) {
+ my $num_conn = 0;
+ foreach my $rip (values %childstatus) {
+ if ($rip eq $iaddr) {
+ ++$num_conn;
}
- }
+ }
+ ++$num_conn; # count this connection, too :)
+ if ($num_conn > $MAXCONNIP) {
+ my $rem_ip = inet_ntoa($iaddr);
+ ::log(LOGINFO,"Too many connections from $rem_ip: "
+ ."$num_conn > $MAXCONNIP. Denying connection.");
+ $deny_reason = "451 Sorry, too many connections from $rem_ip, "
+ ."try again later\r\n";
+ goto PRE_FORK;
+ }
+ } # END if ($MAXCONNIP) {
+
+ PRE_FORK:
my $pid = fork;
if ($pid) {
# parent
$childstatus{$pid} = $iaddr; # add to table
- # $childstatus{$pid} = 1; # add to table
$running++;
close($client);
next;
@@ -143,9 +189,23 @@
# otherwise child
close($server);
-
$SIG{$_} = 'DEFAULT' for keys %SIG;
+
+ # don't do this!
+ #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}";
+ # dup to STDIN/STDOUT
+ POSIX::dup2(fileno($client), 0);
+ POSIX::dup2(fileno($client), 1);
+
+ if ($deny_reason) {
+ $client->autoflush(1);
+ print $client $deny_reason;
+ &wait4quit($client,$deny_reason) if $DENYTIMEOUT;
+ # else drop connection immediately
+ exit;
+ }
+
my $localsockaddr = getsockname($client);
my ($lport, $laddr) = sockaddr_in($localsockaddr);
$ENV{TCPLOCALIP} = inet_ntoa($laddr);
@@ -153,19 +213,10 @@
$ENV{TCPREMOTEIP} = inet_ntoa($iaddr);
$ENV{TCPREMOTEHOST} = gethostbyaddr($iaddr, AF_INET) || "Unknown";
- # don't do this!
- #$0 = "qpsmtpd-forkserver: $ENV{TCPREMOTEIP} / $ENV{TCPREMOTEHOST}";
-
::log(LOGINFO, "Accepted connection $running/$MAXCONN from $ENV{TCPREMOTEIP} /
$ENV{TCPREMOTEHOST}");
-
- # dup to STDIN/STDOUT
- POSIX::dup2(fileno($client), 0);
- POSIX::dup2(fileno($client), 1);
-
my $qpsmtpd = Qpsmtpd::TcpServer->new();
$qpsmtpd->start_connection();
$qpsmtpd->run();
-
exit; # child leaves
}
@@ -173,6 +224,21 @@
my ($level,$message) = @_;
# $level not used yet. this is reimplemented from elsewhere anyway
warn("$$ $message\n");
+}
+
+sub wait4quit {
+ my ($client,$msg) = @_;
+ alarm $DENYTIMEOUT;
+ while (<$client>) {
+ unless (/^\s*QUIT\b\s*/i) {
+ print $client $msg;
+ } else {
+ alarm 0; # do we need this?
+ print $client "250 Ok, see you later\r\n";
+ close $client;
+ last;
+ }
+ }
}
__END__