TL/01 LDAP support uses per connection or global context settings, depending
upon the detected version of the libraries at build time.
+TL/02 Experimental Proxy Protocol support: allows a proxied SMTP connection
+ to extract and use the src ip:port in logging and expansions as if it
+ were a direct connection from the outside internet.
+
Exim version 4.82
-----------------
Version 4.83
------------
+ 1. If built with the EXPERIMENTAL_PROXY feature enabled, Exim can be
+ configured to expect an initial header from a proxy that will make the
+ actual external source IP:host be used in exim instead of the IP of the
+ proxy that is connecting to it.
+
Version 4.82
------------
set acl_c_spam_host = ${lookup redis{GET...}}
+Proxy Protocol Support
+--------------------------------------------------------------
+
+Exim now has Experimental "Proxy Protocol" support. It was built on
+specifications from:
+http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
+
+The purpose of this function is so that an application load balancer,
+such as HAProxy, can sit in front of several Exim servers and Exim
+will log the IP that is connecting to the proxy server instead of
+the IP of the proxy server when it connects to Exim. It resets the
+$sender_address_host and $sender_address_port to the IP:port of the
+connection to the proxy. It also re-queries the DNS information for
+this new IP address so that the original sender's hostname and IP
+get logged in the Exim logfile. There is no logging if a host passes or
+fails Proxy Protocol negotiation, but it can easily be determined and
+recorded in an ACL (example is below).
+
+1. To compile Exim with Proxy Protocol support, put this in
+Local/Makefile:
+
+EXPERIMENTAL_PROXY=yes
+
+2. Global configuration settings:
+
+proxy_required_hosts = HOSTLIST
+
+The proxy_required_hosts option will require any IP in that hostlist
+to use Proxy Protocol. The specification of Proxy Protocol is very
+strict, and if proxy negotiation fails, Exim will not allow any SMTP
+command other than QUIT. (See end of this section for an example.)
+The option is expanded when used, so it can be a hostlist as well as
+string of IP addresses. Since it is expanded, specifying an alternate
+separator is supported for ease of use with IPv6 addresses.
+
+To log the IP of the proxy in the incoming logline, add:
+ log_selector = +proxy
+
+A default incoming logline (wrapped for appearance) will look like this:
+
+ 2013-11-04 09:25:06 1VdNti-0001OY-1V <= me@example.net
+ H=mail.example.net [1.2.3.4] P=esmtp S=433
+
+With the log selector enabled, an email that was proxied through a
+Proxy Protocol server at 192.168.1.2 will look like this:
+
+ 2013-11-04 09:25:06 1VdNti-0001OY-1V <= me@example.net
+ H=mail.example.net [1.2.3.4] P=esmtp PRX=192.168.1.2 S=433
+
+3. In the ACL's the following expansion variables are available.
+
+proxy_host The src IP of the proxy server making the connection
+proxy_port The src port the proxy server is using
+proxy_session Boolean, yes/no, the connected host is required to use
+ Proxy Protocol.
+
+There is no expansion for a failed proxy session, however you can detect
+it by checking if $proxy_session is true but $proxy_host is empty. As
+an example, in my connect ACL, I have:
+
+ warn condition = ${if and{ {bool{$proxy_session}} \
+ {eq{$proxy_host}{}} } }
+ log_message = Failed required proxy protocol negotiation \
+ from $sender_host_name [$sender_host_address]
+
+ warn condition = ${if and{ {bool{$proxy_session}} \
+ {!eq{$proxy_host}{}} } }
+ # But don't log health probes from the proxy itself
+ condition = ${if eq{$proxy_host}{$sender_host_address} \
+ {false}{true}}
+ log_message = Successfully proxied from $sender_host_name \
+ [$sender_host_address] through proxy protocol \
+ host $proxy_host
+
+4. Runtime issues to be aware of:
+ - Since the real connections are all coming from your proxy, and the
+ per host connection tracking is done before Proxy Protocol is
+ evaluated, smtp_accept_max_per_host must be set high enough to
+ handle all of the parallel volume you expect per inbound proxy.
+ - The proxy has 3 seconds (hard-coded in the source code) to send the
+ required Proxy Protocol header after it connects. If it does not,
+ the response to any commands will be:
+ "503 Command refused, required Proxy negotiation failed"
+ - If the incoming connection is configured in Exim to be a Proxy
+ Protocol host, but the proxy is not sending the header, the banner
+ does not get sent until the timeout occurs. If the sending host
+ sent any input (before the banner), this causes a standard Exim
+ synchronization error (i.e. trying to pipeline before PIPELINING
+ was advertised).
+ - This is not advised, but is mentioned for completeness if you have
+ a specific internal configuration that you want this: If the Exim
+ server only has an internal IP address and no other machines in your
+ organization will connect to it to try to send email, you may
+ simply set the hostlist to "*", however, this will prevent local
+ mail programs from working because that would require mail from
+ localhost to use Proxy Protocol. Again, not advised!
+
+5. Example of a refused connection because the Proxy Protocol header was
+not sent from a host configured to use Proxy Protocol. In the example,
+the 3 second timeout occurred (when a Proxy Protocol banner should have
+been sent), the banner was displayed to the user, but all commands are
+rejected except for QUIT:
+
+# nc mail.example.net 25
+220-mail.example.net, ESMTP Exim 4.82+proxy, Mon, 04 Nov 2013 10:45:59
+220 -0800 RFC's enforced
+EHLO localhost
+503 Command refused, required Proxy negotiation failed
+QUIT
+221 mail.example.net closing connection
+
+
--------------------------------------------------------------
End of file
# CFLAGS += -I/usr/local/include
# LDFLAGS += -lhiredis
+# Uncomment the following line to enable Experimental Proxy Protocol
+# EXPERIMENTAL_PROXY=yes
+
###############################################################################
# THESE ARE THINGS YOU MIGHT WANT TO SPECIFY #
#define EXPERIMENTAL_DMARC
#define EXPERIMENTAL_OCSP
#define EXPERIMENTAL_PRDR
+#define EXPERIMENTAL_PROXY
#define EXPERIMENTAL_REDIS
#define EXPERIMENTAL_SPF
#define EXPERIMENTAL_SRS
{ "parent_local_part", vtype_stringptr, &deliver_localpart_parent },
{ "pid", vtype_pid, NULL },
{ "primary_hostname", vtype_stringptr, &primary_hostname },
+#ifdef EXPERIMENTAL_PROXY
+ { "proxy_host", vtype_stringptr, &proxy_host },
+ { "proxy_port", vtype_int, &proxy_port },
+ { "proxy_session", vtype_bool, &proxy_session },
+#endif
{ "prvscheck_address", vtype_stringptr, &prvscheck_address },
{ "prvscheck_keynum", vtype_stringptr, &prvscheck_keynum },
{ "prvscheck_result", vtype_stringptr, &prvscheck_result },
{ US"lost_incoming_connection", L_lost_incoming_connection },
{ US"outgoing_port", LX_outgoing_port },
{ US"pid", LX_pid },
+#ifdef EXPERIMENTAL_PROXY
+ { US"proxy", LX_proxy },
+#endif
{ US"queue_run", L_queue_run },
{ US"queue_time", LX_queue_time },
{ US"queue_time_overall", LX_queue_time_overall },
int process_info_len = 0;
uschar *process_log_path = NULL;
BOOL prod_requires_admin = TRUE;
+
+#ifdef EXPERIMENTAL_PROXY
+uschar *proxy_host = US"";
+int proxy_port = 0;
+uschar *proxy_required_hosts = US"";
+BOOL proxy_session = FALSE;
+BOOL proxy_session_failed = FALSE;
+#endif
+
uschar *prvscheck_address = NULL;
uschar *prvscheck_keynum = NULL;
uschar *prvscheck_result = NULL;
extern int process_info_len;
extern uschar *process_log_path; /* Alternate path */
extern BOOL prod_requires_admin; /* TRUE if prodding requires admin */
+
+#ifdef EXPERIMENTAL_PROXY
+extern uschar *proxy_host; /* IP of proxy server */
+extern int proxy_port; /* Port of proxy server */
+extern uschar *proxy_required_hosts; /* Hostlist which (require) use proxy protocol */
+extern BOOL proxy_session; /* TRUE if receiving mail from valid proxy */
+extern BOOL proxy_session_failed; /* TRUE if required proxy negotiation failed */
+#endif
+
extern uschar *prvscheck_address; /* Set during prvscheck expansion item */
extern uschar *prvscheck_keynum; /* Set during prvscheck expansion item */
extern uschar *prvscheck_result; /* Set during prvscheck expansion item */
#define WAIT_NAME_MAX 50
+/* Wait this long before determining that a Proxy Protocol configured
+host isn't speaking the protocol, and so is disallowed. Can be moved to
+runtime configuration if per site settings become needed. */
+#ifdef EXPERIMENTAL_PROXY
+#define PROXY_NEGOTIATION_TIMEOUT_SEC 3
+#define PROXY_NEGOTIATION_TIMEOUT_USEC 0
+#endif
+
/* Fixed option values for all PCRE functions */
#define PCRE_COPT 0 /* compile */
#define LX_unknown_in_list 0x81000000
#define LX_8bitmime 0x82000000
#define LX_smtp_mailauth 0x84000000
+#define LX_proxy 0x88000000
#define L_default (L_connection_reject | \
L_delay_delivery | \
#define ERRNO_RCPT4XX (-44) /* RCPT gave 4xx error */
#define ERRNO_MAIL4XX (-45) /* MAIL gave 4xx error */
#define ERRNO_DATA4XX (-46) /* DATA gave 4xx error */
+#define ERRNO_PROXYFAIL (-47) /* Negotiation failed for proxy configured host */
/* These must be last, so all retry deferments can easily be identified */
{ "print_topbitchars", opt_bool, &print_topbitchars },
{ "process_log_path", opt_stringptr, &process_log_path },
{ "prod_requires_admin", opt_bool, &prod_requires_admin },
+#ifdef EXPERIMENTAL_PROXY
+ { "proxy_required_hosts", opt_stringptr, &proxy_required_hosts },
+#endif
{ "qualify_domain", opt_stringptr, &qualify_domain_sender },
{ "qualify_recipient", opt_stringptr, &qualify_domain_recipient },
{ "queue_domains", opt_stringptr, &queue_domains },
if (prdr_requested)
s = string_append(s, &size, &sptr, 1, US" PRDR");
#endif
+#ifdef EXPERIMENTAL_PROXY
+if (proxy_session &&
+ (log_extra_selector & LX_proxy) != 0)
+ {
+ s = string_append(s, &size, &sptr, 2, US" PRX=", proxy_host);
+ }
+#endif
sprintf(CS big_buffer, "%d", msg_size);
s = string_append(s, &size, &sptr, 2, US" S=", big_buffer);
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,
+#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 *
*************************************************/
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 ||
}
}
+#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 */
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;
-
/*************************************************
* Initialize for SMTP incoming message *
*************************************************/
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)