From eb57651e8badf0b65af0371732e42f2ee5c7772c Mon Sep 17 00:00:00 2001 From: Todd Lyons Date: Thu, 17 Apr 2014 11:58:09 -0700 Subject: [PATCH] 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. --- doc/doc-txt/experimental-spec.txt | 21 ++- src/src/expand.c | 2 + src/src/globals.c | 2 + src/src/globals.h | 6 +- src/src/smtp_in.c | 109 ++++++++----- src/util/proxy_protocol_client.pl | 250 ++++++++++++++++++++++++++++++ 6 files changed, 347 insertions(+), 43 deletions(-) create mode 100644 src/util/proxy_protocol_client.pl 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 @@ -602,22 +602,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 * *************************************************/ @@ -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 +# +# +# 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 <