summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Lyons <tlyons@exim.org>2014-04-17 11:58:09 -0700
committerTodd Lyons <tlyons@exim.org>2014-04-19 08:32:09 -0700
commiteb57651e8badf0b65af0371732e42f2ee5c7772c (patch)
tree0bc0f5ddf2deb86cf11e1063e5b28942009e36b8
parent887291d23b561d0bb8cf43db80c191810e2d8ce3 (diff)
Fix Proxy Protocol v2 handling
Change recv() to not use MSGPEEK and eliminated flush_input(). Add proxy_target_address/port expansions. Convert ipv6 decoding to memmove(). Use sizeof() for variable sizing. Correct struct member access. Enhance debug output when passed invalid command/family. Add to and enhance documentation. Client script to test Proxy Protocol, interactive on STDIN/STDOUT, so can be chained (ie a swaks pipe), useful for any service, not just Exim and/or smtp.
-rw-r--r--doc/doc-txt/experimental-spec.txt21
-rw-r--r--src/src/expand.c2
-rw-r--r--src/src/globals.c2
-rw-r--r--src/src/globals.h6
-rw-r--r--src/src/smtp_in.c109
-rw-r--r--src/util/proxy_protocol_client.pl250
6 files changed, 347 insertions, 43 deletions
diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt
index 265e1211b..f21609662 100644
--- a/doc/doc-txt/experimental-spec.txt
+++ b/doc/doc-txt/experimental-spec.txt
@@ -1087,10 +1087,16 @@ Proxy Protocol server at 192.168.1.2 will look like this:
3. In the ACL's the following expansion variables are available.
-proxy_host_address The src IP of the proxy server making the connection
-proxy_host_port The src port the proxy server is using
-proxy_session Boolean, yes/no, the connected host is required to use
- Proxy Protocol.
+proxy_host_address The (internal) src IP of the proxy server
+ making the connection to the Exim server.
+proxy_host_port The (internal) src port the proxy server is
+ using to connect to the Exim server.
+proxy_target_address The dest (public) IP of the remote host to
+ the proxy server.
+proxy_target_port The dest port the remote host is using to
+ connect to the proxy server.
+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
@@ -1110,6 +1116,13 @@ an example, in my connect ACL, I have:
[$sender_host_address] through proxy protocol \
host $proxy_host_address
+ # Possibly more clear
+ warn logwrite = Remote Source Address: $sender_host_address:$sender_host_port
+ logwrite = Proxy Target Address: $proxy_target_address:$proxy_target_port
+ logwrite = Proxy Internal Address: $proxy_host_address:$proxy_host_port
+ logwrite = Internal Server Address: $received_ip_address:$received_port
+
+
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
diff --git a/src/src/expand.c b/src/src/expand.c
index d2ac8ca79..7e8c2b49d 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -565,6 +565,8 @@ static var_entry var_table[] = {
{ "proxy_host_address", vtype_stringptr, &proxy_host_address },
{ "proxy_host_port", vtype_int, &proxy_host_port },
{ "proxy_session", vtype_bool, &proxy_session },
+ { "proxy_target_address",vtype_stringptr, &proxy_target_address },
+ { "proxy_target_port", vtype_int, &proxy_target_port },
#endif
{ "prvscheck_address", vtype_stringptr, &prvscheck_address },
{ "prvscheck_keynum", vtype_stringptr, &prvscheck_keynum },
diff --git a/src/src/globals.c b/src/src/globals.c
index 839b91dcc..38bd37bce 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -925,6 +925,8 @@ int proxy_host_port = 0;
uschar *proxy_required_hosts = US"";
BOOL proxy_session = FALSE;
BOOL proxy_session_failed = FALSE;
+uschar *proxy_target_address = US"";
+int proxy_target_port = 0;
#endif
uschar *prvscheck_address = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index 1cc39fc09..b229c1a07 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -595,11 +595,13 @@ extern uschar *process_log_path; /* Alternate path */
extern BOOL prod_requires_admin; /* TRUE if prodding requires admin */
#ifdef EXPERIMENTAL_PROXY
-extern uschar *proxy_host_address; /* IP of proxy server */
-extern int proxy_host_port; /* Port of proxy server */
+extern uschar *proxy_host_address; /* IP of host being proxied */
+extern int proxy_host_port; /* Port of host being proxied */
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 */
+extern uschar *proxy_target_address; /* IP of proxy server inbound */
+extern int proxy_target_port; /* Port of proxy server inbound */
#endif
extern uschar *prvscheck_address; /* Set during prvscheck expansion item */
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 2a3873d33..7fe00be12 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -603,22 +603,6 @@ 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
@@ -664,12 +648,18 @@ union {
} v2;
} hdr;
+/* Temp variables used in PPv2 address:port parsing */
+uint16_t tmpport;
+char tmpip[INET_ADDRSTRLEN];
+struct sockaddr_in tmpaddr;
+char tmpip6[INET6_ADDRSTRLEN];
+struct sockaddr_in6 tmpaddr6;
+
+int get_ok = 0;
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;
-int get_ok = 0;
socklen_t vslen = 0;
struct timeval tvtmp;
@@ -690,7 +680,9 @@ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (char *)&tv,
do
{
- ret = recv(fd, &hdr, sizeof(hdr), MSG_PEEK);
+ /* The inbound host was declared to be a Proxy Protocol host, so
+ don't do a PEEK into the data, actually slurp it up. */
+ ret = recv(fd, &hdr, sizeof(hdr), 0);
}
while (ret == -1 && errno == EINTR);
@@ -715,20 +707,63 @@ if (ret >= 16 &&
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))
+ case 0x11: /* TCPv4 address type */
+ iptype = US"IPv4";
+ tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.src_addr;
+ inet_ntop(AF_INET, &(tmpaddr.sin_addr), (char *)&tmpip, sizeof(tmpip));
+ if (!string_is_ip_address(US tmpip,NULL))
+ {
+ DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
return ERRNO_PROXYFAIL;
- sender_host_address = tmpip;
- sender_host_port = hdr.v2.addr.ip4.src_port;
+ }
+ proxy_host_address = sender_host_address;
+ sender_host_address = string_copy(US tmpip);
+ tmpport = ntohs(hdr.v2.addr.ip4.src_port);
+ proxy_host_port = sender_host_port;
+ sender_host_port = tmpport;
+ /* Save dest ip/port */
+ tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.dst_addr;
+ inet_ntop(AF_INET, &(tmpaddr.sin_addr), (char *)&tmpip, sizeof(tmpip));
+ if (!string_is_ip_address(US tmpip,NULL))
+ {
+ DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
+ return ERRNO_PROXYFAIL;
+ }
+ proxy_target_address = string_copy(US tmpip);
+ tmpport = ntohs(hdr.v2.addr.ip4.dst_port);
+ proxy_target_port = tmpport;
goto done;
- case 0x21: /* TCPv6 */
- tmpip = string_sprintf("%s", hdr.v2.addr.ip6.src_addr);
- if (!string_is_ip_address(tmpip,NULL))
+ case 0x21: /* TCPv6 address type */
+ iptype = US"IPv6";
+ memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.src_addr, 16);
+ inet_ntop(AF_INET6, &(tmpaddr6.sin6_addr), (char *)&tmpip6, sizeof(tmpip6));
+ if (!string_is_ip_address(US tmpip6,NULL))
+ {
+ DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
+ return ERRNO_PROXYFAIL;
+ }
+ proxy_host_address = sender_host_address;
+ sender_host_address = string_copy(US tmpip6);
+ tmpport = ntohs(hdr.v2.addr.ip6.src_port);
+ proxy_host_port = sender_host_port;
+ sender_host_port = tmpport;
+ /* Save dest ip/port */
+ memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.dst_addr, 16);
+ inet_ntop(AF_INET6, &(tmpaddr6.sin6_addr), (char *)&tmpip6, sizeof(tmpip6));
+ if (!string_is_ip_address(US tmpip6,NULL))
+ {
+ DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
return ERRNO_PROXYFAIL;
- sender_host_address = tmpip;
- sender_host_port = hdr.v2.addr.ip6.src_port;
+ }
+ proxy_target_address = string_copy(US tmpip6);
+ tmpport = ntohs(hdr.v2.addr.ip6.dst_port);
+ proxy_target_port = tmpport;
goto done;
+ default:
+ DEBUG(D_receive)
+ debug_printf("Unsupported PROXYv2 connection type: 0x%02x\n",
+ hdr.v2.fam);
+ goto proxyfail;
}
/* Unsupported protocol, keep local connection address */
break;
@@ -736,7 +771,9 @@ if (ret >= 16 &&
/* Keep local connection address for LOCAL */
break;
default:
- DEBUG(D_receive) debug_printf("Unsupported PROXYv2 command\n");
+ DEBUG(D_receive)
+ debug_printf("Unsupported PROXYv2 command: 0x%02x\n",
+ hdr.v2.cmd);
goto proxyfail;
}
}
@@ -816,7 +853,7 @@ else if (ret >= 8 &&
debug_printf("Proxy dest arg is not an %s address\n", iptype);
goto proxyfail;
}
- /* Should save dest ip somewhere? */
+ proxy_target_address = p;
p = sp + 1;
if ((sp = Ustrchr(p, ' ')) == NULL)
{
@@ -846,29 +883,27 @@ else if (ret >= 8 &&
debug_printf("Proxy dest port '%s' not an integer\n", p);
goto proxyfail;
}
- /* Should save dest port somewhere? */
+ proxy_target_port = tmp_port;
/* 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");
+ DEBUG(D_receive) debug_printf("Invalid proxy protocol version negotiation\n");
goto proxyfail;
}
proxyfail:
restore_socket_timeout(fd, get_ok, tvtmp, vslen);
/* 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 */
+ synchronization failure */
return FALSE;
done:
restore_socket_timeout(fd, get_ok, tvtmp, vslen);
-flush_input();
DEBUG(D_receive)
- debug_printf("Valid %s sender from Proxy Protocol header\n",
- iptype);
+ debug_printf("Valid %s sender from Proxy Protocol header\n", iptype);
return proxy_session;
}
#endif
diff --git a/src/util/proxy_protocol_client.pl b/src/util/proxy_protocol_client.pl
new file mode 100644
index 000000000..7cfc13ddc
--- /dev/null
+++ b/src/util/proxy_protocol_client.pl
@@ -0,0 +1,250 @@
+#!/usr/bin/perl
+#
+# Copyright (C) 2014 Todd Lyons
+# License GPLv2: GNU GPL version 2
+# <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
+#
+# This script emulates a proxy which uses Proxy Protocol to communicate
+# to a backend server. It should be run from an IP which is configured
+# to be a Proxy Protocol connection (or not, if you are testing error
+# scenarios) because Proxy Protocol specs require not to fall back to a
+# non-proxied mode.
+#
+# The script is interactive, so when you run it, you are expected to
+# perform whatever conversation is required for the protocol being
+# tested. It uses STDIN/STDOUT, so you can also pipe output to/from the
+# script. It was originally written to test Exim's Proxy Protocol
+# code, and it could be tested like this:
+#
+# swaks --pipe 'perl proxy_protocol_client.pl --server-ip
+# host.internal.lan' --from user@example.com --to user@example.net
+#
+use strict;
+use warnings;
+use IO::Select;
+use IO::Socket;
+use Getopt::Long;
+use Data::Dumper;
+
+my %opts;
+GetOptions( \%opts,
+ 'help',
+ '6|ipv6',
+ 'dest-ip:s',
+ 'dest-port:i',
+ 'source-ip:s',
+ 'source-port:i',
+ 'server-ip:s',
+ 'server-port:i',
+ 'version:i'
+);
+&usage() if ($opts{help} || !$opts{'server-ip'});
+
+my ($dest_ip,$source_ip,$dest_port,$source_port);
+my %socket_map;
+my $status_line = "Testing Proxy Protocol Version " .
+ ($opts{version} ? $opts{version} : '2') .
+ ":\n";
+
+# All ip's and ports are in network byte order in version 2 mode, but are
+# simple strings when in version 1 mode. The binary_pack_*() functions
+# return the required data for the Proxy Protocol version being used.
+
+# Use provided source or fall back to www.mrball.net
+$source_ip = $opts{'source-ip'} ? binary_pack_ip($opts{'source-ip'}) :
+ $opts{6} ?
+ binary_pack_ip("2001:470:d:367::50") :
+ binary_pack_ip("208.89.139.252");
+$source_port = $opts{'source-port'} ?
+ binary_pack_port($opts{'source-port'}) :
+ binary_pack_port(43118);
+
+$status_line .= "-> " if (!$opts{version} || $opts{version} == 2);
+
+# Use provided dest or fall back to mail.exim.org
+$dest_ip = $opts{'dest-ip'} ? binary_pack_ip($opts{'dest-ip'}) :
+ $opts{6} ?
+ binary_pack_ip("2001:630:212:8:204:23ff:fed6:b664") :
+ binary_pack_ip("131.111.8.192");
+$dest_port = $opts{'dest-port'} ?
+ binary_pack_port($opts{'dest-port'}) :
+ binary_pack_port(25);
+
+# The IP and port of the Proxy Protocol backend real server being tested,
+# don't binary pack it.
+my $server_ip = $opts{'server-ip'};
+my $server_port = $opts{'server-port'} ? $opts{'server-port'} : 25;
+
+my $s = IO::Select->new(); # for socket polling
+
+sub generate_preamble {
+ my @preamble;
+ if (!$opts{version} || $opts{version} == 2) {
+ @preamble = (
+ "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A", # 12 byte v2 header
+ "\x02", # declares v2
+ "\x01", # connection is proxied
+ $opts{6} ? "\x21" : "\x11", # inet6/4 and TCP (stream)
+ $opts{6} ? "\x24" : "\x0b", # 36 bytes / 12 bytes
+ $source_ip,
+ $dest_ip,
+ $source_port,
+ $dest_port
+ );
+ }
+ else {
+ @preamble = (
+ "PROXY", " ", # Request proxy mode
+ $opts{6} ? "TCP6" : "TCP4", " ", # inet6/4 and TCP (stream)
+ $source_ip, " ",
+ $dest_ip, " ",
+ $source_port, " ",
+ $dest_port,
+ "\x0d\x0a"
+ );
+ $status_line .= join "", @preamble;
+ }
+ print "\n", $status_line, "\n";
+ print "\n" if (!$opts{version} || $opts{version} == 2);
+ return @preamble;
+}
+
+sub binary_pack_port {
+ my $port = shift();
+ if ($opts{version} && $opts{version} == 1) {
+ return $port
+ if ($port && $port =~ /^\d+$/ && $port > 0 && $port < 65536);
+ die "Not a valid port: $port";
+ }
+ $status_line .= $port." ";
+ $port = pack "S", $port;
+ return $port;
+}
+
+sub binary_pack_ip {
+ my $ip = shift();
+ if ( $ip =~ m/\./ && !$opts{6}) {
+ if (IP4_valid($ip)) {
+ return $ip if ($opts{version} && $opts{version} == 1);
+ $status_line .= $ip.":";
+ $ip = pack "C*", split /\./, $ip;
+ }
+ else { die "Invalid IPv4: $ip"; }
+ }
+ elsif ($ip =~ m/:/ && $opts{6}) {
+ $ip = pad_ipv6($ip);
+ if (IP6_valid($ip)) {
+ return $ip if ($opts{version} && $opts{version} == 1);
+ $status_line .= $ip.":";
+ $ip = pack "S>*", map hex, split /:/, $ip;
+ }
+ else { die "Invalid IPv6: $ip"; }
+ }
+ else { die "Mismatching IP families passed: $ip"; }
+ return $ip;
+}
+
+sub pad_ipv6 {
+ my $ip = shift();
+ my @ip = split /:/, $ip;
+ my $segments = scalar @ip;
+ return $ip if ($segments == 8);
+ $ip = "";
+ for (my $count=1; $count <= $segments; $count++) {
+ my $block = $ip[$count-1];
+ if ($block) {
+ $ip .= $block;
+ $ip .= ":" unless $count == $segments;
+ }
+ elsif ($count == 1) {
+ # Somebody passed us ::1, fix it, but it's not really valid
+ $ip = "0:";
+ }
+ else {
+ $ip .= join ":", map "0", 0..(8-$segments);
+ $ip .= ":";
+ }
+ }
+ return $ip;
+}
+
+sub IP6_valid {
+ my $ip = shift;
+ $ip = lc($ip);
+ return 0 unless ($ip =~ /^[0-9a-f:]+$/);
+ my @ip = split /:/, $ip;
+ return 0 if (scalar @ip != 8);
+ return 1;
+}
+
+sub IP4_valid {
+ my $ip = shift;
+ $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+ foreach ($1,$2,$3,$4){
+ if ($_ <256 && $_ >0) {next;}
+ return 0;
+ }
+ return 1;
+}
+
+sub go_interactive {
+ my $continue = 1;
+ while($continue) {
+ # Check for input on both ends, recheck every 5 sec
+ for my $socket ($s->can_read(5)) {
+ my $remote = $socket_map{$socket};
+ my $buffer;
+ my $read = $socket->sysread($buffer, 4096);
+ if ($read) {
+ $remote->syswrite($buffer);
+ }
+ else {
+ $continue = 0;
+ }
+ }
+ }
+}
+
+sub connect_stdin_to_proxy {
+ my $sock = new IO::Socket::INET(
+ PeerAddr => $server_ip,
+ PeerPort => $server_port,
+ Proto => 'tcp'
+ );
+
+ die "Could not create socket: $!\n" unless $sock;
+ # Add sockets to the Select group
+ $s->add(\*STDIN);
+ $s->add($sock);
+ # Tie the sockets together using this hash
+ $socket_map{\*STDIN} = $sock;
+ $socket_map{$sock} = \*STDOUT;
+ return $sock;
+}
+
+sub usage {
+ chomp(my $prog = `basename $0`);
+ print <<EOF;
+Usage: $prog [required] [optional]
+ Required:
+ --server-ip IP of server to test proxy configuration,
+ a hostname is ok, but for only this setting
+ Optional:
+ --server-port Port server is listening on (default 25)
+ --6 IPv6 source/dest (default IPv4), if none specified,
+ some default, reverse resolvable IP's are used for
+ the source and dest ip/port
+ --dest-ip Public IP of the proxy server
+ --dest-port Port of public IP of proxy server
+ --source-ip IP connecting to the proxy server
+ --source-port Port of IP connecting to the proxy server
+ --help This output
+EOF
+ exit;
+}
+
+
+my $sock = connect_stdin_to_proxy();
+my @preamble = generate_preamble();
+print $sock @preamble;
+go_interactive();