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__

Reply via email to