Imported Bug 1057 multiple clamd patch from PLD repo
authorTodd Lyons <tlyons@exim.org>
Fri, 9 Nov 2012 22:28:37 +0000 (14:28 -0800)
committerTodd Lyons <tlyons@exim.org>
Wed, 9 Oct 2013 15:24:04 +0000 (08:24 -0700)
src/src/malware.c

index 994c62993d26f857f7c1df90ace770588d2faf69..3660476d27d46a7514fa14e46775191a578fc736 100644 (file)
 #include "exim.h"
 #ifdef WITH_CONTENT_SCAN
 
+/* The maximum number of clamd servers that are supported in the configuration */
+#define MAX_CLAMD_SERVERS 32
+#define MAX_CLAMD_SERVERS_S "32"
+/* Maximum length of the hostname that can be specified in the clamd address list */
+#define MAX_CLAMD_ADDRESS_LENGTH 64
+#define MAX_CLAMD_ADDRESS_LENGTH_S "64"
+
+typedef struct clamd_address_container {
+  uschar tcp_addr[MAX_CLAMD_ADDRESS_LENGTH];
+  unsigned int tcp_port;
+} clamd_address_container;
+
 /* declaration of private routines */
 static int mksd_scan_packed(int sock, uschar *scan_filename);
 static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking);
@@ -1301,7 +1313,7 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
      * WITH_OLD_CLAMAV_STREAM is defined.
      * See Exim bug 926 for details.  */
     else if (strcmpic(scanner_name,US"clamd") == 0) {
-      uschar *clamd_options;
+      uschar *clamd_options = NULL;
       uschar clamd_options_buffer[1024];
       uschar clamd_options_default[] = "/tmp/clamd";
       uschar *p, *vname, *result_tag, *response_end;
@@ -1310,16 +1322,16 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
       unsigned int port;
       uschar file_name[1024];
       uschar av_buffer[1024];
-      uschar hostname[256];
+      uschar *hostname = "";
       struct hostent *he;
       struct in_addr in;
-      uschar *clamd_options2;
-      uschar clamd_options2_buffer[1024];
-      uschar clamd_options2_default[] = "";
       uschar *clamav_fbuf;
       int clam_fd, result;
       unsigned int fsize;
-      BOOL use_scan_command, fits;
+      BOOL use_scan_command = FALSE, fits;
+      clamd_address_container * clamd_address_vector[MAX_CLAMD_SERVERS];
+      int current_server;
+      int num_servers = 0;
 #ifdef WITH_OLD_CLAMAV_STREAM
       uschar av_buffer2[1024];
       int sockData;
@@ -1333,16 +1345,60 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
         /* no options supplied, use default options */
         clamd_options = clamd_options_default;
       }
-      if ((clamd_options2 = string_nextinlist(&av_scanner_work, &sep,
-                                             clamd_options2_buffer,
-                                             sizeof(clamd_options2_buffer))) == NULL) {
-        clamd_options2 = clamd_options2_default;
-      }
 
-      if ((*clamd_options == '/') || (strcmpic(clamd_options2,US"local") == 0))
+      if (*clamd_options == '/')
+        /* Local file; so we def want to use_scan_command and don't want to try
+         * passing IP/port combinations */
         use_scan_command = TRUE;
-      else
-        use_scan_command = FALSE;
+      else {
+        uschar *address = clamd_options;
+        uschar address_buffer[MAX_CLAMD_ADDRESS_LENGTH + 20];
+
+        /* Go through the rest of the list of host/port and construct an array
+         * of servers to try. The first one is the bit we just passed from
+         * clamd_options so process that first and then scan the remainder of
+         * the address buffer */
+        do {
+          clamd_address_container *this_clamd;
+
+          /* The 'local' option means use the SCAN command over the network
+           * socket (ie common file storage in use) */
+          if (strcmpic(address,US"local") == 0) {
+            use_scan_command = TRUE;
+            continue;
+          }
+
+          /* XXX: If unsuccessful we should free this memory */
+          this_clamd =
+              (clamd_address_container *)store_get(sizeof(clamd_address_container));
+
+          /* extract host and port part */
+          if( sscanf(CS address, "%" MAX_CLAMD_ADDRESS_LENGTH_S "s %u", this_clamd->tcp_addr,
+                                            &(this_clamd->tcp_port)) != 2 ) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                      "malware acl condition: clamd: invalid address '%s'", address);
+            continue;
+          }
+
+          clamd_address_vector[num_servers] = this_clamd;
+          num_servers++;
+          if (num_servers >= MAX_CLAMD_SERVERS) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                  "More than " MAX_CLAMD_SERVERS_S " clamd servers specified; "
+                  "only using the first " MAX_CLAMD_SERVERS_S );
+            break;
+          }
+        } while ((address = string_nextinlist(&av_scanner_work, &sep,
+                                        address_buffer,
+                                        sizeof(address_buffer))) != NULL);
+
+        /* check if we have at least one server */
+        if (!num_servers) {
+          log_write(0, LOG_MAIN|LOG_PANIC,
+             "malware acl condition: clamd: no useable clamd server addresses in malware configuration option.");
+          return DEFER;
+        }
+      }
 
       /* See the discussion of response formats below to see why we really don't
       like colons in filenames when passing filenames to ClamAV. */
