roundcube / roundcubemail

The Roundcube Webmail suite
https://roundcube.net
GNU General Public License v3.0
5.83k stars 1.63k forks source link

Feature request: PROXY protocol support for IMAP and SMTP #5334

Open logan893 opened 8 years ago

logan893 commented 8 years ago

Feature request to allow for HAproxy style PROXY protocol data to be passed to backend SMTP and IMAP servers. This will allow for unified IP filtering, as well as unified brute force login mitigation on the SMTP and IMAP server side. In addition to making it possible to restrict access on the IMAP and SMTP server side, their logs will also be more useful when they contain the actual IP addresses and not only the IP address of the Roundcube host server.

Support for this PROXY protocol exists in postfix, exim and dovecot.

http://www.haproxy.org/download/1.6/doc/proxy-protocol.txt

I'd like to get a discussion going to figure out the best way to do this, as I am not yet familiar with the design rules of Roundcube. I may be able to lend a hand with the implementation.

It would primarily be useful for the IMAP connection, as this is the first entry point, and SMTP access is likely not possible without this first step.

I don't think this can be done as a plugin, as it will require injection of some data in the form of the very first package sent over each new TCP connection.

As it is possible to specify multiple IMAP servers from a single Roundcube instance, it may also be useful to have a per-server configuration option. With an option it will be possible to disable (default) or select proxy version (1 or 2). This PROXY protocol relies on injected text (version 1) or binary (version 2) data as the very first TCP package, and it is strongly recommended that a server configured to use PROXY protocol does not accept connections without this initial PROXY protocol data.

To ensure backward compatibility, what is the best way to introduce such an option? For IMAP, add a global option, $config['imap_proxy_protocol'], as well as adding the possibility of per-host configuratinos via $config['default_host'] = array(). It could be introduced by making the description field also accept an array.

Current: $config['default_host'] = array( 'protocol://hostname:port' => '' );

With additional PROXY protocol support: $config['default_host'] = array( 'protocol://hostname:port' => array ( 'name' => '', 'proxy_protocol' => 'version(0,1,2)' ) );

Additional configuration parameters which may be useful: Name of headers which contain the source and destination IP addresses (and port numbers, if available).

logan893 commented 8 years ago

Here's a quick proof-of-concept which works quite well, using the latest roundcubemail master branch.

I added these lines to my config.inc.php:

$config['imap_proxy_protocol'] = 1;
$config['proxy_protocol_options'] = array(
  'remote_addr' => '1.2.3.4',
  'remote_port' => 5555,
  'local_addr'  => '2.3.4.5',
  'local_port'  => 6666,
);

Also works without proxy_protocol_options, and it will fall back to using REMOTE_ADDR, SERVER_ADDR, etc, for the proxy data.

roundcubemail\program\lib\Roundcube\rcube.php
@@ -377,6 +377,8 @@ class rcube
             'timeout'        => (int) $this->config->get("{$driver}_timeout"),
             'skip_deleted'   => (bool) $this->config->get('skip_deleted'),
             'driver'         => $driver,
+            'proxy_protocol' => (int) $this->config->get("{$driver}_proxy_protocol"),
+            'proxy_protocol_options'  => $this->config->get("proxy_protocol_options"),
         );

         if (!empty($_SESSION['storage_host'])) {
roundcubemail\program\lib\Roundcube\rcube_imap_generic.php
@@ -957,6 +957,24 @@ class rcube_imap_generic

             return false;
         }
+        
+        if ($this->prefs['proxy_protocol'] === 1) {
+          // text based PROXY protocol
+          $proxy_string = "PROXY TCP4 " .
+            (!empty($this->prefs['proxy_protocol_options']['remote_addr']) ? $this->prefs['proxy_protocol_options']['remote_addr'] : $_SERVER['REMOTE_ADDR'] ) .
+            " " .
+            (!empty($this->prefs['proxy_protocol_options']['local_addr']) ? $this->prefs['proxy_protocol_options']['local_addr'] : $_SERVER['SERVER_ADDR'] ) .
+            " " .
+            (!empty($this->prefs['proxy_protocol_options']['remote_port']) ? $this->prefs['proxy_protocol_options']['remote_port'] : $_SERVER['REMOTE_PORT'] ) .
+            " " .
+            (!empty($this->prefs['proxy_protocol_options']['local_port']) ? $this->prefs['proxy_protocol_options']['local_port'] : $_SERVER['SERVER_PORT'] ) .
+            "\r\n";
+          fputs($this->fp, $proxy_string);
+        }
+        else if ($this->prefs['proxy_protocol'] === 2) {
+          // binary PROXY protocol
+          // not yet implemented
+        }

         if ($this->prefs['timeout'] > 0) {
             stream_set_timeout($this->fp, $this->prefs['timeout']);

Jun 21 19:13:17 mailserver dovecot: imap-login: Login: user=<user@example.com>, method=PLAIN, rip=1.2.3.4, lip=2.3.4.5, mpid=52975, TLS, session=<xxxxxx> remote IP (rip) and local IP (lip) are now populated by this PROXY protocol information

alecpl commented 8 years ago

I propose to use existing imap_conn_options (and other *_conn_options) for configuration.

logan893 commented 8 years ago

I completely agree. I rewrote the proof of concept code to accommodate for this, and it could preferably be broken out into its own function, so that it can also be used for SMTP, and perhaps even managesieve, if desired.

I've added both version 1 (text) and version 2 (binary) of the protocol, as well as IPv6. I don't have IPv6 set up myself, so I cannot test the IPv6 part. Both version 1 and version 2 implementations of the proxy protocol works between my Roundcube test environment and my dovecot IMAP server.

Only change needed currently is in rcube_imap_generic.php

@@ -957,6 +957,62 @@ class rcube_imap_generic

             return false;
         }
