Issue with 5.0 not blocking IPs from Cloudflare

Hello,

After upgrading to v5.0 I noticed that failed login attempts were no longer generating a Cloudflare blocked rule. I have other Wordpress sites hosted on the same server running v4.4.0.4 and they are working correctly.

I believe I have everything configured correctly both in my wp-config.php:

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
	define('ABSPATH', dirname(__FILE__) . '/');

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

/**
 * WP Fail2ban Plugin
 * Cloudflare IP Ranges
 */
define('WP_FAIL2BAN_PROXIES', [
    '103.21.244.0/22',
    '103.22.200.0/22',
    '103.31.4.0/22',
    '104.16.0.0/13',
    '104.24.0.0/14',
    '108.162.192.0/18',
    '131.0.72.0/22',
    '141.101.64.0/18',
    '162.158.0.0/15',
    '172.64.0.0/13',
    '173.245.48.0/20',
    '188.114.96.0/20',
    '190.93.240.0/20',
    '197.234.240.0/22',
    '198.41.128.0/17',
    '2400:cb00::/32',
    '2606:4700::/32',
    '2803:f800::/32',
    '2405:b500::/32',
    '2405:8100::/32',
    '2a06:98c0::/29',
    '2c0f:f248::/32'

& in my nginx.conf

    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2405:b500::/32;
    set_real_ip_from 2405:8100::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;

    real_ip_header CF-Connecting-IP;

So after digging around the code I found the function that was causing an incorrect login attempt to show a “Forbidden” message and failing to create a Cloudflare IP block rule.

function _remote_addr(): ?IP
{
    try {
        // Typical path first
        if (!defined('WP_FAIL2BAN_REMOTE_ADDR')) {
            $ip = new IP($_SERVER['REMOTE_ADDR'], true, 'REMOTE_ADDR');

            if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) {

                /**
                 * User-defined proxies, typically upstream nginx
                 */
                $proxies = array_filter(Config::get('WP_FAIL2BAN_PROXIES'));

                if (empty($proxies)) {
                    // No proxies set; don't care about the header
                    return $ip;

                } elseif ($ip->inRanges($proxies, 'WP_FAIL2BAN_PROXIES')) {
                    // From a known proxy

                    $xIPs = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
                    return new IP($xIPs[0], true, 'HTTP_X_FORWARDED_FOR');

                } else {
                    // Not a known proxy: hard fail and bail out
                    Syslog::single(LOG_NOTICE, "Unknown Proxy with X-Forwarded-For", 'WP_FAIL2BAN_AUTH_LOG', (string)$ip);

                    do_action(__FUNCTION__.'.unknown_proxy', $ip);

                    bail();
                }

Specifically this part:

                } else {
                    // Not a known proxy: hard fail and bail out
                    Syslog::single(LOG_NOTICE, "Unknown Proxy with X-Forwarded-For", 'WP_FAIL2BAN_AUTH_LOG', (string)$ip);

                    do_action(__FUNCTION__.'.unknown_proxy', $ip);

                    bail();
                }

I ended up modifying this else statement to return the IP that was correctly set.

} else {
                    // Not a known proxy: hard fail and bail out
                    //BV Workaround
                    $xIPs = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
                    return new IP($xIPs[0], true, 'HTTP_X_FORWARDED_FOR');

//                    Syslog::single(LOG_NOTICE, "Unknown Proxy with X-Forwarded-For", 'WP_FAIL2BAN_AUTH_LOG', (string)$ip);
//
//                    do_action(__FUNCTION__.'.unknown_proxy', $ip);
//
//                    bail();
                }

My website is being correctly proxied through Cloudflare, headers indicate HIT for my resources.

My question at this point is: Is this a known bug or is this the correct behavior?

TL;DR: This is the correct behaviour.


The first problem I can see is that you’ve added the define() too late in wp-config.php. It must be set before the line that says:

/* That's all, stop editing! Happy publishing. */

or you’ll get weird and often unpredictable behaviour.

However, the main problem is that you’re telling both nginx and WPf2b to do the same thing:

  • The CF proxy connects to nginx
  • nginx says “Aha! I know that’s a proxy - I’ll translate the IP to the one the proxy says is the real one”
  • nginx passes the request to WordPress and WPf2b
  • WPf2b sees WP_FAIL2BAN_PROXIES has been defined so looks for the X-Forwarded-For header
  • WPf2b finds it, but, because nginx has already translated the IP, that IP isn’t in the list of trusted proxies
  • WPf2b bails with a hard fail

So, either:

  1. Use nginx to translate the IPs and don’t define WP_FAIL2BAN_PROXIES, or
  2. Define WP_FAIL2BAN_PROXIES and don’t use nginx to translate the IPs

Which option you pick is more of a philosophical question than technical. Personally, I think REMOTE_ADDR should be the address that’s actually connected to the server, so I go with option 2.

1 Like

That clears everything up for me. Much appreciated, thank you!

Glad that helped. I’m interested to know which option you went with and why - it might be useful info to feed back into the docs.

I tested both methods and they worked perfectly without any issues.

Since I’m hosting a large number of Wordpress sites that are using your awesome plugin (thanks!), I opted to have NGINX translate the IP to avoid having to manage this info within a ton of wp-config.php(s).

Initially I was hesitant to have NGINX do this since I was acutely concerned with a visitor spoofing the CF-Connecting-IP header but after some research (and testing) it appears that Cloudflare’s Edge drops the header and replaces it with your real one before forwarding. And of course REMOTE_ADDR is almost impossible to spoof due to its three-way handshake.

It would be interesting to see a benchmark comparing the two methods of translating IPs, but I assume the performance differences are marginal.

Thank again for your help!