@@ -1353,45 +1409,72 @@ static int malware_internal(uschar **listptr, uschar *eml_filename, BOOL faking)
        return DEFER;
       }
 
-      /* socket does not start with '/' -> network socket */
-      if (*clamd_options != '/') {
+      /* We have some network servers specified */
+      if (num_servers) {
 
         /* Confirmed in ClamAV source (0.95.3) that the TCPAddr option of clamd
          * only supports AF_INET, but we should probably be looking to the
          * future and rewriting this to be protocol-independent anyway. */
 
-        /* extract host and port part */
-        if( sscanf(CS clamd_options, "%s %u", hostname, &port) != 2 ) {
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: invalid socket '%s'", clamd_options);
-          return DEFER;
-        };
+        while ( num_servers > 0 ) {
+          /* Randomly pick a server to start with */
+          current_server = random_number( num_servers );
 
-        /* Lookup the host */
-        if((he = gethostbyname(CS hostname)) == 0) {
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: failed to lookup host '%s'", hostname);
-          return DEFER;
-        }
+          debug_printf("trying server name %s, port %u\n",
+                       clamd_address_vector[current_server]->tcp_addr,
+                       clamd_address_vector[current_server]->tcp_port);
 
-        in = *(struct in_addr *) he->h_addr_list[0];
+          /* Lookup the host. This is to ensure that we connect to the same IP
+           * on both connections (as one host could resolve to multiple ips) */
+          if((he = gethostbyname(CS clamd_address_vector[current_server]->tcp_addr))
+                          == 0) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                    "malware acl condition: clamd: failed to lookup host '%s'",
+                    clamd_address_vector[current_server]->tcp_addr
+                    );
+            goto try_next_server;
+          }
 
-        /* Open the ClamAV Socket */
-        if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: unable to acquire socket (%s)",
-                    strerror(errno));
-          return DEFER;
-        }
+          in = *(struct in_addr *) he->h_addr_list[0];
 
-        if (ip_connect(sock, AF_INET, (uschar*)inet_ntoa(in), port, 5) < 0) {
-          (void)close(sock);
-          log_write(0, LOG_MAIN|LOG_PANIC,
-                    "malware acl condition: clamd: connection to %s, port %u failed (%s)",
-                    inet_ntoa(in), port, strerror(errno));
-          return DEFER;
+          /* Open the ClamAV Socket */
+          if ( (sock = ip_socket(SOCK_STREAM, AF_INET)) < 0) {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+                      "malware acl condition: clamd: unable to acquire socket (%s)",
+                      strerror(errno));
+            goto try_next_server;
+          }
+
+          if (ip_connect( sock,
+                          AF_INET,
+                          (uschar*)inet_ntoa(in),
+                          clamd_address_vector[current_server]->tcp_port,
+                          5 ) > -1) {
+            /* Connection successfully established with a server */
+            hostname = clamd_address_vector[current_server]->tcp_addr;
+            break;
+          } else {
+            log_write(0, LOG_MAIN|LOG_PANIC,
+               "malware acl condition: clamd: connection to %s, port %u failed (%s)",
+               clamd_address_vector[current_server]->tcp_addr,
+               clamd_address_vector[current_server]->tcp_port,
+               strerror(errno));
+
+            (void)close(sock);
+          }
+
+try_next_server:
+          /* Remove the server from the list. XXX We should free the memory */
+          num_servers--;
+          int i;
+          for( i = current_server; i < num_servers; i++ )
+            clamd_address_vector[i] = clamd_address_vector[i+1];
         }
 
+        if ( num_servers == 0 ) {
+          log_write(0, LOG_MAIN|LOG_PANIC, "malware acl condition: all clamd servers failed");
+            return DEFER;
+        }
       } else {
         /* open the local socket */
         if ((sock = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {