Proxy Protocol - Server support
[exim.git] / src / src / smtp_in.c
index e3746d99db3147db7e316e6c2f7359dc9d112b5e..144ad28a22e82fef88638a6fd5326eca8b130404 100644 (file)
@@ -94,6 +94,10 @@ enum {
 
   QUIT_CMD, HELP_CMD,
 
+#ifdef EXPERIMENTAL_PROXY
+  PROXY_FAIL_IGNORE_CMD,
+#endif
+
   /* These are specials that don't correspond to actual commands */
 
   EOF_CMD, OTHER_CMD, BADARG_CMD, BADCHAR_CMD, BADSYN_CMD,
@@ -210,7 +214,10 @@ static uschar *protocols[] = {
 /* Sanity check and validate optional args to MAIL FROM: envelope */
 enum {
   ENV_MAIL_OPT_SIZE, ENV_MAIL_OPT_BODY, ENV_MAIL_OPT_AUTH,
-  ENV_MAIL_OPT_PRDR, ENV_MAIL_OPT_NULL
+#ifdef EXPERIMENTAL_PRDR
+  ENV_MAIL_OPT_PRDR,
+#endif
+  ENV_MAIL_OPT_NULL
   };
 typedef struct {
   uschar *   name;  /* option requested during MAIL cmd */
@@ -222,7 +229,10 @@ static env_mail_type_t env_mail_type_list[] = {
     { US"SIZE",   ENV_MAIL_OPT_SIZE,   TRUE  },
     { US"BODY",   ENV_MAIL_OPT_BODY,   TRUE  },
     { US"AUTH",   ENV_MAIL_OPT_AUTH,   TRUE  },
-    { US"NULL",   ENV_MAIL_OPT_NULL,   FALSE }  /* Placeholder for ending */
+#ifdef EXPERIMENTAL_PRDR
+    { US"PRDR",   ENV_MAIL_OPT_PRDR,   FALSE },
+#endif
+    { US"NULL",   ENV_MAIL_OPT_NULL,   FALSE }
   };
 
 /* When reading SMTP from a remote host, we have to use our own versions of the
@@ -543,6 +553,294 @@ exim_exit(EXIT_FAILURE);
 
 
 
+#ifdef EXPERIMENTAL_PROXY
+/*************************************************
+*       Check if host is required proxy host     *
+*************************************************/
+/* The function determines if inbound host will be a regular smtp host
+or if it is configured that it must use Proxy Protocol.
+
+Arguments: none
+Returns:   bool
+*/
+
+static BOOL
+check_proxy_protocol_host()
+{
+int rc;
+/* Cannot configure local connection as a proxy inbound */
+if (sender_host_address == NULL) return proxy_session;
+
+rc = verify_check_this_host(&proxy_required_hosts, NULL, NULL,
+                           sender_host_address, NULL);
+if (rc == OK)
+  {
+  DEBUG(D_receive)
+    debug_printf("Detected proxy protocol configured host\n");
+  proxy_session = TRUE;
+  }
+return proxy_session;
+}
+
+
+/*************************************************
+*           Flush waiting input string           *
+*************************************************/
+static void
+flush_input()
+{
+int rc;
+
+rc = smtp_getc();
+while (rc != '\n')             /* End of input string */
+  {
+  rc = smtp_getc();
+  }
+}
+
+
+/*************************************************
+*         Setup host for proxy protocol          *
+*************************************************/
+/* The function configures the connection based on a header from the
+inbound host to use Proxy Protocol. The specification is very exact
+so exit with an error if do not find the exact required pieces. This
+includes an incorrect number of spaces separating args.
+
+Arguments: none
+Returns:   int
+*/
+
+static BOOL
+setup_proxy_protocol_host()
+{
+union {
+  struct {
+    uschar line[108];
+  } v1;
+  struct {
+    uschar sig[12];
+    uschar ver;
+    uschar cmd;
+    uschar fam;
+    uschar len;
+    union {
+      struct { /* TCP/UDP over IPv4, len = 12 */
+        uint32_t src_addr;
+        uint32_t dst_addr;
+        uint16_t src_port;
+        uint16_t dst_port;
+      } ip4;
+      struct { /* TCP/UDP over IPv6, len = 36 */
+        uint8_t  src_addr[16];
+        uint8_t  dst_addr[16];
+        uint16_t src_port;
+        uint16_t dst_port;
+      } ip6;
+      struct { /* AF_UNIX sockets, len = 216 */
+        uschar   src_addr[108];
+        uschar   dst_addr[108];
+      } unx;
+    } addr;
+  } v2;
+} hdr;
+
+int size, ret, fd;
+uschar *tmpip;
+const char v2sig[13] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A\x02";
+uschar *iptype;  /* To display debug info */
+struct timeval tv;
+
+fd = fileno(smtp_in);
+
+/* Proxy Protocol host must send header within a short time
+(default 3 seconds) or it's considered invalid */
+tv.tv_sec  = PROXY_NEGOTIATION_TIMEOUT_SEC;
+tv.tv_usec = PROXY_NEGOTIATION_TIMEOUT_USEC;
+setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,
+           sizeof(struct timeval));
+do
+  {
+  ret = recv(fd, &hdr, sizeof(hdr), MSG_PEEK);
+  }
+  while (ret == -1 && errno == EINTR);
+
+if (ret == -1)
+  return (errno == EAGAIN) ? 0 : ERRNO_PROXYFAIL;
+
+if (ret >= 16 &&
+    memcmp(&hdr.v2, v2sig, 13) == 0)
+  {
+  DEBUG(D_receive) debug_printf("Detected PROXYv2 header\n");
+  size = 16 + hdr.v2.len;
+  if (ret < size)
+    {
+    DEBUG(D_receive) debug_printf("Truncated or too large PROXYv2 header\n");
+    goto proxyfail;
+    }
+  switch (hdr.v2.cmd)
+    {
+    case 0x01: /* PROXY command */
+      switch (hdr.v2.fam)
+        {
+        case 0x11:  /* TCPv4 */
+          tmpip = string_sprintf("%s", hdr.v2.addr.ip4.src_addr);
+          if (!string_is_ip_address(tmpip,NULL))
+            return ERRNO_PROXYFAIL;
+          sender_host_address = tmpip;
+          sender_host_port    = hdr.v2.addr.ip4.src_port;
+          goto done;
+        case 0x21:  /* TCPv6 */
+          tmpip = string_sprintf("%s", hdr.v2.addr.ip6.src_addr);
+          if (!string_is_ip_address(tmpip,NULL))
+            return ERRNO_PROXYFAIL;
+          sender_host_address = tmpip;
+          sender_host_port    = hdr.v2.addr.ip6.src_port;
+          goto done;
+        }
+      /* Unsupported protocol, keep local connection address */
+      break;
+    case 0x00: /* LOCAL command */
+      /* Keep local connection address for LOCAL */
+      break;
+    default:
+      DEBUG(D_receive) debug_printf("Unsupported PROXYv2 command\n");
+      goto proxyfail;
+    }
+  }
+else if (ret >= 8 &&
+         memcmp(hdr.v1.line, "PROXY", 5) == 0)
+  {
+  uschar *p = string_copy(hdr.v1.line);
+  uschar *end = memchr(p, '\r', ret - 1);
+  uschar *sp;     /* Utility variables follow */
+  int     tmp_port;
+  char   *endc;
+
+  if (!end || end[1] != '\n')
+    {
+    DEBUG(D_receive) debug_printf("Partial or invalid PROXY header\n");
+    goto proxyfail;
+    }
+  *end = '\0'; /* Terminate the string */
+  size = end + 2 - hdr.v1.line; /* Skip header + CRLF */
+  DEBUG(D_receive) debug_printf("Detected PROXYv1 header\n");
+  /* Step through the string looking for the required fields. Ensure
+     strict adherance to required formatting, exit for any error. */
+  p += 5;
+  if (!isspace(*(p++)))
+    {
+    DEBUG(D_receive) debug_printf("Missing space after PROXY command\n");
+    goto proxyfail;
+    }
+  if (!Ustrncmp(p, CCS"TCP4", 4))
+    iptype = US"IPv4";
+  else if (!Ustrncmp(p,CCS"TCP6", 4))
+    iptype = US"IPv6";
+  else if (!Ustrncmp(p,CCS"UNKNOWN", 7))
+    {
+    iptype = US"Unknown";
+    goto done;
+    }
+  else
+    {
+    DEBUG(D_receive) debug_printf("Invalid TCP type\n");
+    goto proxyfail;
+    }
+
+  p += Ustrlen(iptype);
+  if (!isspace(*(p++)))
+    {
+    DEBUG(D_receive) debug_printf("Missing space after TCP4/6 command\n");
+    goto proxyfail;
+    }
+  /* Find the end of the arg */
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive)
+      debug_printf("Did not find proxied src %s\n", iptype);
+    goto proxyfail;
+    }
+  *sp = '\0';
+  if(!string_is_ip_address(p,NULL))
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxied src arg is not an %s address\n", iptype);
+    goto proxyfail;
+    }
+  proxy_host = sender_host_address;
+  sender_host_address = p;
+  p = sp + 1;
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive)
+      debug_printf("Did not find proxy dest %s\n", iptype);
+    goto proxyfail;
+    }
+  *sp = '\0';
+  if(!string_is_ip_address(p,NULL))
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxy dest arg is not an %s address\n", iptype);
+    goto proxyfail;
+    }
+  /* Should save dest ip somewhere? */
+  p = sp + 1;
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive) debug_printf("Did not find proxied src port\n");
+    goto proxyfail;
+    }
+  *sp = '\0';
+  tmp_port = strtol(CCS p,&endc,10);
+  if (*endc || tmp_port == 0)
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxied src port '%s' not an integer\n", p);
+    goto proxyfail;
+    }
+  proxy_port = sender_host_port;
+  sender_host_port = tmp_port;
+  p = sp + 1;
+  if ((sp = Ustrchr(p, '\0')) == NULL)
+    {
+    DEBUG(D_receive) debug_printf("Did not find proxy dest port\n");
+    goto proxyfail;
+    }
+  tmp_port = strtol(CCS p,&endc,10);
+  if (*endc || tmp_port == 0)
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxy dest port '%s' not an integer\n", p);
+    goto proxyfail;
+    }
+  /* Should save dest port somewhere? */
+  /* Already checked for /r /n above. Good V1 header received. */
+  goto done;
+  }
+else
+  {
+  /* Wrong protocol */
+  DEBUG(D_receive) debug_printf("Wrong proxy protocol specified\n");
+  goto proxyfail;
+  }
+
+proxyfail:
+/* Don't flush any potential buffer contents. Any input should cause a
+synchronization failure or we just don't want to speak SMTP to them */
+return FALSE;
+
+done:
+flush_input();
+DEBUG(D_receive)
+  debug_printf("Valid %s sender from Proxy Protocol header\n",
+               iptype);
+return proxy_session;
+}
+#endif
+
+
+
 /*************************************************
 *           Read one command line                *
 *************************************************/
@@ -616,6 +914,14 @@ if required. */
 
 for (p = cmd_list; p < cmd_list_end; p++)
   {
+  #ifdef EXPERIMENTAL_PROXY
+  /* Only allow QUIT command if Proxy Protocol parsing failed */
+  if (proxy_session && proxy_session_failed)
+    {
+    if (p->cmd != QUIT_CMD)
+      continue;
+    }
+  #endif
   if (strncmpic(smtp_cmd_buffer, US p->name, p->len) == 0 &&
        (smtp_cmd_buffer[p->len-1] == ':' ||   /* "mail from:" or "rcpt to:" */
         smtp_cmd_buffer[p->len] == 0 ||
@@ -663,6 +969,12 @@ for (p = cmd_list; p < cmd_list_end; p++)
     }
   }
 
+#ifdef EXPERIMENTAL_PROXY
+/* Only allow QUIT command if Proxy Protocol parsing failed */
+if (proxy_session && proxy_session_failed)
+  return PROXY_FAIL_IGNORE_CMD;
+#endif
+
 /* Enforce synchronization for unknown commands */
 
 if (smtp_inptr < smtp_inend &&                     /* Outstanding input */
@@ -998,19 +1310,23 @@ uschar *n;
 uschar *v = smtp_cmd_data + Ustrlen(smtp_cmd_data) - 1;
 while (isspace(*v)) v--;
 v[1] = 0;
-
 while (v > smtp_cmd_data && *v != '=' && !isspace(*v)) v--;
-if (*v != '=') return FALSE;
 
 n = v;
-while(isalpha(n[-1])) n--;
-
-/* RFC says SP, but TAB seen in wild and other major MTAs accept it */
-if (!isspace(n[-1])) return FALSE;
-
-n[-1] = 0;
-*name = n;
+if (*v == '=')
+{
+  while(isalpha(n[-1])) n--;
+  /* RFC says SP, but TAB seen in wild and other major MTAs accept it */
+  if (!isspace(n[-1])) return FALSE;
+  n[-1] = 0;
+}
+else
+{
+  n++;
+  if (v == smtp_cmd_data) return FALSE;
+}
 *v++ = 0;
+*name = n;
 *value = v;
 return TRUE;
 }
@@ -1822,6 +2138,28 @@ if (!sender_host_unknown)
 
 if (smtp_batched_input) return TRUE;
 
+#ifdef EXPERIMENTAL_PROXY
+/* If valid Proxy Protocol source is connecting, set up session.
+ * Failure will not allow any SMTP function other than QUIT. */
+proxy_session = FALSE;
+proxy_session_failed = FALSE;
+if (check_proxy_protocol_host())
+  {
+  if (setup_proxy_protocol_host() == FALSE)
+    {
+    proxy_session_failed = TRUE;
+    DEBUG(D_receive)
+      debug_printf("Failure to extract proxied host, only QUIT allowed\n");
+    }
+  else
+    {
+    sender_host_name = NULL;
+    (void)host_name_lookup();
+    host_build_sender_fullhost();
+    }
+  }
+#endif
+
 /* Run the ACL if it exists */
 
 user_msg = NULL;
@@ -2201,6 +2539,9 @@ uschar *what =
 #endif
   (where == ACL_WHERE_PREDATA)? US"DATA" :
   (where == ACL_WHERE_DATA)? US"after DATA" :
+#ifdef EXPERIMENTAL_PRDR
+  (where == ACL_WHERE_PRDR)? US"after DATA PRDR" :
+#endif
   (smtp_cmd_data == NULL)?
     string_sprintf("%s in \"connect\" ACL", acl_wherenames[where]) :
     string_sprintf("%s %s", acl_wherenames[where], smtp_cmd_data);
@@ -2581,7 +2922,6 @@ smtp_respond(code, len, TRUE, user_msg);
 
 
 
-
 /*************************************************
 *       Initialize for SMTP incoming message     *
 *************************************************/
@@ -2808,6 +3148,7 @@ while (done <= 0)
         if (set_id != NULL) authenticated_id = string_copy_malloc(set_id);
         sender_host_authenticated = au->name;
         authentication_failed = FALSE;
+        authenticated_fail_id = NULL;   /* Impossible to already be set? */
         received_protocol =
           protocols[pextend + pauthed + ((tls_in.active >= 0)? pcrpted:0)] +
             ((sender_host_address != NULL)? pnlocal : 0);
@@ -2823,6 +3164,7 @@ while (done <= 0)
       /* Fall through */
 
       case DEFER:
+      if (set_id != NULL) authenticated_fail_id = string_copy_malloc(set_id);
       s = string_sprintf("435 Unable to authenticate at present%s",
         auth_defer_user_msg);
       ss = string_sprintf("435 Unable to authenticate at present%s: %s",
@@ -2842,11 +3184,13 @@ while (done <= 0)
       break;
 
       case FAIL:
+      if (set_id != NULL) authenticated_fail_id = string_copy_malloc(set_id);
       s = US"535 Incorrect authentication data";
       ss = string_sprintf("535 Incorrect authentication data%s", set_id);
       break;
 
       default:
+      if (set_id != NULL) authenticated_fail_id = string_copy_malloc(set_id);
       s = US"435 Internal error";
       ss = string_sprintf("435 Internal error%s: return %d from authentication "
         "check", set_id, c);
@@ -3119,6 +3463,7 @@ while (done <= 0)
         pipelining_advertised = TRUE;
         }
 
+
       /* If any server authentication mechanisms are configured, advertise
       them if the current host is in auth_advertise_hosts. The problem with
       advertising always is that some clients then require users to
@@ -3177,6 +3522,14 @@ while (done <= 0)
         }
       #endif
 
+      #ifdef EXPERIMENTAL_PRDR
+      /* Per Recipient Data Response, draft by Eric A. Hall extending RFC */
+      if (prdr_enable) {
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-PRDR\r\n", 7);
+      }
+      #endif
+
       /* Finish off the multiline reply with one that is always available. */
 
       s = string_cat(s, &size, &ptr, smtp_code, 3);
@@ -3192,7 +3545,9 @@ while (done <= 0)
     if (tls_in.active >= 0) (void)tls_write(TRUE, s, ptr); else
     #endif
 
-    (void)fwrite(s, 1, ptr, smtp_out);
+      {
+      int i = fwrite(s, 1, ptr, smtp_out); i = i; /* compiler quietening */
+      }
     DEBUG(D_receive)
       {
       uschar *cr;
@@ -3291,17 +3646,12 @@ while (done <= 0)
         }
       if (mail_args->need_value && strcmpic(value, US"") == 0)
         break;
-      /* This doesn't seem right to use
-        if ((char *)mail_args >= (char *)env_mail_type_list + sizeof(env_mail_type_list))
-        goto BAD_MAIL_ARGS;
-      */
 
       switch(mail_args->value)
         {
         /* Handle SIZE= by reading the value. We don't do the check till later,
         in order to be able to log the sender address on failure. */
         case ENV_MAIL_OPT_SIZE:
-          /* if (strcmpic(name, US"SIZE") == 0 && */
           if (((size = Ustrtoul(value, &end, 10)), *end == 0))
             {
             if ((size == ULONG_MAX && errno == ERANGE) || size > INT_MAX)
@@ -3353,8 +3703,8 @@ while (done <= 0)
             if (auth_xtextdecode(value, &authenticated_sender) < 0)
               {
               /* Put back terminator overrides for error message */
-              name[-1] = ' ';
               value[-1] = '=';
+              name[-1] = ' ';
               done = synprot_error(L_smtp_syntax_error, 501, NULL,
                 US"invalid data for AUTH");
               goto COMMAND_LOOP;
@@ -3397,22 +3747,30 @@ while (done <= 0)
               overrides for error message */
   
               default:
-              name[-1] = ' ';
               value[-1] = '=';
+              name[-1] = ' ';
               (void)smtp_handle_acl_fail(ACL_WHERE_MAILAUTH, rc, user_msg,
                 log_msg);
               goto COMMAND_LOOP;
               }
             }
             break;
+
+#ifdef EXPERIMENTAL_PRDR
+        case ENV_MAIL_OPT_PRDR:
+          if ( prdr_enable )
+            prdr_requested = TRUE;
+          break;
+#endif
+
         /* Unknown option. Stick back the terminator characters and break
-        the loop. An error for a malformed address will occur. */
+        the loop.  Do the name-terminator second as extract_option sets
+       value==name when it found no equal-sign.
+       An error for a malformed address will occur. */
         default:
-
-          /* BAD_MAIL_ARGS: */
-          name[-1] = ' ';
           value[-1] = '=';
+          name[-1] = ' ';
+          arg_error = TRUE;
           break;
         }
       /* Break out of for loop if switch() had bad argument or
@@ -3534,8 +3892,21 @@ while (done <= 0)
 
     if (rc == OK || rc == DISCARD)
       {
-      if (user_msg == NULL) smtp_printf("250 OK\r\n");
-        else smtp_user_msg(US"250", user_msg);
+      if (user_msg == NULL) 
+        smtp_printf("%s%s%s", US"250 OK",
+                  #ifdef EXPERIMENTAL_PRDR
+                    prdr_requested == TRUE ? US", PRDR Requested" :
+                  #endif
+                    US"",
+                    US"\r\n");
+      else 
+        {
+      #ifdef EXPERIMENTAL_PRDR
+        if ( prdr_requested == TRUE )
+           user_msg = string_sprintf("%s%s", user_msg, US", PRDR Requested");
+      #endif
+        smtp_user_msg(US"250",user_msg);
+        }
       smtp_delay_rcpt = smtp_rlr_base;
       recipients_discarded = (rc == DISCARD);
       was_rej_mail = FALSE;
@@ -3799,9 +4170,11 @@ while (done <= 0)
 
     if (rc == OK)
       {
+      uschar * code;
+      code = US"354";
       if (user_msg == NULL)
-        smtp_printf("354 Enter message, ending with \".\" on a line by itself\r\n");
-      else smtp_user_msg(US"354", user_msg);
+        smtp_printf("%s Enter message, ending with \".\" on a line by itself\r\n", code);
+      else smtp_user_msg(code, user_msg);
       done = 3;
       message_ended = END_NOTENDED;   /* Indicate in middle of data */
       }
@@ -4328,11 +4701,16 @@ while (done <= 0)
     incomplete_transaction_log(US"too many non-mail commands");
     log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many "
       "nonmail commands (last was \"%.*s\")",  host_and_ident(FALSE),
-      s - smtp_cmd_buffer, smtp_cmd_buffer);
+      (int)(s - smtp_cmd_buffer), smtp_cmd_buffer);
     smtp_notquit_exit(US"bad-commands", US"554", US"Too many nonmail commands");
     done = 1;   /* Pretend eof - drops connection */
     break;
 
+    #ifdef EXPERIMENTAL_PROXY
+    case PROXY_FAIL_IGNORE_CMD:
+    smtp_printf("503 Command refused, required Proxy negotiation failed\r\n");
+    break;
+    #endif
 
     default:
     if (unknown_command_count++ >= smtp_max_unknown_commands)