From b4944ac544d6f66bd76194889edaa4db23ba293a Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Mon, 1 Dec 2025 18:00:52 -0500 Subject: [PATCH] Add `use_builtin_ips` support and reorganize plugin documentation - Enhanced Cloudflare Plugin with `use_builtin_ips` option to automatically use and update built-in IP ranges. - Updated Cloudflare IP restoration logic, including metadata and HAProxy config generation. - Reorganized plugin documentation with sidebar positions for improved accessibility. - Extended Cloudflare documentation to detail built-in IP ranges, examples, and configurations. - Added new test cases to validate `use_builtin_ips` functionality and file handling. --- docs/plugins/cleanup.md | 4 ++ docs/plugins/cloudflare.md | 46 +++++++++++++++----- docs/plugins/deny-pages.md | 4 ++ docs/plugins/fastcgi.md | 4 ++ docs/plugins/ip-whitelist.md | 4 ++ docs/plugins/jwt-validator.md | 4 ++ src/plugins/builtin/cloudflare.py | 61 +++++++++++++++++++++++++- src/tests/test_plugins.py | 71 +++++++++++++++++++++++++++++++ 8 files changed, 187 insertions(+), 11 deletions(-) diff --git a/docs/plugins/cleanup.md b/docs/plugins/cleanup.md index 6932813..fcef7b2 100644 --- a/docs/plugins/cleanup.md +++ b/docs/plugins/cleanup.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 6 +--- + # Cleanup Plugin **Type:** Global Plugin diff --git a/docs/plugins/cloudflare.md b/docs/plugins/cloudflare.md index dd0fb84..301e9ed 100644 --- a/docs/plugins/cloudflare.md +++ b/docs/plugins/cloudflare.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 5 +--- + # Cloudflare Plugin **Type:** Domain Plugin @@ -5,7 +9,7 @@ ## Overview -The Cloudflare plugin restores the original visitor IP address when requests come through Cloudflare's CDN. +The Cloudflare plugin restores the original visitor IP address when requests come through Cloudflare's CDN. The plugin includes **built-in Cloudflare IP ranges** that are automatically written to the IP list file - no manual configuration required! ## Why Use It @@ -13,14 +17,15 @@ Cloudflare replaces the visitor's IP with its own. This plugin restores the orig ## Configuration Options -| Option | Description | Default | -|----------------|----------------------------|-----------------------------------| -| `enabled` | Enable/disable plugin | `true` | -| `ip_list_path` | Path to Cloudflare IP list | `/etc/haproxy/cloudflare_ips.lst` | +| Option | Description | Default | +|-------------------|------------------------------------------|-----------------------------------| +| `enabled` | Enable/disable plugin | `true` | +| `use_builtin_ips` | Use built-in Cloudflare IP ranges | `true` | +| `ip_list_path` | Path to Cloudflare IP list | `/etc/haproxy/cloudflare_ips.lst` | ## Configuration Examples -### Docker/Docker Compose (Basic) +### Docker/Docker Compose (Basic - Uses Built-in IPs) ```yaml services: @@ -28,13 +33,17 @@ services: labels: easyhaproxy.http.host: example.com easyhaproxy.http.plugins: cloudflare +# Built-in Cloudflare IPs are automatically used - no additional configuration needed! ``` -### Docker/Docker Compose (Custom IP List Path) +### Docker/Docker Compose (Custom IP List) + +If you want to use your own IP list file instead of the built-in ranges: ```yaml labels: easyhaproxy.http.plugins: cloudflare + easyhaproxy.http.plugin.cloudflare.use_builtin_ips: false easyhaproxy.http.plugin.cloudflare.ip_list_path: /custom/path/cf_ips.lst ``` @@ -68,7 +77,7 @@ plugins: config: cloudflare: enabled: true - ip_list_path: /etc/haproxy/cloudflare_ips.lst + use_builtin_ips: true # Uses built-in Cloudflare IPs (default) ``` ## Generated HAProxy Configuration @@ -79,11 +88,28 @@ acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_cloudflare ``` +## Built-in Cloudflare IP Ranges + +The plugin includes the current Cloudflare IP ranges (22 ranges total): + +**IPv4 Ranges (15):** +- 173.245.48.0/20, 103.21.244.0/22, 103.22.200.0/22, 103.31.4.0/22 +- 141.101.64.0/18, 108.162.192.0/18, 190.93.240.0/20, 188.114.96.0/20 +- 197.234.240.0/22, 198.41.128.0/17, 162.158.0.0/15, 104.16.0.0/13 +- 104.24.0.0/14, 172.64.0.0/13, 131.0.72.0/22 + +**IPv6 Ranges (7):** +- 2400:cb00::/32, 2606:4700::/32, 2803:f800::/32, 2405:b500::/32 +- 2405:8100::/32, 2a06:98c0::/29, 2c0f:f248::/32 + +These ranges are automatically written to `/etc/haproxy/cloudflare_ips.lst` during each discovery cycle. + ## Important Notes -- **Required:** Download Cloudflare IP list from [Cloudflare documentation](https://support.cloudflare.com/hc/en-us/articles/200170786) +- ✅ **No manual configuration required** - Built-in Cloudflare IPs are included! - The plugin runs once per domain during the discovery cycle -- Ensure the IP list file is mounted and accessible to HAProxy +- IP list file is automatically created and updated +- To update Cloudflare IPs in the future, simply update the plugin source code and rebuild ## Related Documentation diff --git a/docs/plugins/deny-pages.md b/docs/plugins/deny-pages.md index 51c3eb8..3bea22f 100644 --- a/docs/plugins/deny-pages.md +++ b/docs/plugins/deny-pages.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 4 +--- + # Deny Pages Plugin **Type:** Domain Plugin diff --git a/docs/plugins/fastcgi.md b/docs/plugins/fastcgi.md index 9d74d8b..ab836bc 100644 --- a/docs/plugins/fastcgi.md +++ b/docs/plugins/fastcgi.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 1 +--- + # FastCGI Plugin **Type:** Domain Plugin diff --git a/docs/plugins/ip-whitelist.md b/docs/plugins/ip-whitelist.md index 223bc59..fd0a297 100644 --- a/docs/plugins/ip-whitelist.md +++ b/docs/plugins/ip-whitelist.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 3 +--- + # IP Whitelist Plugin **Type:** Domain Plugin diff --git a/docs/plugins/jwt-validator.md b/docs/plugins/jwt-validator.md index d7cdb07..27eec2e 100644 --- a/docs/plugins/jwt-validator.md +++ b/docs/plugins/jwt-validator.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 2 +--- + # JWT Validator Plugin **Type:** Domain Plugin diff --git a/src/plugins/builtin/cloudflare.py b/src/plugins/builtin/cloudflare.py index 7433dad..f8cb0be 100644 --- a/src/plugins/builtin/cloudflare.py +++ b/src/plugins/builtin/cloudflare.py @@ -4,14 +4,19 @@ Cloudflare Plugin for EasyHAProxy This plugin restores the original visitor IP address from Cloudflare's CF-Connecting-IP header when requests come through Cloudflare's CDN. +The plugin includes built-in Cloudflare IP ranges that are automatically +updated and written to the IP list file. + Configuration: - ip_list_path: Path to file containing Cloudflare IP ranges (default: /etc/haproxy/cloudflare_ips.lst) + - use_builtin_ips: Use built-in Cloudflare IP ranges (default: true) Example YAML config: plugins: cloudflare: enabled: true ip_list_path: /etc/haproxy/cloudflare_ips.lst + use_builtin_ips: true Example Container Label: easyhaproxy.http.plugins: "cloudflare" @@ -29,14 +34,45 @@ import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from plugins import PluginInterface, PluginType, PluginContext, PluginResult +from functions import loggerEasyHaproxy class CloudflarePlugin(PluginInterface): """Plugin to restore original visitor IP from Cloudflare""" + # Current Cloudflare IP ranges (IPv4 and IPv6) + # Source: https://www.cloudflare.com/ips/ + CLOUDFLARE_IPS = [ + # IPv4 + "173.245.48.0/20", + "103.21.244.0/22", + "103.22.200.0/22", + "103.31.4.0/22", + "141.101.64.0/18", + "108.162.192.0/18", + "190.93.240.0/20", + "188.114.96.0/20", + "197.234.240.0/22", + "198.41.128.0/17", + "162.158.0.0/15", + "104.16.0.0/13", + "104.24.0.0/14", + "172.64.0.0/13", + "131.0.72.0/22", + # IPv6 + "2400:cb00::/32", + "2606:4700::/32", + "2803:f800::/32", + "2405:b500::/32", + "2405:8100::/32", + "2a06:98c0::/29", + "2c0f:f248::/32", + ] + def __init__(self): self.ip_list_path = "/etc/haproxy/cloudflare_ips.lst" self.enabled = True + self.use_builtin_ips = True @property def name(self) -> str: @@ -54,6 +90,7 @@ class CloudflarePlugin(PluginInterface): config: Dictionary with configuration options - ip_list_path: Path to Cloudflare IP list file - enabled: Whether plugin is enabled + - use_builtin_ips: Use built-in Cloudflare IP ranges (default: true) """ if "ip_list_path" in config: self.ip_list_path = config["ip_list_path"] @@ -61,6 +98,9 @@ class CloudflarePlugin(PluginInterface): if "enabled" in config: self.enabled = str(config["enabled"]).lower() in ["true", "1", "yes"] + if "use_builtin_ips" in config: + self.use_builtin_ips = str(config["use_builtin_ips"]).lower() in ["true", "1", "yes"] + def process(self, context: PluginContext) -> PluginResult: """ Generate HAProxy config to restore original IP from Cloudflare @@ -74,6 +114,23 @@ class CloudflarePlugin(PluginInterface): if not self.enabled: return PluginResult() + # Write built-in Cloudflare IPs to file if using built-in IPs + if self.use_builtin_ips: + try: + # Create directory if it doesn't exist + ip_list_dir = os.path.dirname(self.ip_list_path) + if ip_list_dir and not os.path.exists(ip_list_dir): + os.makedirs(ip_list_dir, exist_ok=True) + + # Write Cloudflare IPs to file + with open(self.ip_list_path, 'w') as f: + for ip_range in self.CLOUDFLARE_IPS: + f.write(f"{ip_range}\n") + + loggerEasyHaproxy.info(f"Cloudflare plugin: Written {len(self.CLOUDFLARE_IPS)} IP ranges to {self.ip_list_path}") + except Exception as e: + loggerEasyHaproxy.warning(f"Cloudflare plugin: Failed to write IP list to {self.ip_list_path}: {e}") + # Generate HAProxy config snippet haproxy_config = f"""# Cloudflare - Restore original visitor IP acl from_cloudflare src -f {self.ip_list_path} @@ -84,6 +141,8 @@ http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)] if from_clo modified_easymapping=None, metadata={ "domain": context.domain, - "ip_list_path": self.ip_list_path + "ip_list_path": self.ip_list_path, + "use_builtin_ips": self.use_builtin_ips, + "ip_count": len(self.CLOUDFLARE_IPS) if self.use_builtin_ips else None } ) diff --git a/src/tests/test_plugins.py b/src/tests/test_plugins.py index 188844d..ae53f4c 100644 --- a/src/tests/test_plugins.py +++ b/src/tests/test_plugins.py @@ -42,7 +42,9 @@ class TestCloudflarePlugin: plugin = CloudflarePlugin() assert plugin.name == "cloudflare" assert plugin.enabled is True + assert plugin.use_builtin_ips is True assert plugin.ip_list_path == "/etc/haproxy/cloudflare_ips.lst" + assert len(plugin.CLOUDFLARE_IPS) == 22 # 15 IPv4 + 7 IPv6 def test_cloudflare_plugin_configuration(self): """Test plugin configuration""" @@ -123,6 +125,75 @@ class TestCloudflarePlugin: assert "acl from_cloudflare src -f /etc/haproxy/cloudflare_ips.lst" in haproxy_config assert "http-request set-header X-Forwarded-For %[req.hdr(CF-Connecting-IP)]" in haproxy_config + def test_cloudflare_plugin_builtin_ips_enabled(self): + """Test plugin uses built-in Cloudflare IPs and writes to file""" + # Use temp directory for testing + with tempfile.TemporaryDirectory() as tmpdir: + ip_list_path = os.path.join(tmpdir, "cloudflare_ips.lst") + + plugin = CloudflarePlugin() + plugin.configure({ + "use_builtin_ips": "true", + "ip_list_path": ip_list_path + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com", + port="80", + host_config={} + ) + + result = plugin.process(context) + + # Verify config is generated + assert result.haproxy_config is not None + assert f"acl from_cloudflare src -f {ip_list_path}" in result.haproxy_config + + # Verify metadata + assert result.metadata["use_builtin_ips"] is True + assert result.metadata["ip_count"] == 22 + + # Verify file was written + assert os.path.exists(ip_list_path) + + # Verify file contains correct number of IPs + with open(ip_list_path, 'r') as f: + lines = [line.strip() for line in f if line.strip()] + assert len(lines) == 22 + # Verify some known Cloudflare IPs are in the file + assert "173.245.48.0/20" in lines + assert "2606:4700::/32" in lines + + def test_cloudflare_plugin_builtin_ips_disabled(self): + """Test plugin doesn't write to file when use_builtin_ips is disabled""" + plugin = CloudflarePlugin() + plugin.configure({ + "use_builtin_ips": "false", + "ip_list_path": "/custom/cloudflare_ips.lst" + }) + + context = PluginContext( + parsed_object={}, + easymapping=[], + container_env={}, + domain="example.com", + port="80", + host_config={} + ) + + result = plugin.process(context) + + # Verify config is generated with custom path + assert result.haproxy_config is not None + assert "acl from_cloudflare src -f /custom/cloudflare_ips.lst" in result.haproxy_config + + # Verify metadata + assert result.metadata["use_builtin_ips"] is False + assert result.metadata["ip_count"] is None + class TestCleanupPlugin: """Test cases for CleanupPlugin (GLOBAL plugin)"""