+        
+        if (isset($this->prefs['socket_options']['proxy_protocol'])) {
+            if (is_array($this->prefs['socket_options']['proxy_protocol'])) {
+                $proxy_protocol_version = $this->prefs['socket_options']['proxy_protocol']['version'];
+                $proxy_protocol_options = $this->prefs['socket_options']['proxy_protocol'];
+            }
+            else {
+                $proxy_protocol_version = $this->prefs['socket_options']['proxy_protocol'];
+                $proxy_protocol_options = array();
+            }
+            
+            $proxy_protocol_remote_addr = (!empty($proxy_protocol_options['remote_addr']) ? $proxy_protocol_options['remote_addr'] : $_SERVER['REMOTE_ADDR'] );
+            $proxy_protocol_remote_port = (!empty($proxy_protocol_options['remote_port']) ? $proxy_protocol_options['remote_port'] : $_SERVER['REMOTE_PORT'] );
+            $proxy_protocol_local_addr = (!empty($proxy_protocol_options['local_addr']) ? $proxy_protocol_options['local_addr'] : $_SERVER['SERVER_ADDR'] );
+            $proxy_protocol_local_port = (!empty($proxy_protocol_options['local_port']) ? $proxy_protocol_options['local_port'] : $_SERVER['SERVER_PORT'] );
+            $proxy_protocol_ip_version = (strpos($proxy_protocol_remote_addr, ":") === false ? 4 : 6);
+            
+            if ($proxy_protocol_version === 1) {
+              // text based PROXY protocol
+              
+              // PROXY protocol does not support dual IPv6+IPv4 type addresses, e.g. ::127.0.0.1
+              if ($proxy_protocol_ip_version === 6 && strpos($proxy_protocol_remote_addr, ".") !== false) {
+                  $proxy_protocol_remote_addr = inet_ntop(inet_pton($proxy_protocol_remote_addr));
+              }
+              if ($proxy_protocol_ip_version === 6 && strpos($proxy_protocol_local_addr, ".") !== false) {
+                  $proxy_protocol_local_addr = inet_ntop(inet_pton($proxy_protocol_local_addr));
+              }
+              
+              $proxy_protocol_string = "PROXY " . // protocol header
+                ($proxy_protocol_ip_version === 6 ? "TCP6 " : "TCP4 ") . // IP version type
+                $proxy_protocol_remote_addr .
+                " " .
+                $proxy_protocol_local_addr .
+                " " .
+                $proxy_protocol_remote_port .
+                " " .
+                $proxy_protocol_local_port .
+                "\r\n";
+              fputs($this->fp, $proxy_protocol_string);
+            }
+            else if ($proxy_protocol_version === 2) {
+                // binary PROXY protocol
+                $proxy_protocol_hex = "0D0A0D0A000D0A515549540A" . // protocol header
+                  "21" . // protocol version and command
+                  ($proxy_protocol_ip_version === 6 ? "2" : "1") . // IP version type
+                  "1"; // TCP
+                $proxy_protocol_addr = inet_pton($proxy_protocol_remote_addr) .
+                  inet_pton($proxy_protocol_local_addr) .
+                  pack("n", $proxy_protocol_remote_port) .
+                  pack("n", $proxy_protocol_local_port);
+                $proxy_protocol_bin = pack("H*", $proxy_protocol_hex) . pack("n", strlen($proxy_protocol_addr)) .
+                  $proxy_protocol_addr;
+                
+              fputs($this->fp, $proxy_protocol_bin, strlen($proxy_protocol_bin));
+            }
+        }

         if ($this->prefs['timeout'] > 0) {
             stream_set_timeout($this->fp, $this->prefs['timeout']);
logan893 commented 8 years ago

Where would be a good place to put such a common TCP connectivity function? Would it be suitable for rcube_utils.php, or does it warrant its own file, e.g. rcube_proxy_protocol.php?

This does not have to be a wrapper in any way, it just has to be a static function, called upon in a timely manner, so as to send the very first TCP packet after connection setup, if the use of proxy protocol is configured.

alecpl commented 8 years ago

I think rcube_utils will be fine for now.

logan893 commented 8 years ago

Unfortunately SMTP is completely obscured by Net_SMTP, and proxy protocol support would need to be included in the middle of the connect function.

For IMAP support, I've created a pull request.

5335

alecpl commented 8 years ago

If we implement proxy protocol support in Net_Socket::connect() in similar fashion (i.e. with socket options) we'd get support for SMTP and Managesieve protocols "for free". Would you mind creating a PR at https://github.com/pear/Net_Socket?

njean42 commented 2 years ago

Hi everyone,

There is a roundcube plugin for sending the original client IP to dovecot. This makes dovecot aware of client IPs making IMAP login attempts, which enables brute-force and other protection mechanisms on the dovecot side (e.g. auth policy).

It would seem that we need a way to do the same for postfix / SMTP login attempts, which you @alecpl and @logan893 have been discussing with proxy protocol and NetSocket.

My newbie question: Does roundcube allow SMTP login attempts when the user is not logged in? (When using roundcube, I get the feeling that I need to be logged in -- IMAP login forwarded to dovecot -- before being able to send an email -- SMTP login attempt forwarded to postfix.)

Said otherwise, does roundcube provide an open, as in not-previously-logged-in interface for trying SMTP logins against the underlying postfix?

If not, and we consider IMAP login attempts protected by dovecot (that receives actual client IPs thanks to said plugin, and can blocks offending ones), then SMTP login attempts seem to be of lesser concern, no? (and so we have a complete alternative to proxy protocol -- which I'd still like to see happening!)